Skip to content

Commit 66497e7

Browse files
Support writing of hex elements to VTKHDF format. (openmc-dev#3623)
Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
1 parent 3c7a030 commit 66497e7

4 files changed

Lines changed: 103 additions & 62 deletions

File tree

docs/source/usersguide/processing.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,15 @@ VTK Mesh File Generation
9797
------------------------
9898

9999
VTK files of OpenMC meshes can be created using the
100-
:meth:`openmc.Mesh.write_data_to_vtk` method. Data can be applied to the
100+
:meth:`openmc.Mesh.write_data_to_vtk` method. This method supports several VTK
101+
formats depending on the mesh type. Structured meshes
102+
(:class:`~openmc.RegularMesh`, :class:`~openmc.RectilinearMesh`,
103+
:class:`~openmc.CylindricalMesh`, and :class:`~openmc.SphericalMesh`) can be
104+
exported to legacy VTK format (``.vtk``). The :class:`~openmc.UnstructuredMesh`
105+
class supports VTK unstructured grid formats (``.vtu``) as well as an HDF5-based
106+
format (``.vtkhdf``) that does not require the ``vtk`` module to write.
107+
108+
Data can be applied to the
101109
elements of the resulting mesh from mesh filter objects. This data can be
102110
provided either as a flat array or, in the case of structured meshes
103111
(:class:`~openmc.RegularMesh`, :class:`~openmc.RectilinearMesh`,

openmc/mesh.py

Lines changed: 52 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2738,7 +2738,8 @@ class UnstructuredMesh(MeshBase):
27382738
_UNSUPPORTED_ELEM = -1
27392739
_LINEAR_TET = 0
27402740
_LINEAR_HEX = 1
2741-
_VTK_TETRA = 10
2741+
_VTK_TET = 10
2742+
_VTK_HEX = 12
27422743

27432744
def __init__(self, filename: PathLike, library: str, mesh_id: int | None = None,
27442745
name: str = '', length_multiplier: float = 1.0,
@@ -3049,7 +3050,9 @@ def _write_data_to_vtk_ascii_format(
30493050
n_skipped += 1
30503051
continue
30513052
else:
3052-
raise RuntimeError(f"Invalid element type {elem_type} found")
3053+
raise RuntimeError(
3054+
f"Invalid element type {elem_type} found in mesh {self.id}"
3055+
)
30533056

30543057
for i, c in enumerate(conn):
30553058
if c == -1:
@@ -3106,35 +3109,42 @@ def _write_data_to_vtk_hdf5_format(
31063109
datasets: dict | None = None,
31073110
volume_normalization: bool = True,
31083111
):
3109-
def append_dataset(dset, array):
3110-
"""Convenience function to append data to an HDF5 dataset"""
3111-
origLen = dset.shape[0]
3112-
dset.resize(origLen + array.shape[0], axis=0)
3113-
dset[origLen:] = array
3114-
3115-
if self.library != "moab":
3116-
raise NotImplementedError("VTKHDF output is only supported for MOAB meshes")
3117-
3118-
# the self.connectivity contains arrays of length 8 to support hex
3119-
# elements as well, in the case of tetrahedra mesh elements, the
3120-
# last 4 values are -1 and are removed
3121-
trimmed_connectivity = []
3122-
for cell in self.connectivity:
3123-
# Find the index of the first -1 value, if any
3124-
first_negative_index = np.where(cell == -1)[0]
3125-
if first_negative_index.size > 0:
3126-
# Slice the array up to the first -1 value
3127-
trimmed_connectivity.append(cell[: first_negative_index[0]])
3112+
# This writer supports linear tetrahedra and linear hexahedra elements
3113+
conn_list = [] # flattened connectivity ids
3114+
cell_sizes = [] # number of points per cell
3115+
vtk_types = [] # VTK cell types per cell (uint8)
3116+
n_skipped = 0
3117+
3118+
for conn, etype in zip(self.connectivity, self.element_types):
3119+
if etype == self._LINEAR_TET:
3120+
ids = conn[:4]
3121+
vtk_types.append(self._VTK_TET)
3122+
elif etype == self._LINEAR_HEX:
3123+
ids = conn[:8]
3124+
vtk_types.append(self._VTK_HEX)
3125+
elif etype == self._UNSUPPORTED_ELEM:
3126+
n_skipped += 1
3127+
continue
31283128
else:
3129-
# No -1 values, append the whole cell
3130-
trimmed_connectivity.append(cell)
3131-
trimmed_connectivity = np.array(trimmed_connectivity, dtype="int32").flatten()
3129+
raise RuntimeError(
3130+
f"Invalid element type {etype} found in mesh {self.id}"
3131+
)
3132+
conn_list.extend(ids.tolist())
3133+
cell_sizes.append(len(ids))
31323134

3133-
# MOAB meshes supports tet elements only so we know it has 4 points per cell
3134-
points_per_cell = 4
3135+
if n_skipped > 0:
3136+
warnings.warn(
3137+
f"{n_skipped} elements were not written because "
3138+
"they are not of type linear tet/hex"
3139+
)
31353140

3136-
# offsets are the indices of the first point of each cell in the array of points
3137-
offsets = np.arange(0, self.n_elements * points_per_cell + 1, points_per_cell)
3141+
connectivity = np.asarray(conn_list, dtype=np.int64)
3142+
3143+
# Offsets must be length (numCells + 1) with a leading 0 and
3144+
# cumulative end-indices thereafter, per VTK's layout
3145+
cell_sizes_arr = np.asarray(cell_sizes, dtype=np.int64)
3146+
offsets = np.zeros(cell_sizes_arr.size + 1, dtype=np.int64)
3147+
np.cumsum(cell_sizes_arr, out=offsets[1:])
31383148

31393149
for name, data in datasets.items():
31403150
if data.shape != self.dimension:
@@ -3156,42 +3166,27 @@ def append_dataset(dset, array):
31563166
dtype=h5py.string_dtype("ascii", len(ascii_type)),
31573167
)
31583168

3159-
# create hdf5 file structure
3160-
root.create_dataset("NumberOfPoints", (0,), maxshape=(None,), dtype="i8")
3161-
root.create_dataset("Types", (0,), maxshape=(None,), dtype="uint8")
3162-
root.create_dataset("Points", (0, 3), maxshape=(None, 3), dtype="f")
3163-
root.create_dataset(
3164-
"NumberOfConnectivityIds", (0,), maxshape=(None,), dtype="i8"
3165-
)
3166-
root.create_dataset("NumberOfCells", (0,), maxshape=(None,), dtype="i8")
3167-
root.create_dataset("Offsets", (0,), maxshape=(None,), dtype="i8")
3168-
root.create_dataset("Connectivity", (0,), maxshape=(None,), dtype="i8")
3169-
3170-
append_dataset(root["NumberOfPoints"], np.array([len(self.vertices)]))
3171-
append_dataset(root["Points"], self.vertices)
3172-
append_dataset(
3173-
root["NumberOfConnectivityIds"],
3174-
np.array([len(trimmed_connectivity)]),
3175-
)
3176-
append_dataset(root["Connectivity"], trimmed_connectivity)
3177-
append_dataset(root["NumberOfCells"], np.array([self.n_elements]))
3178-
append_dataset(root["Offsets"], offsets)
3169+
# Create HDF5 file structure compliant with VTKHDF UnstructuredGrid
3170+
n_points = int(len(self.vertices))
3171+
n_cells = int(len(cell_sizes))
3172+
n_conn_ids = int(len(connectivity))
31793173

3180-
append_dataset(
3181-
root["Types"], np.full(self.n_elements, self._VTK_TETRA, dtype="uint8")
3182-
)
3174+
root.create_dataset("NumberOfPoints", data=(n_points,), dtype="i8")
3175+
root.create_dataset("NumberOfCells", data=(n_cells,), dtype="i8")
3176+
root.create_dataset("NumberOfConnectivityIds", data=(n_conn_ids,), dtype="i8")
3177+
root.create_dataset("Points", data=self.vertices.astype(np.float64, copy=False), dtype="f8")
3178+
root.create_dataset("Types", data=np.asarray(vtk_types, dtype=np.uint8), dtype="uint8")
3179+
root.create_dataset("Offsets", data=offsets.astype("i8"), dtype="i8")
3180+
root.create_dataset("Connectivity", data=connectivity.astype("i8"), dtype="i8")
31833181

31843182
cell_data_group = root.create_group("CellData")
31853183

31863184
for name, data in datasets.items():
3187-
3188-
cell_data_group.create_dataset(
3189-
name, (0,), maxshape=(None,), dtype="float64", chunks=True
3190-
)
3191-
31923185
if volume_normalization:
31933186
data /= self.volumes
3194-
append_dataset(cell_data_group[name], data)
3187+
cell_data_group.create_dataset(
3188+
name, data=data, dtype="float64", chunks=True
3189+
)
31953190

31963191
@classmethod
31973192
def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str):

tests/unit_tests/test_mesh.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -490,22 +490,37 @@ def test_umesh(run_in_tmpdir, simple_umesh, export_type):
490490
simple_umesh.write_data_to_vtk(datasets={'mean': ref_data[:-2]}, filename=filename)
491491

492492

493-
@pytest.mark.skipif(not openmc.lib._dagmc_enabled(), reason="DAGMC not enabled.")
494-
def test_write_vtkhdf(request, run_in_tmpdir):
493+
vtkhdf_tests = [
494+
(
495+
Path("test_mesh_dagmc_tets.vtk"),
496+
"moab"
497+
),
498+
(
499+
Path("test_mesh_hexes.exo"),
500+
"libmesh"
501+
)
502+
]
503+
@pytest.mark.parametrize('mesh_file, mesh_library', vtkhdf_tests)
504+
def test_write_vtkhdf(mesh_file, mesh_library, request, run_in_tmpdir):
495505
"""Performs a minimal UnstructuredMesh simulation, reads in the resulting
496506
statepoint file and writes the mesh data to vtk and vtkhdf files. It is
497507
necessary to read in the unstructured mesh from a statepoint file to ensure
498508
it has all the required attributes
499509
"""
510+
if mesh_library == 'moab' and not openmc.lib._dagmc_enabled():
511+
pytest.skip("DAGMC not enabled.")
512+
if mesh_library == 'libmesh' and not openmc.lib._libmesh_enabled():
513+
pytest.skip("LibMesh not enabled.")
514+
500515
model = openmc.Model()
501516

502517
surf1 = openmc.Sphere(r=1000.0, boundary_type="vacuum")
503518
cell1 = openmc.Cell(region=-surf1)
504519
model.geometry = openmc.Geometry([cell1])
505520

506521
umesh = openmc.UnstructuredMesh(
507-
request.path.parent / "test_mesh_dagmc_tets.vtk",
508-
"moab",
522+
request.path.parent / mesh_file,
523+
mesh_library,
509524
mesh_id = 1
510525
)
511526
mesh_filter = openmc.MeshFilter(umesh)
@@ -514,6 +529,8 @@ def test_write_vtkhdf(request, run_in_tmpdir):
514529
mesh_tally = openmc.Tally(name="test_tally")
515530
mesh_tally.filters = [mesh_filter]
516531
mesh_tally.scores = ["flux"]
532+
if mesh_library == "libmesh":
533+
mesh_tally.estimator = "collision"
517534

518535
model.tallies = [mesh_tally]
519536

@@ -556,6 +573,26 @@ def test_write_vtkhdf(request, run_in_tmpdir):
556573
with h5py.File("test_mesh.vtkhdf", "r"):
557574
...
558575

576+
import vtk
577+
reader = vtk.vtkHDFReader()
578+
reader.SetFileName("test_mesh.vtkhdf")
579+
reader.Update()
580+
581+
# Get mean from file and make sure it matches original data
582+
num_elements = reader.GetOutput().GetNumberOfCells()
583+
assert num_elements == umesh_from_sp.n_elements
584+
585+
num_vertices = reader.GetOutput().GetNumberOfPoints()
586+
assert num_vertices == umesh_from_sp.vertices.shape[0]
587+
588+
arr = reader.GetOutput().GetCellData().GetArray("mean")
589+
mean = np.array([arr.GetTuple1(i) for i in range(my_tally.mean.size)])
590+
np.testing.assert_almost_equal(mean, my_tally.mean.flatten()/umesh_from_sp.volumes)
591+
592+
arr = reader.GetOutput().GetCellData().GetArray("std_dev")
593+
std_dev = np.array([arr.GetTuple1(i) for i in range(my_tally.std_dev.size)])
594+
np.testing.assert_almost_equal(std_dev, my_tally.std_dev.flatten()/umesh_from_sp.volumes)
595+
559596

560597
def test_mesh_get_homogenized_materials():
561598
"""Test the get_homogenized_materials method"""
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../regression_tests/unstructured_mesh/test_mesh_hexes.exo

0 commit comments

Comments
 (0)