|
| 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 | +) |
0 commit comments