cffview 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cffview/boundary.py +306 -0
- cffview/main.py +524 -0
- cffview/utils.py +125 -0
- cffview-0.2.0.dist-info/METADATA +128 -0
- cffview-0.2.0.dist-info/RECORD +9 -0
- cffview-0.2.0.dist-info/WHEEL +5 -0
- cffview-0.2.0.dist-info/entry_points.txt +2 -0
- cffview-0.2.0.dist-info/licenses/LICENSE +28 -0
- cffview-0.2.0.dist-info/top_level.txt +1 -0
cffview/boundary.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BoundaryFactory:
|
|
5
|
+
_REGISTRY = {}
|
|
6
|
+
|
|
7
|
+
@classmethod
|
|
8
|
+
def register(cls, type_name: str):
|
|
9
|
+
def decorator(subclass):
|
|
10
|
+
cls._REGISTRY[type_name] = subclass
|
|
11
|
+
return subclass
|
|
12
|
+
return decorator
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def create(cls, name: str, id_: str, type_: str):
|
|
16
|
+
boundary_cls = cls._REGISTRY.get(type_, NotImplementedBoundary)
|
|
17
|
+
return boundary_cls(name, id_)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
@BoundaryFactory.register('fluid')
|
|
22
|
+
class Fluid:
|
|
23
|
+
name: str
|
|
24
|
+
id_: str
|
|
25
|
+
material: str = ''
|
|
26
|
+
sources: str = ''
|
|
27
|
+
fixed: str = ''
|
|
28
|
+
mrf_motion: str = ''
|
|
29
|
+
mgrid_motion: str = ''
|
|
30
|
+
solid_motion: str = ''
|
|
31
|
+
laminar: str = ''
|
|
32
|
+
porous: str = ''
|
|
33
|
+
fanzone: str = ''
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
@BoundaryFactory.register('solid')
|
|
38
|
+
class Solid:
|
|
39
|
+
name: str
|
|
40
|
+
id_: str
|
|
41
|
+
material: str = ''
|
|
42
|
+
sources: str = ''
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
@BoundaryFactory.register('velocity-inlet')
|
|
47
|
+
class VelocityInlet:
|
|
48
|
+
name: str
|
|
49
|
+
id_: str
|
|
50
|
+
velocity_spec: str = ''
|
|
51
|
+
frame_of_reference: str = ''
|
|
52
|
+
vmag: str = ''
|
|
53
|
+
t: str = ''
|
|
54
|
+
ke_spec: str = ''
|
|
55
|
+
turb_intensity: str = ''
|
|
56
|
+
turb_hydraulic_diam: str = ''
|
|
57
|
+
turb_viscosity_ratio: str = ''
|
|
58
|
+
|
|
59
|
+
_VELOCITY_SPEC = {
|
|
60
|
+
'1': 'Magnitude and Direction',
|
|
61
|
+
'2': 'Magnitude, Normal to Boundary',
|
|
62
|
+
'3': 'Components',
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_FRAME_OF_REFERENCE = {
|
|
66
|
+
'0': 'Absolute',
|
|
67
|
+
'1': 'Ralative to Adjacent Cell Zone',
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_KE_SPEC = {
|
|
71
|
+
'1': 'Intensity and Length Scale',
|
|
72
|
+
'2': 'Intensity and Viscosity Ratio',
|
|
73
|
+
'3': 'Intensity and Hydraulic Diameter',
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
def to_dict(self) -> dict[str, str]:
|
|
77
|
+
data = self.__dict__.copy()
|
|
78
|
+
|
|
79
|
+
data['velocity_spec'] = self._VELOCITY_SPEC.get(self.velocity_spec, 'unknown')
|
|
80
|
+
data['frame_of_reference'] = self._FRAME_OF_REFERENCE.get(self.frame_of_reference, 'unknown')
|
|
81
|
+
data['ke_spec'] = self._KE_SPEC.get(self.ke_spec, 'unknown')
|
|
82
|
+
|
|
83
|
+
match data['ke_spec']:
|
|
84
|
+
case 'Intensity and Length Scale':
|
|
85
|
+
data.pop('turb_viscosity_ratio', None)
|
|
86
|
+
data.pop('turb_hydraulic_diam', None)
|
|
87
|
+
case 'Intensity and Viscosity Ratio':
|
|
88
|
+
data.pop('turb_length_scale', None)
|
|
89
|
+
data.pop('turb_hydraulic_diam', None)
|
|
90
|
+
case 'Intensity and Hydraulic Diameter':
|
|
91
|
+
data.pop('turb_length_scale', None)
|
|
92
|
+
data.pop('turb_viscosity_ratio', None)
|
|
93
|
+
|
|
94
|
+
return data
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
@BoundaryFactory.register('mass-flow-inlet')
|
|
99
|
+
class MassFlowInlet:
|
|
100
|
+
name: str
|
|
101
|
+
id_: str
|
|
102
|
+
mass_flow: str = ''
|
|
103
|
+
t: str = ''
|
|
104
|
+
turb_intensity: str = ''
|
|
105
|
+
turb_hydraulic_diam: str = ''
|
|
106
|
+
turb_viscosity_ratio: str = ''
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
@BoundaryFactory.register('mass-flow-outlet')
|
|
111
|
+
class MassFlowOutlet:
|
|
112
|
+
name: str
|
|
113
|
+
id_: str
|
|
114
|
+
mass_flow: str = ''
|
|
115
|
+
t: str = ''
|
|
116
|
+
turb_intensity: str = ''
|
|
117
|
+
turb_hydraulic_diam: str = ''
|
|
118
|
+
turb_viscosity_ratio: str = ''
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
@BoundaryFactory.register('pressure-inlet')
|
|
123
|
+
class PressureInlet:
|
|
124
|
+
name: str
|
|
125
|
+
id_: str
|
|
126
|
+
frame_of_reference: str = ''
|
|
127
|
+
p0: str = ''
|
|
128
|
+
p: str = ''
|
|
129
|
+
t0: str = ''
|
|
130
|
+
ke_spec: str = ''
|
|
131
|
+
prevent_reverse_flow: str = ''
|
|
132
|
+
turb_intensity: str = ''
|
|
133
|
+
turb_length_scale: str = ''
|
|
134
|
+
turb_hydraulic_diam: str = ''
|
|
135
|
+
turb_viscosity_ratio: str = ''
|
|
136
|
+
|
|
137
|
+
_FRAME_OF_REFERENCE = {
|
|
138
|
+
'0': 'Absolute',
|
|
139
|
+
'1': 'Ralative to Adjacent Cell Zone',
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
_KE_SPEC = {
|
|
143
|
+
'1': 'Intensity and Length Scale',
|
|
144
|
+
'2': 'Intensity and Viscosity Ratio',
|
|
145
|
+
'3': 'Intensity and Hydraulic Diameter',
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
def to_dict(self) -> dict[str, str]:
|
|
149
|
+
data = self.__dict__.copy()
|
|
150
|
+
|
|
151
|
+
data['frame_of_reference'] = self._FRAME_OF_REFERENCE.get(self.frame_of_reference, 'unknown')
|
|
152
|
+
data['ke_spec'] = self._KE_SPEC.get(self.ke_spec, 'unknown')
|
|
153
|
+
|
|
154
|
+
match data['ke_spec']:
|
|
155
|
+
case 'Intensity and Length Scale':
|
|
156
|
+
data.pop('turb_viscosity_ratio', None)
|
|
157
|
+
data.pop('turb_hydraulic_diam', None)
|
|
158
|
+
case 'Intensity and Viscosity Ratio':
|
|
159
|
+
data.pop('turb_length_scale', None)
|
|
160
|
+
data.pop('turb_hydraulic_diam', None)
|
|
161
|
+
case 'Intensity and Hydraulic Diameter':
|
|
162
|
+
data.pop('turb_length_scale', None)
|
|
163
|
+
data.pop('turb_viscosity_ratio', None)
|
|
164
|
+
|
|
165
|
+
return data
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass
|
|
169
|
+
@BoundaryFactory.register('pressure-outlet')
|
|
170
|
+
class PressureOutlet:
|
|
171
|
+
name: str
|
|
172
|
+
id_: str
|
|
173
|
+
p: str = ''
|
|
174
|
+
t0: str = ''
|
|
175
|
+
ke_spec: str = ''
|
|
176
|
+
prevent_reverse_flow: str = ''
|
|
177
|
+
radial: str = ''
|
|
178
|
+
avg_press_spec: str = ''
|
|
179
|
+
turb_intensity: str = ''
|
|
180
|
+
turb_length_scale: str = ''
|
|
181
|
+
targeted_mf_boundary: str = ''
|
|
182
|
+
turb_hydraulic_diam: str = ''
|
|
183
|
+
turb_viscosity_ratio: str = ''
|
|
184
|
+
|
|
185
|
+
_KE_SPEC = {
|
|
186
|
+
'1': 'Intensity and Length Scale',
|
|
187
|
+
'2': 'Intensity and Viscosity Ratio',
|
|
188
|
+
'3': 'Intensity and Hydraulic Diameter',
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
def to_dict(self) -> dict[str, str]:
|
|
192
|
+
data = self.__dict__.copy()
|
|
193
|
+
|
|
194
|
+
if self.prevent_reverse_flow == '#t':
|
|
195
|
+
for key in ['t', 'ke_spec', 'turb_intensity', 'turb_length_scale', 'targeted_mf_boundary', 'turb_hydraulic_diam', 'turb_viscosity_ratio']:
|
|
196
|
+
data.pop(key, None)
|
|
197
|
+
else:
|
|
198
|
+
data['ke_spec'] = self._KE_SPEC.get(self.ke_spec, 'unknown')
|
|
199
|
+
match data['ke_spec']:
|
|
200
|
+
case 'Intensity and Length Scale':
|
|
201
|
+
data.pop('turb_viscosity_ratio', None)
|
|
202
|
+
data.pop('turb_hydraulic_diam', None)
|
|
203
|
+
case 'Intensity and Viscosity Ratio':
|
|
204
|
+
data.pop('turb_length_scale', None)
|
|
205
|
+
data.pop('turb_hydraulic_diam', None)
|
|
206
|
+
case 'Intensity and Hydraulic Diameter':
|
|
207
|
+
data.pop('turb_length_scale', None)
|
|
208
|
+
data.pop('turb_viscosity_ratio', None)
|
|
209
|
+
|
|
210
|
+
return data
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass
|
|
214
|
+
@BoundaryFactory.register('wall')
|
|
215
|
+
class Wall:
|
|
216
|
+
name: str
|
|
217
|
+
id_: str
|
|
218
|
+
d: str = ''
|
|
219
|
+
q_dot: str = ''
|
|
220
|
+
material: str = ''
|
|
221
|
+
thermal_bc: str = ''
|
|
222
|
+
t: str = ''
|
|
223
|
+
q: str = ''
|
|
224
|
+
h: str = ''
|
|
225
|
+
motion_bc: str = ''
|
|
226
|
+
shear_bc: str = ''
|
|
227
|
+
rough_bc: str = ''
|
|
228
|
+
moving: str = ''
|
|
229
|
+
relative: str = ''
|
|
230
|
+
roughness_height: str = ''
|
|
231
|
+
roughness_const: str = ''
|
|
232
|
+
|
|
233
|
+
_THERMAL_BC = {
|
|
234
|
+
'1': 'Heat Flux',
|
|
235
|
+
'2': 'Temperature',
|
|
236
|
+
'3': 'Coupled',
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
_MOTION_BC = {}
|
|
240
|
+
|
|
241
|
+
_SHEAR_BC = {}
|
|
242
|
+
|
|
243
|
+
_ROUGH_BC = {}
|
|
244
|
+
|
|
245
|
+
_THERMAL_BC_WHITELIST = {
|
|
246
|
+
'1': {'q_dot'}, # Heat Flux
|
|
247
|
+
'2': {'t'}, # Temperature
|
|
248
|
+
'3': set(), # Coupled
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
def to_dict(self) -> dict[str, str]:
|
|
252
|
+
data = self.__dict__.copy()
|
|
253
|
+
|
|
254
|
+
allowed_attrs = self._THERMAL_BC_WHITELIST.get(self.thermal_bc, set())
|
|
255
|
+
all_thermal_attrs = {'t', 'q_dot', 'h', 'q'}
|
|
256
|
+
attrs_to_remove = all_thermal_attrs - allowed_attrs
|
|
257
|
+
for attr in attrs_to_remove:
|
|
258
|
+
data.pop(attr, None)
|
|
259
|
+
|
|
260
|
+
data['thermal_bc'] = self._THERMAL_BC.get(self.thermal_bc, 'unknown')
|
|
261
|
+
|
|
262
|
+
return data
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@dataclass
|
|
266
|
+
@BoundaryFactory.register('porous-jump')
|
|
267
|
+
class PorousJump:
|
|
268
|
+
name: str
|
|
269
|
+
id_: str
|
|
270
|
+
alpha: str = ''
|
|
271
|
+
dm: str = ''
|
|
272
|
+
c2: str = ''
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@dataclass
|
|
276
|
+
@BoundaryFactory.register('fan')
|
|
277
|
+
class Fan:
|
|
278
|
+
name: str
|
|
279
|
+
id_: str
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@dataclass
|
|
283
|
+
@BoundaryFactory.register('radiator')
|
|
284
|
+
class Radiator:
|
|
285
|
+
name: str
|
|
286
|
+
id_: str
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@dataclass
|
|
290
|
+
@BoundaryFactory.register('interior')
|
|
291
|
+
class Interior:
|
|
292
|
+
name: str
|
|
293
|
+
id_: str
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@dataclass
|
|
297
|
+
@BoundaryFactory.register('symmetry')
|
|
298
|
+
class Symmetry:
|
|
299
|
+
name: str
|
|
300
|
+
id_: str
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@dataclass
|
|
304
|
+
class NotImplementedBoundary:
|
|
305
|
+
name: str
|
|
306
|
+
id_: str
|
cffview/main.py
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
def print_version(file_path: str) -> str:
|
|
2
|
+
"""Get the version of the .h5 file
|
|
3
|
+
|
|
4
|
+
Parameters
|
|
5
|
+
---------
|
|
6
|
+
file_path : str
|
|
7
|
+
Path to the .h5 file
|
|
8
|
+
|
|
9
|
+
Returns
|
|
10
|
+
-------
|
|
11
|
+
str
|
|
12
|
+
Version of the .h5 file
|
|
13
|
+
"""
|
|
14
|
+
import h5py
|
|
15
|
+
|
|
16
|
+
with h5py.File(file_path, "r") as f:
|
|
17
|
+
print(f['/settings/Version'][0].decode())
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def read_case(file_path: str, **kwargs) -> dict[str]:
|
|
21
|
+
"""Read the cas.h5 file
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
---------
|
|
25
|
+
file_path : str
|
|
26
|
+
Path to the cas.h5 file
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
dict[str]
|
|
31
|
+
A dictionary containing the case settings
|
|
32
|
+
"""
|
|
33
|
+
import re
|
|
34
|
+
import h5py
|
|
35
|
+
|
|
36
|
+
with h5py.File(file_path) as f:
|
|
37
|
+
settings: h5py.Group = f['/settings']
|
|
38
|
+
general_info = settings['Rampant Variables'][0].decode()
|
|
39
|
+
boundary_info = settings['Thread Variables'][0].decode()
|
|
40
|
+
|
|
41
|
+
if not any(kwargs.values()):
|
|
42
|
+
kwargs = dict.fromkeys(kwargs.keys(), True)
|
|
43
|
+
|
|
44
|
+
data = {}
|
|
45
|
+
|
|
46
|
+
if kwargs['solver']:
|
|
47
|
+
case_config = re.search(
|
|
48
|
+
r'^\(case-config.*',
|
|
49
|
+
general_info,
|
|
50
|
+
re.M
|
|
51
|
+
).group()
|
|
52
|
+
kvs = {
|
|
53
|
+
m[0]: m[1]
|
|
54
|
+
for m in re.findall(
|
|
55
|
+
r"\(([^()\s]+)\s+\.\s+([^()\s]+)\)",
|
|
56
|
+
case_config
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
data['solver'] = {}
|
|
61
|
+
data['solver']['type'] = "pbns" if kvs['rp-seg?'] == "#t" else "dbns"
|
|
62
|
+
data['solver']['time'] = "transient" if kvs['rp-unsteady?'] == "#t" else "steady"
|
|
63
|
+
data['solver']['dimension'] = "3d" if kvs['rp-3d?'] == "#t" else "2d"
|
|
64
|
+
data['solver']['precision'] = "double" if kvs['rp-double?'] == "#t" else "single"
|
|
65
|
+
data['solver']['axi'] = "true" if kvs['rp-axi?'] == "#t" else "false"
|
|
66
|
+
data['solver']['init'] = "hybrid" if kvs['hyb-init?'] == "#t" else "standard"
|
|
67
|
+
|
|
68
|
+
if kvs['rp-visc?'] == "#f":
|
|
69
|
+
data['solver']['turb'] = "inviscid"
|
|
70
|
+
else:
|
|
71
|
+
for key in [
|
|
72
|
+
'rp-lam?', 'rp-ke?', 'rp-kw?', 'rp-sa?', 'sg-rsm?',
|
|
73
|
+
'rp-les?', 'rp-des?', 'rp-kklw', 'rp-v2f?'
|
|
74
|
+
]:
|
|
75
|
+
if kvs[key] == "#t":
|
|
76
|
+
data['solver']['turb'] = key[3:-1]
|
|
77
|
+
break
|
|
78
|
+
|
|
79
|
+
data['solver']['energy'] = "true" if kvs['rf-energy?'] == "#t" else "false"
|
|
80
|
+
data['solver']['radiation'] = "false"
|
|
81
|
+
for key in ['sg-rosseland?', 'sg-p1?', 'sg-dtrm?', 'sg-s2s?', 'sg-disco?']:
|
|
82
|
+
if kvs[key] != "#f":
|
|
83
|
+
data['solver']['radiation'] = key[3:-1]
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
gravity = re.search(
|
|
87
|
+
r'\(gravity\?\s+([^)\s]+)\)',
|
|
88
|
+
general_info
|
|
89
|
+
).group(1)
|
|
90
|
+
if gravity == "#t":
|
|
91
|
+
axes = ['x', 'y', 'z'] if data['solver']['dimension'] == "3d" else ['x', 'y']
|
|
92
|
+
for axis in axes:
|
|
93
|
+
sel = re.search(
|
|
94
|
+
fr'\(gravity/{axis}-sel\s+"([^"]+)"\)',
|
|
95
|
+
general_info
|
|
96
|
+
).group(1)
|
|
97
|
+
expr = re.search(
|
|
98
|
+
fr'\(gravity/{axis}-expr\s+"([^"]+)"\)',
|
|
99
|
+
general_info
|
|
100
|
+
).group(1)
|
|
101
|
+
data['solver'][f'gravity/{axis}'] = f'{sel}/{expr}'
|
|
102
|
+
else:
|
|
103
|
+
data['solver']['gravity'] = "false"
|
|
104
|
+
|
|
105
|
+
operating_conditions = [
|
|
106
|
+
'operating-pressure',
|
|
107
|
+
'pressure-reference/x', 'pressure-reference/y', 'pressure-reference/z',
|
|
108
|
+
'operating-temperature',
|
|
109
|
+
]
|
|
110
|
+
use_operating_density = re.search(
|
|
111
|
+
r'\(use-operating-density\?\s+([^)\s]+)\)',
|
|
112
|
+
general_info
|
|
113
|
+
).group(1)
|
|
114
|
+
if use_operating_density == '#t':
|
|
115
|
+
operating_conditions.append('operating-density')
|
|
116
|
+
for condition in operating_conditions:
|
|
117
|
+
sel = re.search(
|
|
118
|
+
fr'\({condition}-sel\s+"([^"]+)"\)',
|
|
119
|
+
general_info
|
|
120
|
+
).group(1)
|
|
121
|
+
expr = re.search(
|
|
122
|
+
fr'\({condition}-expr\s+"([^"]+)"\)',
|
|
123
|
+
general_info
|
|
124
|
+
).group(1)
|
|
125
|
+
data['solver'][condition] = f'{sel}/{expr}'
|
|
126
|
+
|
|
127
|
+
if kwargs['mat']:
|
|
128
|
+
import sexpdata
|
|
129
|
+
|
|
130
|
+
data['materials'] = {}
|
|
131
|
+
materials = re.search(
|
|
132
|
+
r'(\(materials.*)',
|
|
133
|
+
general_info,
|
|
134
|
+
re.M
|
|
135
|
+
).group(1)
|
|
136
|
+
materials: list = sexpdata.loads(materials)
|
|
137
|
+
for material in materials[1]:
|
|
138
|
+
name = str(material[0])
|
|
139
|
+
data['materials'][name] = {}
|
|
140
|
+
data['materials'][name]['type'] = str(material[1])
|
|
141
|
+
for i in range(2, len(material)):
|
|
142
|
+
property_ = material[i]
|
|
143
|
+
if property_[1] == sexpdata.Symbol('.'):
|
|
144
|
+
data['materials'][name][str(property_[0])] = str(property_[2])
|
|
145
|
+
elif isinstance(p1 := property_[1], list):
|
|
146
|
+
if p1[1] == sexpdata.Symbol('.'):
|
|
147
|
+
data['materials'][name][str(property_[0])] = f'{p1[0]}/{p1[2]}'
|
|
148
|
+
else:
|
|
149
|
+
value = ' '.join(str(p) for p in p1[1:])
|
|
150
|
+
data['materials'][name][str(property_[0])] = f'{p1[0]}/{value}'
|
|
151
|
+
|
|
152
|
+
if kwargs['bd']:
|
|
153
|
+
import sexpdata
|
|
154
|
+
from .boundary import BoundaryFactory
|
|
155
|
+
|
|
156
|
+
data['boundary'] = {}
|
|
157
|
+
boundaries: list[list] = sexpdata.parse(boundary_info, true=None)
|
|
158
|
+
for boundary_info in boundaries:
|
|
159
|
+
id_, type_, name, _ = [str(_) for _ in boundary_info[1]]
|
|
160
|
+
new_boundary = BoundaryFactory.create(name, id_, type_)
|
|
161
|
+
b_list = data['boundary'].get(type_, [])
|
|
162
|
+
|
|
163
|
+
for property_ in boundary_info[2]:
|
|
164
|
+
property_name = str(property_[0]).replace('-', '_').replace('?', '').replace('/', '_')
|
|
165
|
+
if hasattr(new_boundary, property_name):
|
|
166
|
+
if property_[1] == sexpdata.Symbol('.'):
|
|
167
|
+
setattr(new_boundary, property_name, str(property_[2]))
|
|
168
|
+
elif isinstance(property_[1], list):
|
|
169
|
+
setattr(new_boundary, property_name, f'{property_[1][0]}/{property_[1][2]}')
|
|
170
|
+
|
|
171
|
+
b_list.append(new_boundary.to_dict() if hasattr(new_boundary, 'to_dict') else new_boundary.__dict__)
|
|
172
|
+
data['boundary'][type_] = b_list
|
|
173
|
+
|
|
174
|
+
if kwargs['ne']:
|
|
175
|
+
import sexpdata
|
|
176
|
+
|
|
177
|
+
data['named-expressions'] = {}
|
|
178
|
+
nes = re.search(
|
|
179
|
+
r'(\(named-expressions.*)',
|
|
180
|
+
general_info,
|
|
181
|
+
re.M
|
|
182
|
+
).group(1)
|
|
183
|
+
nes: list = sexpdata.loads(nes, true=None)[1]
|
|
184
|
+
for ne in nes:
|
|
185
|
+
ne_dict = {
|
|
186
|
+
str(property_[0]): str(property_[2])
|
|
187
|
+
for property_ in ne
|
|
188
|
+
}
|
|
189
|
+
data['named-expressions'][ne_dict['name']] = ne_dict
|
|
190
|
+
|
|
191
|
+
if kwargs['disc']:
|
|
192
|
+
from .utils import FLUENT_ENUM
|
|
193
|
+
|
|
194
|
+
data['disc-scheme'] = {}
|
|
195
|
+
disc_scheme = {
|
|
196
|
+
ds[0]: FLUENT_ENUM[ds[1]]
|
|
197
|
+
for ds in re.findall(
|
|
198
|
+
r'\((.*)/scheme\s+(\d+)\)',
|
|
199
|
+
general_info
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
for eq in ['flow', 'pressure', 'mom', 'temperature', 'k', 'omega', 'epsilon']:
|
|
203
|
+
data['disc-scheme'][eq] = disc_scheme.get(eq)
|
|
204
|
+
|
|
205
|
+
data['relax-factor'] = {}
|
|
206
|
+
if data['disc-scheme']['flow'] == 'Coupled':
|
|
207
|
+
for eq in ['pressure', 'mom']:
|
|
208
|
+
data['relax-factor'][eq] = re.search(
|
|
209
|
+
fr'\(pressure-coupled/{eq}/pseudo-explicit-relax\s+([\d.]+)\)',
|
|
210
|
+
general_info
|
|
211
|
+
).group(1)
|
|
212
|
+
for eq in ['temperature', 'k', 'omega', 'epsilon', 'turb-viscosity', 'density', 'body-force']:
|
|
213
|
+
data['relax-factor'][eq] = re.search(
|
|
214
|
+
fr'\({eq}/pseudo-relax\s+([\d.]+)\)',
|
|
215
|
+
general_info
|
|
216
|
+
).group(1)
|
|
217
|
+
else:
|
|
218
|
+
relax_factor = {
|
|
219
|
+
ur[0]: ur[1]
|
|
220
|
+
for ur in re.findall(
|
|
221
|
+
fr'\((.*)/relax\s+([\d.]+)\)',
|
|
222
|
+
general_info
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
for eq in ['pressure', 'mom', 'temperature', 'k', 'omega', 'epsilon', 'turb-viscosity', 'density', 'body-force']:
|
|
226
|
+
data['relax-factor'][eq] = relax_factor.get(eq, '')
|
|
227
|
+
|
|
228
|
+
if kwargs['rd']:
|
|
229
|
+
import sexpdata
|
|
230
|
+
|
|
231
|
+
data['report-definitions'] = {}
|
|
232
|
+
rds = re.search(
|
|
233
|
+
r'(\(monitor/report-definitions.*)',
|
|
234
|
+
general_info,
|
|
235
|
+
re.M
|
|
236
|
+
).group(1)
|
|
237
|
+
rds: list = sexpdata.loads(rds, true=None)[1]
|
|
238
|
+
for rd in rds:
|
|
239
|
+
name = str(rd[0][2])
|
|
240
|
+
type_ = str(rd[1][1])
|
|
241
|
+
data['report-definitions'][name] = {'type': type_}
|
|
242
|
+
if 'volume' in type_:
|
|
243
|
+
data['report-definitions'][name]['field'] = str(rd[1][2][2])
|
|
244
|
+
data['report-definitions'][name]['zones'] = [str(zone) for zone in rd[1][6][1:]]
|
|
245
|
+
data['report-definitions'][name]['per-zone?'] = str(rd[1][-5][2])
|
|
246
|
+
elif 'surface' in type_:
|
|
247
|
+
data['report-definitions'][name]['field'] = str(rd[1][2][2])
|
|
248
|
+
data['report-definitions'][name]['surfaces'] = [str(surface) for surface in rd[1][5][1:]]
|
|
249
|
+
data['report-definitions'][name]['per-surface?'] = str(rd[1][-5][2])
|
|
250
|
+
elif 'flux' in type_:
|
|
251
|
+
data['report-definitions'][name]['zones'] = [str(zone) for zone in rd[1][3][1:]]
|
|
252
|
+
data['report-definitions'][name]['per-zone?'] = str(rd[1][-5][2])
|
|
253
|
+
|
|
254
|
+
if kwargs['plotsets']:
|
|
255
|
+
import sexpdata
|
|
256
|
+
|
|
257
|
+
data['plotsets'] = {}
|
|
258
|
+
plotsets = re.search(
|
|
259
|
+
r'(\(monitor/plotsets.*)',
|
|
260
|
+
general_info,
|
|
261
|
+
re.M
|
|
262
|
+
).group(1)
|
|
263
|
+
plotsets: list = sexpdata.loads(plotsets, true=None)[1]
|
|
264
|
+
for plotset in plotsets:
|
|
265
|
+
plotset_dict = {
|
|
266
|
+
str(property_[0]): str(property_[2])
|
|
267
|
+
for property_ in plotset
|
|
268
|
+
if str(property_[0]) not in ['old-props', 'report-defs']
|
|
269
|
+
}
|
|
270
|
+
plotset_dict['report-defs'] = plotset[6][1:]
|
|
271
|
+
data['plotsets'][plotset_dict['name']] = plotset_dict
|
|
272
|
+
|
|
273
|
+
if kwargs['monitorsets']:
|
|
274
|
+
import sexpdata
|
|
275
|
+
|
|
276
|
+
data['monitorsets'] = {}
|
|
277
|
+
monitorsets = re.search(
|
|
278
|
+
r'(\(monitor/monitorsets.*)',
|
|
279
|
+
general_info,
|
|
280
|
+
re.M
|
|
281
|
+
).group(1)
|
|
282
|
+
monitorsets: list = sexpdata.loads(monitorsets, true=None)[1]
|
|
283
|
+
for monitorset in monitorsets:
|
|
284
|
+
monitorset_dict = {
|
|
285
|
+
str(property_[0]): str(property_[2])
|
|
286
|
+
for property_ in monitorset
|
|
287
|
+
if str(property_[0]) not in ['old-props', 'report-defs']
|
|
288
|
+
}
|
|
289
|
+
monitorset_dict['report-defs'] = monitorset[-4][1:]
|
|
290
|
+
data['monitorsets'][monitorset_dict['name']] = monitorset_dict
|
|
291
|
+
|
|
292
|
+
if kwargs['iter']:
|
|
293
|
+
data['iter'] = {}
|
|
294
|
+
|
|
295
|
+
if not (solver_time := data.get('solver', {}).get('time', None)):
|
|
296
|
+
case_config = re.search(
|
|
297
|
+
r'^\(case-config.*',
|
|
298
|
+
general_info,
|
|
299
|
+
re.M
|
|
300
|
+
).group()
|
|
301
|
+
is_unsteady = re.search(
|
|
302
|
+
r"\(rp-unsteady\?\s+\.\s+([^()\s]+)\)",
|
|
303
|
+
case_config
|
|
304
|
+
).group(1)
|
|
305
|
+
solver_time = "transient" if is_unsteady == "#t" else "steady"
|
|
306
|
+
|
|
307
|
+
if solver_time == 'steady':
|
|
308
|
+
data['iter']['iterations'] = re.search(
|
|
309
|
+
r'\(number-of-iterations\s+(\d+)\)',
|
|
310
|
+
general_info
|
|
311
|
+
).group(1)
|
|
312
|
+
else:
|
|
313
|
+
sel = re.search(
|
|
314
|
+
r'\(physical-time-step-sel\s+"([^"]+)"\)',
|
|
315
|
+
general_info
|
|
316
|
+
).group(1)
|
|
317
|
+
expr = re.search(
|
|
318
|
+
r'\(physical-time-step-expr\s+"([^"]+)"\)',
|
|
319
|
+
general_info
|
|
320
|
+
).group(1)
|
|
321
|
+
data['iter']['physical-time-step'] = f'{sel}/{expr}'
|
|
322
|
+
for key in ['time-steps', 'max-iters-per-step', 'time-step', 'flow-time']:
|
|
323
|
+
data['iter'][key] = re.search(
|
|
324
|
+
fr'\({key}\s+(\d+)\)',
|
|
325
|
+
general_info
|
|
326
|
+
).group(1)
|
|
327
|
+
|
|
328
|
+
return data
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def extract_h5(file_path: str) -> None:
|
|
332
|
+
"""Extract cas.h5 general and boundary string to files
|
|
333
|
+
|
|
334
|
+
Parameters
|
|
335
|
+
---------
|
|
336
|
+
file_path : str
|
|
337
|
+
Path to the cas.h5 file
|
|
338
|
+
"""
|
|
339
|
+
import h5py
|
|
340
|
+
with h5py.File(file_path) as f:
|
|
341
|
+
settings: h5py.Group = f['/settings']
|
|
342
|
+
general_info = settings['Rampant Variables'][0].decode()
|
|
343
|
+
boundary_info = settings['Thread Variables'][0].decode()
|
|
344
|
+
with open('general.scm', 'w', encoding='utf-8') as f:
|
|
345
|
+
f.write(general_info)
|
|
346
|
+
with open('boundary.scm', 'w', encoding='utf-8') as f:
|
|
347
|
+
f.write(boundary_info)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def show_mesh(file_path: str) -> None:
|
|
351
|
+
"""Show mesh with PyVista
|
|
352
|
+
|
|
353
|
+
Parameters
|
|
354
|
+
---------
|
|
355
|
+
file_path : str
|
|
356
|
+
Path to the .h5 file
|
|
357
|
+
"""
|
|
358
|
+
import pyvista as pv
|
|
359
|
+
if file_path.endswith('cas.h5'):
|
|
360
|
+
pv.plot(
|
|
361
|
+
pv.read(file_path, progress_bar=True),
|
|
362
|
+
show_edges=True,
|
|
363
|
+
show_axes=True,
|
|
364
|
+
smooth_shading=True,
|
|
365
|
+
split_sharp_edges=True,
|
|
366
|
+
)
|
|
367
|
+
elif file_path.endswith('msh.h5'):
|
|
368
|
+
import numpy as np
|
|
369
|
+
from h5py import File, Group, Dataset
|
|
370
|
+
|
|
371
|
+
with File(file_path) as f:
|
|
372
|
+
root_group: Group = f['/meshes/1']
|
|
373
|
+
dimension: np.int32 = root_group.attrs['dimension'][0]
|
|
374
|
+
nodeCount: np.uint64 = root_group.attrs['nodeCount'][0]
|
|
375
|
+
faceCount: np.uint64 = root_group.attrs['faceCount'][0]
|
|
376
|
+
pv_points = np.zeros((nodeCount, 3), dtype=np.float64)
|
|
377
|
+
nnodes = np.zeros(faceCount, dtype=np.int16)
|
|
378
|
+
zoneTopology: Group = root_group['nodes/zoneTopology']
|
|
379
|
+
nZones: np.uint64 = zoneTopology.attrs['nZones'][0]
|
|
380
|
+
minId: Dataset = zoneTopology['minId']
|
|
381
|
+
maxId: Dataset = zoneTopology['maxId']
|
|
382
|
+
|
|
383
|
+
coords_group: Group = root_group['nodes/coords']
|
|
384
|
+
for i in range(nZones):
|
|
385
|
+
pv_points[minId[i] - 1: maxId[i], :dimension] = coords_group[f'{i+1}'][:]
|
|
386
|
+
|
|
387
|
+
zoneTopology: Group = root_group['faces/zoneTopology']
|
|
388
|
+
minId: Dataset = zoneTopology['minId']
|
|
389
|
+
maxId: Dataset = zoneTopology['maxId']
|
|
390
|
+
|
|
391
|
+
faces_nodes_group: Group = root_group['faces/nodes']
|
|
392
|
+
nSections: np.uint64 = faces_nodes_group.attrs['nSections'][0]
|
|
393
|
+
for i in range(nSections):
|
|
394
|
+
section_group: Group = faces_nodes_group[f"{i+1}"]
|
|
395
|
+
nnodes[minId[i] - 1: maxId[i]] = section_group['nnodes'][:]
|
|
396
|
+
nodes_count = np.sum(nnodes)
|
|
397
|
+
nodes = np.zeros(nodes_count, dtype=np.uint32)
|
|
398
|
+
nodes_start_index = 0
|
|
399
|
+
for i in range(nSections):
|
|
400
|
+
section_group: Group = faces_nodes_group[f"{i+1}"]
|
|
401
|
+
nodes_num = section_group['nodes'].size
|
|
402
|
+
nodes[nodes_start_index: nodes_start_index + nodes_num] = section_group['nodes'][:] - 1
|
|
403
|
+
nodes_start_index += nodes_num
|
|
404
|
+
offsets = np.cumsum(nnodes) - nnodes
|
|
405
|
+
pv_faces = np.insert(nodes, offsets, nnodes)
|
|
406
|
+
|
|
407
|
+
mesh = pv.PolyData(pv_points, pv_faces)
|
|
408
|
+
plotter = pv.Plotter()
|
|
409
|
+
plotter.add_mesh(mesh, show_edges=True, color='cyan', line_width=2, smooth_shading=True, split_sharp_edges=True)
|
|
410
|
+
plotter.add_axes()
|
|
411
|
+
plotter.show()
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def main() -> None:
|
|
415
|
+
import argparse
|
|
416
|
+
|
|
417
|
+
desc = "A Python CLI tool to inspect Ansys Fluent .cas.h5/.msh.h5 files without opening Fluent"
|
|
418
|
+
parser = argparse.ArgumentParser(description=desc)
|
|
419
|
+
|
|
420
|
+
parser.add_argument(
|
|
421
|
+
"file_path",
|
|
422
|
+
type=str,
|
|
423
|
+
help="path to the .h5 file"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
parser.add_argument(
|
|
427
|
+
"--extract",
|
|
428
|
+
action="store_true",
|
|
429
|
+
help="extract cas.h5 general and boundary string to files"
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
parser.add_argument(
|
|
433
|
+
"-v", "--version",
|
|
434
|
+
action="store_true",
|
|
435
|
+
help="show the version of the .h5 file"
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
parser.add_argument(
|
|
439
|
+
"--solver",
|
|
440
|
+
action="store_true",
|
|
441
|
+
help="show solver settings"
|
|
442
|
+
)
|
|
443
|
+
parser.add_argument(
|
|
444
|
+
"--mat", "--materials",
|
|
445
|
+
action="store_true",
|
|
446
|
+
help="show materials settings"
|
|
447
|
+
)
|
|
448
|
+
parser.add_argument(
|
|
449
|
+
"--bd", "--boundary",
|
|
450
|
+
action="store_true",
|
|
451
|
+
help="show boundary settings"
|
|
452
|
+
)
|
|
453
|
+
parser.add_argument(
|
|
454
|
+
"--ne", "--named-expressions",
|
|
455
|
+
action="store_true",
|
|
456
|
+
help="show named-expressions settings"
|
|
457
|
+
)
|
|
458
|
+
parser.add_argument(
|
|
459
|
+
"--disc",
|
|
460
|
+
action="store_true",
|
|
461
|
+
help="show disc-scheme and relax-factor settings"
|
|
462
|
+
)
|
|
463
|
+
parser.add_argument(
|
|
464
|
+
"--rd", "--report-definitions",
|
|
465
|
+
action="store_true",
|
|
466
|
+
help="show report-definitions settings"
|
|
467
|
+
)
|
|
468
|
+
parser.add_argument(
|
|
469
|
+
"--plotsets",
|
|
470
|
+
action="store_true",
|
|
471
|
+
help="show report-definitions plotsets settings"
|
|
472
|
+
)
|
|
473
|
+
parser.add_argument(
|
|
474
|
+
"--monitorsets",
|
|
475
|
+
action="store_true",
|
|
476
|
+
help="show report-definitions monitorsets settings"
|
|
477
|
+
)
|
|
478
|
+
parser.add_argument(
|
|
479
|
+
"--iter",
|
|
480
|
+
action="store_true",
|
|
481
|
+
help="show iteration settings"
|
|
482
|
+
)
|
|
483
|
+
parser.add_argument(
|
|
484
|
+
"--save",
|
|
485
|
+
action="store_true",
|
|
486
|
+
help="save output to file"
|
|
487
|
+
)
|
|
488
|
+
parser.add_argument(
|
|
489
|
+
"--showmesh",
|
|
490
|
+
action="store_true",
|
|
491
|
+
help="show mesh using pyvista"
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
args = parser.parse_args()
|
|
495
|
+
|
|
496
|
+
if args.version:
|
|
497
|
+
print_version(args.file_path)
|
|
498
|
+
elif args.extract:
|
|
499
|
+
extract_h5(args.file_path)
|
|
500
|
+
elif args.file_path.endswith(".msh.h5"):
|
|
501
|
+
show_mesh(args.file_path)
|
|
502
|
+
elif args.file_path.endswith(".cas.h5"):
|
|
503
|
+
if args.showmesh:
|
|
504
|
+
show_mesh(args.file_path)
|
|
505
|
+
else:
|
|
506
|
+
from .utils import print_colored_dict
|
|
507
|
+
kwargs = {
|
|
508
|
+
'solver': args.solver,
|
|
509
|
+
'mat': args.mat,
|
|
510
|
+
'bd': args.bd,
|
|
511
|
+
'ne': args.ne,
|
|
512
|
+
'disc': args.disc,
|
|
513
|
+
'rd': args.rd,
|
|
514
|
+
'plotsets': args.plotsets,
|
|
515
|
+
'monitorsets': args.monitorsets,
|
|
516
|
+
'iter': args.iter
|
|
517
|
+
}
|
|
518
|
+
output = read_case(args.file_path, **kwargs)
|
|
519
|
+
print_colored_dict(output)
|
|
520
|
+
|
|
521
|
+
if args.save:
|
|
522
|
+
import json
|
|
523
|
+
with open(f"{args.file_path}.json", "w", encoding="utf-8") as f:
|
|
524
|
+
json.dump(output, f, ensure_ascii=False, indent=4)
|
cffview/utils.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# From context.scm
|
|
2
|
+
FLUENT_ENUM = {
|
|
3
|
+
"0": "First Order Upwind",
|
|
4
|
+
"1": "Second Order Upwind",
|
|
5
|
+
"2": "Power Law",
|
|
6
|
+
"3": "Central Difference",
|
|
7
|
+
"4": "Quick",
|
|
8
|
+
"5": "Modified HRIC",
|
|
9
|
+
"6": "Third-Order MUSCL",
|
|
10
|
+
"7": "Bounded Central Differencing",
|
|
11
|
+
"8": "CICSAM",
|
|
12
|
+
"9": "Low Diffusion Second Order",
|
|
13
|
+
"10": "Standard",
|
|
14
|
+
"11": "Linear",
|
|
15
|
+
"12": "Second Order",
|
|
16
|
+
"13": "Body Force Weighted",
|
|
17
|
+
"14": "PRESTO!",
|
|
18
|
+
"15": "Continuity Based",
|
|
19
|
+
"16": "Geo-Reconstruct",
|
|
20
|
+
"17": "Donor-Acceptor",
|
|
21
|
+
"18": "Modified Body Force Weighted",
|
|
22
|
+
"20": "SIMPLE",
|
|
23
|
+
"21": "SIMPLEC",
|
|
24
|
+
"22": "PISO",
|
|
25
|
+
"23": "Phase Coupled SIMPLE",
|
|
26
|
+
"24": "Coupled",
|
|
27
|
+
"25": "Fractional Step",
|
|
28
|
+
"28": "Compressive",
|
|
29
|
+
"29": "BGM",
|
|
30
|
+
"30": "Phase Coupled PISO",
|
|
31
|
+
"31": "Low Diffusion Central",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
FLUENT_BOUNDARY_TYPES = {
|
|
35
|
+
2: "interior",
|
|
36
|
+
3: "wall",
|
|
37
|
+
4: "inlet-vent",
|
|
38
|
+
4: "pressure-inlet",
|
|
39
|
+
4: "intake-fan",
|
|
40
|
+
5: "pressure-outlet",
|
|
41
|
+
5: "outlet-vent",
|
|
42
|
+
5: "exhaust-fan",
|
|
43
|
+
7: "symmetry",
|
|
44
|
+
9: "pressure-far-field",
|
|
45
|
+
10: "velocity-inlet",
|
|
46
|
+
14: "radiator",
|
|
47
|
+
14: "fan",
|
|
48
|
+
14: "porous-jump",
|
|
49
|
+
20: "mass-flow-inlet",
|
|
50
|
+
20: "mass-flow-outlet",
|
|
51
|
+
24: "interface",
|
|
52
|
+
25: "overset",
|
|
53
|
+
36: "outflow",
|
|
54
|
+
37: "axis",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
CELL_TYPES = {
|
|
58
|
+
1: "Triangle",
|
|
59
|
+
2: "Tetrahedron",
|
|
60
|
+
3: "Quadrilateral",
|
|
61
|
+
4: "Hexahedral",
|
|
62
|
+
5: "Pyramid",
|
|
63
|
+
6: "Wedge",
|
|
64
|
+
7: "Polyhedron",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def print_colored_dict(data) -> None:
|
|
69
|
+
"""Print nested data in 4-space JSON format.
|
|
70
|
+
|
|
71
|
+
Keys (including colon) : cycle through 6 hued colours per nesting level.
|
|
72
|
+
String values : bright white — no hue, never clashes with any key colour.
|
|
73
|
+
Numbers / bool / null : white-grey — no hue, never clashes with any key colour.
|
|
74
|
+
"""
|
|
75
|
+
# 6 hued colours used exclusively for keys
|
|
76
|
+
LEVEL_COLORS = [
|
|
77
|
+
"\033[36m", # cyan
|
|
78
|
+
"\033[32m", # green
|
|
79
|
+
"\033[33m", # yellow
|
|
80
|
+
"\033[35m", # magenta
|
|
81
|
+
"\033[34m", # blue
|
|
82
|
+
"\033[31m", # red
|
|
83
|
+
]
|
|
84
|
+
STR_COLOR = "\033[97m" # bright white — string values
|
|
85
|
+
PRIM_COLOR = "\033[37m" # white-grey — numbers / booleans / null
|
|
86
|
+
RESET = "\033[0m"
|
|
87
|
+
|
|
88
|
+
import re
|
|
89
|
+
import json
|
|
90
|
+
|
|
91
|
+
# Three capture groups: key (with quotes) | colon + whitespace | value (with optional trailing comma)
|
|
92
|
+
KV_RE = re.compile(r'^("(?:[^"\\]|\\.)*")(:\s*)(.+)$')
|
|
93
|
+
|
|
94
|
+
text = json.dumps(data, indent=4, ensure_ascii=False, default=str)
|
|
95
|
+
|
|
96
|
+
for line in text.splitlines():
|
|
97
|
+
content = line.lstrip(" ")
|
|
98
|
+
level = (len(line) - len(content)) // 4
|
|
99
|
+
indent = " " * (len(line) - len(content))
|
|
100
|
+
lc = LEVEL_COLORS[level % len(LEVEL_COLORS)]
|
|
101
|
+
|
|
102
|
+
m = KV_RE.match(content)
|
|
103
|
+
if m:
|
|
104
|
+
key, colon, rest = m.group(1), m.group(2), m.group(3)
|
|
105
|
+
# Strip trailing comma only for type detection; print the original `rest`
|
|
106
|
+
bare = rest.rstrip(",").strip()
|
|
107
|
+
|
|
108
|
+
if bare in ("{", "["):
|
|
109
|
+
# Value is an opening bracket — keep structural chars in the level colour
|
|
110
|
+
print(f"{indent}{lc}{key}{colon}{rest}{RESET}")
|
|
111
|
+
elif bare.startswith('"'):
|
|
112
|
+
print(f"{indent}{lc}{key}{colon}{RESET}{STR_COLOR}{rest}{RESET}")
|
|
113
|
+
else:
|
|
114
|
+
# Number / boolean / null
|
|
115
|
+
print(f"{indent}{lc}{key}{colon}{RESET}{PRIM_COLOR}{rest}{RESET}")
|
|
116
|
+
else:
|
|
117
|
+
# Structural line ({ } [ ]) or bare array element
|
|
118
|
+
bare = content.rstrip(",").strip()
|
|
119
|
+
if bare.startswith('"'):
|
|
120
|
+
print(f"{indent}{STR_COLOR}{content}{RESET}")
|
|
121
|
+
elif bare in ("{", "}", "[", "]"):
|
|
122
|
+
print(f"{indent}{lc}{content}{RESET}")
|
|
123
|
+
else:
|
|
124
|
+
# Number / boolean / null inside an array
|
|
125
|
+
print(f"{indent}{PRIM_COLOR}{content}{RESET}")
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cffview
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A Python CLI tool to inspect Ansys Fluent .cas.h5/.msh.h5 files without opening Fluent
|
|
5
|
+
Author-email: preamer <1524477126@qq.com>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Repository, https://github.com/preamer/cffview
|
|
8
|
+
Keywords: fluent,cfd,ansys,hdf5,mesh
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: h5py>=3.16.0
|
|
20
|
+
Requires-Dist: pyvista>=0.48.4
|
|
21
|
+
Requires-Dist: sexpdata>=1.0.2
|
|
22
|
+
Requires-Dist: tqdm>=4.68.3
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
[中文](README.zh.md)
|
|
26
|
+
|
|
27
|
+
# cffview
|
|
28
|
+
|
|
29
|
+
A command-line tool for inspecting Ansys Fluent `.cas.h5` / `.msh.h5` files **without opening Fluent**.
|
|
30
|
+
|
|
31
|
+
- Read solver settings, materials, boundary conditions, discretisation schemes, and more directly from the HDF5 file.
|
|
32
|
+
- Visualise the mesh with [PyVista](https://pyvista.org).
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install cffview
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Build from source
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone https://github.com/preamer/cffview.git
|
|
46
|
+
cd cffview
|
|
47
|
+
pip install .
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
cffview <file> [options]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Options
|
|
59
|
+
|
|
60
|
+
| Option | Description |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `-v`, `--version` | Print the Fluent version stored in the file |
|
|
63
|
+
| `--extract` | Dump raw Scheme settings to `general.scm` and `boundary.scm` |
|
|
64
|
+
| `--showmesh` | Visualise the mesh interactively with PyVista |
|
|
65
|
+
| `--solver` | Solver type, time, dimension, precision, turbulence model, energy, radiation, gravity |
|
|
66
|
+
| `--mat` | Material properties |
|
|
67
|
+
| `--bd` | Boundary condition settings |
|
|
68
|
+
| `--ne` | Named expressions |
|
|
69
|
+
| `--disc` | Discretisation schemes and relaxation factors |
|
|
70
|
+
| `--rd` | Report definitions |
|
|
71
|
+
| `--plotsets` | Plot sets |
|
|
72
|
+
| `--monitorsets` | Monitor sets |
|
|
73
|
+
| `--iter` | Iteration / time-step settings |
|
|
74
|
+
| `--save` | Save the output to `<file>.json` |
|
|
75
|
+
|
|
76
|
+
Multiple flags can be combined freely. Case settings flags (`--solver`, `--mat`, etc.) apply to `.cas.h5` files only.
|
|
77
|
+
|
|
78
|
+
### Examples
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Show solver configuration and boundary conditions
|
|
82
|
+
cffview case.cas.h5 --solver --bd
|
|
83
|
+
|
|
84
|
+
# Show all settings and save to JSON
|
|
85
|
+
cffview case.cas.h5 --solver --mat --bd --ne --disc --rd --iter --save
|
|
86
|
+
|
|
87
|
+
# Visualise the mesh (.cas.h5 and .msh.h5 both supported)
|
|
88
|
+
cffview case.cas.h5 --showmesh
|
|
89
|
+
cffview mesh.msh.h5 --showmesh
|
|
90
|
+
|
|
91
|
+
# Check the Fluent version embedded in the file
|
|
92
|
+
cffview case.cas.h5 --version
|
|
93
|
+
|
|
94
|
+
# Extract raw Scheme strings for manual inspection
|
|
95
|
+
cffview case.cas.h5 --extract
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## File Format Support
|
|
101
|
+
|
|
102
|
+
| Feature | `.cas.h5` | `.msh.h5` |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| Case settings (`--solver`, `--mat`, `--bd`, …) | ✅ | — |
|
|
105
|
+
| Mesh visualisation (3D) | ✅ | ✅ |
|
|
106
|
+
| Mesh visualisation (2D) | ✅ | ✅ |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Supported Settings
|
|
111
|
+
|
|
112
|
+
| Flag | Contents |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `--solver` | Algorithm (PBNS/DBNS), steady/transient, 2D/3D, single/double precision, turbulence model, energy, radiation, gravity |
|
|
115
|
+
| `--mat` | Fluid/solid materials with properties and evaluation methods |
|
|
116
|
+
| `--bd` | Velocity-inlet, pressure-outlet, mass-flow-inlet/outlet, wall (thermal & motion BC), porous-jump, interior, symmetry, … |
|
|
117
|
+
| `--ne` | Named expressions defined in the case |
|
|
118
|
+
| `--disc` | Per-equation scheme (Second Order Upwind, QUICK, …) and under-relaxation / pseudo-transient factors |
|
|
119
|
+
| `--rd` | Report definitions (volume, surface, flux) with field, zones, and per-zone flag |
|
|
120
|
+
| `--plotsets` | Plot set configurations |
|
|
121
|
+
| `--monitorsets` | Monitor set configurations |
|
|
122
|
+
| `--iter` | Iteration count (steady) or time-step size, number of steps, max inner iterations (transient) |
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
[BSD-3-Clause](LICENSE)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
cffview/boundary.py,sha256=Qf_VCbKlr3Wb2Jad0bgHfzFo7Ha3kE8RftIZgOfwvfY,7652
|
|
2
|
+
cffview/main.py,sha256=-ejDB4En0RSkDHQAEIK-ChlozGhTkzDmTrtufeOGidw,18348
|
|
3
|
+
cffview/utils.py,sha256=bJTjNvKMVVWMz-75AjWzEwf7l-EbPVru1bLay6gpIWY,3863
|
|
4
|
+
cffview-0.2.0.dist-info/licenses/LICENSE,sha256=fX5KINFPzeX8XkVSp0LkkDa8x7lNrL6HRM3KNAyEGZU,1493
|
|
5
|
+
cffview-0.2.0.dist-info/METADATA,sha256=dWSzgNpLV893moxqRyEfLxSg532MjNcgIUG2jt0Pgcs,3884
|
|
6
|
+
cffview-0.2.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
7
|
+
cffview-0.2.0.dist-info/entry_points.txt,sha256=QPeK1LuSRH44i315vFZnvNz_DyXiipMbE0Q6wbXjiSw,46
|
|
8
|
+
cffview-0.2.0.dist-info/top_level.txt,sha256=VnE4_JsocgSYopbiqxE9oI4435vuwaE_ATafiEeztc8,8
|
|
9
|
+
cffview-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, preamer
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cffview
|