Skip to content

Commit 5fbd6eb

Browse files
author
Emmanuel Branlard
committed
IO: adding plot3d file
1 parent dc5587a commit 5fbd6eb

File tree

2 files changed

+239
-0
lines changed

2 files changed

+239
-0
lines changed

pydatview/io/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def fileFormats(userpath=None, ignoreErrors=False, verbose=False):
6969
from .raawmat_file import RAAWMatFile
7070
from .rosco_discon_file import ROSCODISCONFile
7171
from .rosco_performance_file import ROSCOPerformanceFile
72+
from .plot3d_file import Plot3DFile
7273
priorities = []
7374
formats = []
7475
def addFormat(priority, fmt):
@@ -108,6 +109,7 @@ def addFormat(priority, fmt):
108109
addFormat(60, FileFormat(GNUPlotFile))
109110
addFormat(60, FileFormat(ParquetFile))
110111
addFormat(60, FileFormat(PickleFile))
112+
addFormat(60, FileFormat(Plot3DFile))
111113
addFormat(70, FileFormat(CactusFile))
112114
addFormat(70, FileFormat(RAAWMatFile))
113115

pydatview/io/plot3d_file.py

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
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

Comments
 (0)