|
| 1 | +""" |
| 2 | +Input/output class for the Plot3D file format (formatted, ASCII) |
| 3 | +""" |
| 4 | +import numpy as np |
| 5 | +import pandas as pd |
| 6 | +import os |
| 7 | + |
| 8 | +try: |
| 9 | + from .file import File, WrongFormatError, BrokenFormatError, EmptyFileError |
| 10 | +except: |
| 11 | + File = dict |
| 12 | + EmptyFileError = type('EmptyFileError', (Exception,),{}) |
| 13 | + WrongFormatError = type('WrongFormatError', (Exception,),{}) |
| 14 | + BrokenFormatError = type('BrokenFormatError', (Exception,),{}) |
| 15 | + |
| 16 | +class Plot3DFile(File): |
| 17 | + """ |
| 18 | + Read/write a Plot3D file (formatted, ASCII). The object behaves as a dictionary. |
| 19 | +
|
| 20 | + Main methods |
| 21 | + ------------ |
| 22 | + - read, write, toDataFrame, keys |
| 23 | +
|
| 24 | + Examples |
| 25 | + -------- |
| 26 | + f = Plot3DFile('file.fmt') |
| 27 | + print(f.keys()) |
| 28 | + print(f.toDataFrame().columns) |
| 29 | + """ |
| 30 | + |
| 31 | + @staticmethod |
| 32 | + def defaultExtensions(): |
| 33 | + """ List of file extensions expected for this fileformat""" |
| 34 | + return ['.g', '.x', '.y', '.xy', '.xyz', '.fmt'] |
| 35 | + |
| 36 | + @staticmethod |
| 37 | + def formatName(): |
| 38 | + return 'Plot3D formatted ASCII file' |
| 39 | + |
| 40 | + @staticmethod |
| 41 | + def priority(): return 60 # Priority in weio.read fileformat list between 0=high and 100:low |
| 42 | + |
| 43 | + def __init__(self, filename=None, **kwargs): |
| 44 | + """ Class constructor. If a `filename` is given, the file is read. """ |
| 45 | + self.filename = filename |
| 46 | + if filename: |
| 47 | + self.read(**kwargs) |
| 48 | + |
| 49 | + def read(self, filename=None, verbose=False, method='numpy'): |
| 50 | + """ Reads the file self.filename, or `filename` if provided """ |
| 51 | + if filename: |
| 52 | + self.filename = filename |
| 53 | + if not self.filename: |
| 54 | + raise Exception('No filename provided') |
| 55 | + if not os.path.isfile(self.filename): |
| 56 | + raise OSError(2, 'File not found:', self.filename) |
| 57 | + if os.stat(self.filename).st_size == 0: |
| 58 | + raise EmptyFileError('File is empty:', self.filename) |
| 59 | + |
| 60 | + coords_list, dims = read_plot3d(self.filename, verbose=verbose, method=method, singleblock=False) |
| 61 | + self['coords'] = coords_list |
| 62 | + self['dims'] = dims |
| 63 | + |
| 64 | + def write(self, filename=None): |
| 65 | + """ Rewrite object to file, or write object to `filename` if provided """ |
| 66 | + if filename: |
| 67 | + self.filename = filename |
| 68 | + if not self.filename: |
| 69 | + raise Exception('No filename provided') |
| 70 | + |
| 71 | + write_plot3d(self.filename, self['coords'], self['dims'], singleblock=False) |
| 72 | + |
| 73 | + def toDataFrame(self): |
| 74 | + """ Returns one DataFrame (single block) or a dict of DataFrames (multi-block) """ |
| 75 | + coords_list = self.get('coords', None) |
| 76 | + if coords_list is None: |
| 77 | + raise Exception("No coordinates loaded.") |
| 78 | + if len(coords_list) == 1: |
| 79 | + coords = coords_list[0] |
| 80 | + df = pd.DataFrame(coords, columns=['x', 'y', 'z']) |
| 81 | + return df |
| 82 | + else: |
| 83 | + dfs={} |
| 84 | + for i, coords in enumerate(coords_list): |
| 85 | + df = pd.DataFrame(coords, columns=['x', 'y', 'z']) |
| 86 | + dfs[f'block_{i}'] = df |
| 87 | + return dfs |
| 88 | + |
| 89 | + def __repr__(self): |
| 90 | + s = '<{} object>:\n'.format(type(self).__name__) |
| 91 | + s += '|Main attributes:\n' |
| 92 | + s += '| - filename: {}\n'.format(self.filename) |
| 93 | + if 'dims' in self: |
| 94 | + for i, dims in enumerate(self['dims']): |
| 95 | + s += '| - dims[{}]: shape {}\n'.format(i, dims) |
| 96 | + if 'coords' in self: |
| 97 | + for i, coords in enumerate(self['coords']): |
| 98 | + s += '| - coords[{}]: shape {}\n'.format(i, coords.shape) |
| 99 | + s += '|Main methods:\n' |
| 100 | + s += '| - read, write, toDataFrame, keys' |
| 101 | + return s |
| 102 | + |
| 103 | + def toString(self): |
| 104 | + """ """ |
| 105 | + s = '' |
| 106 | + return s |
| 107 | + |
| 108 | + def plot(self, ax=None, **kwargs): |
| 109 | + """ |
| 110 | + Plots the x, y coordinates as a scatter plot. |
| 111 | +
|
| 112 | + Parameters |
| 113 | + ---------- |
| 114 | + ax : matplotlib.axes.Axes, optional |
| 115 | + Existing matplotlib axes to plot on. If None, a new figure and axes are created. |
| 116 | + **kwargs : dict |
| 117 | + Additional keyword arguments passed to plt.scatter. |
| 118 | + """ |
| 119 | + import matplotlib.pyplot as plt |
| 120 | + dfs = self.toDataFrame() |
| 121 | + if isinstance(dfs, dict): |
| 122 | + for key, df in dfs.items(): |
| 123 | + if ax is None: |
| 124 | + fig, ax = plt.subplots() |
| 125 | + ax.scatter(df['x'], df['y'], label=key, **kwargs) |
| 126 | + ax.legend() |
| 127 | + else: |
| 128 | + df = dfs |
| 129 | + if ax is None: |
| 130 | + fig, ax = plt.subplots() |
| 131 | + ax.scatter(df['x'], df['y'], **kwargs) |
| 132 | + ax.set_xlabel('x') |
| 133 | + ax.set_ylabel('y') |
| 134 | + ax.set_title('Plot3D x-y Scatter') |
| 135 | + ax.axis('equal') |
| 136 | + ax.grid(True) |
| 137 | + return ax |
| 138 | + |
| 139 | + |
| 140 | + |
| 141 | + |
| 142 | +# --------------------------------------------------------------------------------} |
| 143 | +# --- Low level functions |
| 144 | +# --------------------------------------------------------------------------------{ |
| 145 | +def read_plot3d(filename, verbose=False, method='numpy', singleblock=True): |
| 146 | + """ |
| 147 | + Reads a simple multi-block Plot3D file (formatted, ASCII). |
| 148 | + Returns: |
| 149 | + coords_list: list of (n_points, 3) arrays, one per block |
| 150 | + dims: (n_blocks, 3) list of (ni, nj, nk) |
| 151 | + """ |
| 152 | + coords_list = [] |
| 153 | + if method == 'numpy': |
| 154 | + dims = np.loadtxt(filename, skiprows=1, max_rows=1, dtype=int) |
| 155 | + coords = np.loadtxt(filename, skiprows=2) |
| 156 | + coords = coords.reshape((3, dims[0]*dims[1]*dims[2])).transpose() |
| 157 | + dims = [dims] |
| 158 | + coords_list = [coords] |
| 159 | + else: |
| 160 | + with open(filename, "r") as f: |
| 161 | + nblocks = int(f.readline()) |
| 162 | + dims = [] |
| 163 | + for _ in range(nblocks): |
| 164 | + dims.append(tuple(int(x) for x in f.readline().split())) |
| 165 | + for block in range(nblocks): |
| 166 | + ni, nj, nk = dims[block] |
| 167 | + npts = ni * nj * nk |
| 168 | + block_coords = np.zeros((npts, 3)) |
| 169 | + for idim in range(3): |
| 170 | + for k in range(nk): |
| 171 | + for j in range(nj): |
| 172 | + for i in range(ni): |
| 173 | + idx = i + j * ni + k * ni * nj |
| 174 | + val = float(f.readline()) |
| 175 | + block_coords[idx, idim] = val |
| 176 | + coords_list.append(block_coords) |
| 177 | + if singleblock and len(coords_list) == 1: |
| 178 | + return coords_list[0], dims[0] |
| 179 | + if verbose: |
| 180 | + for i, arr in enumerate(coords_list): |
| 181 | + print(f"Block {i}: shape {arr.shape}, dims {dims[i]}") |
| 182 | + x_min, x_max = arr[:, 0].min(), arr[:, 0].max() |
| 183 | + y_min, y_max = arr[:, 1].min(), arr[:, 1].max() |
| 184 | + z_min, z_max = arr[:, 2].min(), arr[:, 2].max() |
| 185 | + print(f" x range: [{x_min:.6f}, {x_max:.6f}]") |
| 186 | + print(f" y range: [{y_min:.6f}, {y_max:.6f}]") |
| 187 | + print(f" z range: [{z_min:.6f}, {z_max:.6f}]") |
| 188 | + return coords_list, dims |
| 189 | + |
| 190 | +def write_plot3d(filename, coords_list, dims, singleblock=False): |
| 191 | + """ |
| 192 | + Writes a simple multi-block Plot3D file (formatted, ASCII). |
| 193 | + Args: |
| 194 | + filename: Output file name |
| 195 | + coords_list: list of (n_points, 3) arrays, one per block |
| 196 | + dims: (n_blocks, 3) list of (ni, nj, nk) or a single (ni, nj, nk) |
| 197 | + singleblock: If True, write as single block (no nblocks header) |
| 198 | + """ |
| 199 | + if singleblock: |
| 200 | + coords_list = [coords_list] # Ensure coords_list is a list with one block |
| 201 | + dims = [dims] # Ensure dims is a list with one block |
| 202 | + # Ensure coords_list is a list |
| 203 | + if not isinstance(coords_list, list): |
| 204 | + coords_list = [coords_list] |
| 205 | + with open(filename, "w") as f: |
| 206 | + nblocks = len(coords_list) |
| 207 | + f.write(f"{nblocks}\n") |
| 208 | + |
| 209 | + if nblocks == 1: |
| 210 | + ni, nj, nk = dims if isinstance(dims, (list, tuple)) and len(dims) == 3 else dims[0] |
| 211 | + f.write(f"{ni} {nj} {nk}\n") |
| 212 | + coords = coords_list[0] |
| 213 | + for idim in range(3): |
| 214 | + for idx in range(coords.shape[0]): |
| 215 | + f.write(f"{coords[idx, idim]}\n") |
| 216 | + else: |
| 217 | + for block, (coords, (ni, nj, nk)) in enumerate(zip(coords_list, dims)): |
| 218 | + f.write(f"{ni} {nj} {nk}\n") |
| 219 | + for idim in range(3): |
| 220 | + for idx in range(coords.shape[0]): |
| 221 | + f.write(f"{coords[idx, idim]}\n") |
| 222 | + #for k in range(nk): |
| 223 | + # for j in range(nj): |
| 224 | + # for i in range(ni): |
| 225 | + # idx = i + j * ni + k * ni * nj |
| 226 | + # f.write(f"{coords[idx, idim]}\n") |
| 227 | + |
| 228 | + |
| 229 | +if __name__ == '__main__': |
| 230 | + import matplotlib.pyplot as plt |
| 231 | + p3d = Plot3DFile('C:/Work/cfd/nalulib/examples/airfoils/naca0012_blunt.fmt') |
| 232 | + print(p3d) |
| 233 | + p3d.plot() |
| 234 | + p3d.write('C:/Work/cfd/nalulib/examples/airfoils/_naca0012_blunt_out.fmt') |
| 235 | + plt.show() |
| 236 | + |
| 237 | + |
0 commit comments