Skip to content

Commit 58c9696

Browse files
committed
feat: add exodus reader
1 parent bfef1f0 commit 58c9696

File tree

3 files changed

+306
-0
lines changed

3 files changed

+306
-0
lines changed
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
"""
2+
I/O for Exodus II. This file is based on the meshio project see:
3+
4+
https://github.com/nschloe/meshio/blob/main/src/meshio/exodus/_exodus.py
5+
"""
6+
7+
import re
8+
9+
import numpy as np
10+
11+
from meshio.__about__ import __version__
12+
from meshio._common import warn
13+
from meshio._exceptions import ReadError
14+
from meshio._mesh import Mesh
15+
16+
exodus_to_meshio_type = {
17+
"SPHERE": "vertex",
18+
# curves
19+
"BEAM": "line",
20+
"BEAM2": "line",
21+
"BEAM3": "line3",
22+
"BAR2": "line",
23+
# surfaces
24+
"SHELL": "quad",
25+
"SHELL4": "quad",
26+
"SHELL8": "quad8",
27+
"SHELL9": "quad9",
28+
"QUAD": "quad",
29+
"QUAD4": "quad",
30+
"QUAD5": "quad5",
31+
"QUAD8": "quad8",
32+
"QUAD9": "quad9",
33+
#
34+
"TRI": "triangle",
35+
"TRIANGLE": "triangle",
36+
"TRI3": "triangle",
37+
"TRI6": "triangle6",
38+
"TRI7": "triangle7",
39+
# 'TRISHELL': 'triangle',
40+
# 'TRISHELL3': 'triangle',
41+
# 'TRISHELL6': 'triangle6',
42+
# 'TRISHELL7': 'triangle',
43+
#
44+
# volumes
45+
"HEX": "hexahedron",
46+
"HEXAHEDRON": "hexahedron",
47+
"HEX8": "hexahedron",
48+
"HEX9": "hexahedron9",
49+
"HEX20": "hexahedron20",
50+
"HEX27": "hexahedron27",
51+
#
52+
"TETRA": "tetra",
53+
"TETRA4": "tetra4",
54+
"TET4": "tetra4",
55+
"TETRA8": "tetra8",
56+
"TETRA10": "tetra10",
57+
"TETRA14": "tetra14",
58+
#
59+
"PYRAMID": "pyramid",
60+
"WEDGE": "wedge",
61+
}
62+
meshio_to_exodus_type = {v: k for k, v in exodus_to_meshio_type.items()}
63+
64+
65+
def _categorize(names):
66+
# Check if there are any <name>R, <name>Z tuples or <name>X, <name>Y, <name>Z
67+
# triplets in the point data. If yes, they belong together.
68+
single = []
69+
double = []
70+
triple = []
71+
is_accounted_for = [False] * len(names)
72+
k = 0
73+
while True:
74+
if k == len(names):
75+
break
76+
if is_accounted_for[k]:
77+
k += 1
78+
continue
79+
name = names[k]
80+
if name[-1] == "X":
81+
ix = k
82+
try:
83+
iy = names.index(name[:-1] + "Y")
84+
except ValueError:
85+
iy = None
86+
try:
87+
iz = names.index(name[:-1] + "Z")
88+
except ValueError:
89+
iz = None
90+
if iy and iz:
91+
triple.append((name[:-1], ix, iy, iz))
92+
is_accounted_for[ix] = True
93+
is_accounted_for[iy] = True
94+
is_accounted_for[iz] = True
95+
else:
96+
single.append((name, ix))
97+
is_accounted_for[ix] = True
98+
elif name[-2:] == "_R":
99+
ir = k
100+
try:
101+
iz = names.index(name[:-2] + "_Z")
102+
except ValueError:
103+
iz = None
104+
if iz:
105+
double.append((name[:-2], ir, iz))
106+
is_accounted_for[ir] = True
107+
is_accounted_for[iz] = True
108+
else:
109+
single.append((name, ir))
110+
is_accounted_for[ir] = True
111+
else:
112+
single.append((name, k))
113+
is_accounted_for[k] = True
114+
115+
k += 1
116+
117+
if not all(is_accounted_for):
118+
raise ReadError()
119+
return single, double, triple
120+
121+
122+
def read_exodus(filename, use_set_names=False): # noqa: C901
123+
import netCDF4
124+
125+
with netCDF4.Dataset(filename) as nc:
126+
# assert nc.version == np.float32(5.1)
127+
# assert nc.api_version == np.float32(5.1)
128+
# assert nc.floating_point_word_size == 8
129+
130+
# assert b''.join(nc.variables['coor_names'][0]) == b'X'
131+
# assert b''.join(nc.variables['coor_names'][1]) == b'Y'
132+
# assert b''.join(nc.variables['coor_names'][2]) == b'Z'
133+
134+
points = np.zeros((len(nc.dimensions["num_nodes"]), 3))
135+
point_data_names = []
136+
cell_data_names = []
137+
pd = {}
138+
cd = {}
139+
cells = []
140+
ns_names = []
141+
eb_names = []
142+
ns = []
143+
point_sets = {}
144+
info = []
145+
146+
cell_sets = {}
147+
148+
element_running_index = 0
149+
150+
for key, value in nc.variables.items():
151+
if key == "info_records":
152+
value.set_auto_mask(False)
153+
for c in value[:]:
154+
info += [b"".join(c).decode("UTF-8")]
155+
elif key == "qa_records":
156+
value.set_auto_mask(False)
157+
for val in value:
158+
info += [b"".join(c).decode("UTF-8") for c in val[:]]
159+
elif key[:7] == "connect":
160+
meshio_type = exodus_to_meshio_type[value.elem_type.upper()]
161+
cell_sets[str(len(cell_sets) + 1)] = np.arange(
162+
element_running_index, element_running_index + len(value[:])
163+
)
164+
cells.append((meshio_type, value[:] - 1))
165+
elif key == "coord":
166+
points = nc.variables["coord"][:].T
167+
elif key == "coordx":
168+
points[:, 0] = value[:]
169+
elif key == "coordy":
170+
points[:, 1] = value[:]
171+
elif key == "coordz":
172+
points[:, 2] = value[:]
173+
elif key == "name_nod_var":
174+
value.set_auto_mask(False)
175+
point_data_names = [b"".join(c).decode("UTF-8") for c in value[:]]
176+
elif key[:12] == "vals_nod_var":
177+
idx = 0 if len(key) == 12 else int(key[12:]) - 1
178+
value.set_auto_mask(False)
179+
# For now only take the first value
180+
pd[idx] = value[0]
181+
if len(value) > 1:
182+
warn("Skipping some time data")
183+
elif key == "name_elem_var":
184+
value.set_auto_mask(False)
185+
cell_data_names = [b"".join(c).decode("UTF-8") for c in value[:]]
186+
elif key[:13] == "vals_elem_var":
187+
# eb: element block
188+
m = re.match("vals_elem_var(\\d+)?(?:eb(\\d+))?", key)
189+
idx = 0 if m.group(1) is None else int(m.group(1)) - 1
190+
block = 0 if m.group(2) is None else int(m.group(2)) - 1
191+
192+
value.set_auto_mask(False)
193+
# For now only take the first value
194+
if idx not in cd:
195+
cd[idx] = {}
196+
cd[idx][block] = value[0]
197+
198+
if len(value) > 1:
199+
warn("Skipping some time data")
200+
elif key == "ns_names":
201+
value.set_auto_mask(False)
202+
ns_names = [b"".join(c).decode("UTF-8") for c in value[:]]
203+
elif key == "eb_names":
204+
value.set_auto_mask(False)
205+
eb_names = [b"".join(c).decode("UTF-8") for c in value[:]]
206+
elif key.startswith("node_ns"): # Expected keys: node_ns1, node_ns2
207+
ns.append(value[:] - 1) # Exodus is 1-based
208+
209+
# merge element block data; can't handle blocks yet
210+
for k, value in cd.items():
211+
cd[k] = np.concatenate(list(value.values()))
212+
213+
# Check if there are any <name>R, <name>Z tuples or <name>X, <name>Y, <name>Z
214+
# triplets in the point data. If yes, they belong together.
215+
single, double, triple = _categorize(point_data_names)
216+
217+
point_data = {}
218+
for name, idx in single:
219+
point_data[name] = pd[idx]
220+
for name, idx0, idx1 in double:
221+
point_data[name] = np.column_stack([pd[idx0], pd[idx1]])
222+
for name, idx0, idx1, idx2 in triple:
223+
point_data[name] = np.column_stack([pd[idx0], pd[idx1], pd[idx2]])
224+
225+
cell_data = {}
226+
k = 0
227+
for _, cell in cells:
228+
n = len(cell)
229+
for name, data in zip(cell_data_names, cd.values()):
230+
if name not in cell_data:
231+
cell_data[name] = []
232+
cell_data[name].append(data[k : k + n])
233+
k += n
234+
235+
point_sets = {str(i + 1): dat.tolist() for i, dat in enumerate(ns)}
236+
237+
if use_set_names:
238+
point_sets = {name: dat for name, dat in zip(ns_names, ns)}
239+
cell_sets = {
240+
name: cell_set for name, cell_set in zip(eb_names, cell_sets.values())
241+
}
242+
243+
return Mesh(
244+
points,
245+
cells,
246+
point_data=point_data,
247+
cell_data=cell_data,
248+
point_sets=point_sets,
249+
info=info,
250+
cell_sets=cell_sets,
251+
)
252+
253+
254+
class FourCGeometry:
255+
256+
def __init__(
257+
self,
258+
mesh: Mesh,
259+
element_blocks: dict[str, np.array],
260+
node_sets: dict[str, np.array],
261+
):
262+
self.mesh = mesh
263+
self.element_blocks = element_blocks
264+
self.node_sets = node_sets
265+
266+
def get_element_ids_of_block(self, element_block_id):
267+
return self.element_blocks[element_block_id]
268+
269+
def get_node_sets(self, node_set_id):
270+
return self.node_sets[node_set_id]
271+
272+
@classmethod
273+
def from_exodus(cls, file_path):
274+
mesh = read_exodus(file_path, False)
275+
element_blocks = mesh.cell_sets.copy()
276+
node_sets = mesh.point_sets.copy()
277+
mesh.cell_sets = {}
278+
mesh.point_sets = {}
279+
280+
return cls(mesh, element_blocks, node_sets)
281+
282+
def __str__(self):
283+
string = "4C Geometry"
284+
if self.mesh.info:
285+
string += "\n Info"
286+
for s in self.mesh.info:
287+
string += "\n " + s
288+
string += f"\n Nodes: {len(self.mesh.points)}"
289+
string += f"\n Cells: {sum([len(c) for c in self.mesh.cells])}"
290+
if self.element_blocks:
291+
string += f"\n Element Blocks"
292+
for e, v in self.element_blocks.items():
293+
string += f"\n {e}: {len(v)} cells"
294+
if self.node_sets:
295+
string += f"\n Node Sets"
296+
for e, v in self.node_sets.items():
297+
string += f"\n {e}: {len(v)} nodes"
298+
299+
return string
300+
301+
302+
from pathlib import Path
303+
304+
mesh = FourCGeometry.from_exodus(
305+
Path(__file__).parents[2] / "tests/files/tutorial_solid_geo.e"
306+
)

tests/files/cube.exo

3.79 KB
Binary file not shown.

tests/files/tutorial_solid_geo.e

145 KB
Binary file not shown.

0 commit comments

Comments
 (0)