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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cffview = cffview.main:main
@@ -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