Skip to content

Commit 529a901

Browse files
committed
VTKHDF output for structured meshes and clean up implementation
1 parent 4bda85f commit 529a901

3 files changed

Lines changed: 560 additions & 101 deletions

File tree

openmc/mesh.py

Lines changed: 255 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,32 @@ def material_volumes(
502502

503503
# Restore original tallies
504504
model.tallies = original_tallies
505-
return volumes
505+
506+
# Sort each element's materials by material ID (ascending), with
507+
# None (-1) after positive IDs and empty slots (-2) last. This
508+
# gives a deterministic ordering that is independent of the ray
509+
# traversal order used by the C library.
510+
mat_arr = volumes._materials.copy()
511+
vol_arr = volumes._volumes.copy()
512+
bbox_arr = volumes._bboxes.copy() if volumes._bboxes is not None else None
513+
n_elements, table_size = mat_arr.shape
514+
515+
def _sort_key(m):
516+
m = int(m)
517+
if m == -2:
518+
return (2, 0) # empty slot – always last
519+
if m == -1:
520+
return (1, 0) # vacuum / None – just before empty
521+
return (0, m) # real material – ascending ID
522+
523+
for i in range(n_elements):
524+
indices = sorted(range(table_size), key=lambda j: _sort_key(mat_arr[i, j]))
525+
mat_arr[i] = mat_arr[i, indices]
526+
vol_arr[i] = vol_arr[i, indices]
527+
if bbox_arr is not None:
528+
bbox_arr[i] = bbox_arr[i, indices]
529+
530+
return MeshMaterialVolumes(mat_arr, vol_arr, bbox_arr)
506531

507532

508533
class StructuredMesh(MeshBase):
@@ -662,7 +687,7 @@ def num_mesh_cells(self):
662687
def write_data_to_vtk(self,
663688
filename: PathLike,
664689
datasets: dict | None = None,
665-
volume_normalization: bool = True,
690+
volume_normalization: bool = False,
666691
curvilinear: bool = False):
667692
"""Creates a VTK object of the mesh
668693
@@ -712,6 +737,15 @@ def write_data_to_vtk(self,
712737
>>> heating = tally.get_reshaped_data(expand_dims=True)
713738
>>> mesh.write_data_to_vtk({'heating': heating})
714739
"""
740+
if Path(filename).suffix == ".vtkhdf":
741+
write_impl = getattr(self, '_write_vtk_hdf5', None)
742+
if write_impl is None:
743+
raise NotImplementedError(f"VTKHDF output not implemented for {type(self).__name__}")
744+
# write_impl is a bound method – do NOT pass self again
745+
write_impl(filename, datasets, volume_normalization)
746+
return None
747+
748+
# vtk is an optional dependency only needed for the legacy ASCII path
715749
import vtk
716750
from vtk.util import numpy_support as nps
717751

@@ -737,11 +771,11 @@ def write_data_to_vtk(self,
737771
# API
738772
# TODO: update to "C" ordering throughout
739773
if dataset.ndim == 3:
740-
dataset = dataset.T.ravel()
774+
dataset = dataset.ravel()
741775
datasets_out.append(dataset)
742776

743777
if volume_normalization:
744-
dataset /= self.volumes.T.ravel()
778+
dataset /= self.volumes.ravel()
745779

746780
dataset_array = vtk.vtkDoubleArray()
747781
dataset_array.SetName(label)
@@ -1477,6 +1511,72 @@ def get_indices_at_coords(self, coords: Sequence[float]) -> tuple:
14771511
indices = np.floor((coords_array - lower_left) / spacing).astype(int)
14781512
return tuple(int(i) for i in indices[:ndim])
14791513

1514+
def _write_vtk_hdf5(self, filename, datasets, volume_normalization):
1515+
"""Write RegularMesh as VTKHDF StructuredGrid format."""
1516+
dims = self.dimension
1517+
ndim = len(dims)
1518+
1519+
# Vertex dimensions (cells + 1) – store only ndim entries so that
1520+
# 1-D and 2-D meshes carry the right number of dimensions.
1521+
vertex_dims = [d + 1 for d in dims]
1522+
1523+
# Build explicit point coordinates. Pad coordinate arrays to 3-D so
1524+
# that every point has an (x, y, z) triple; extra coordinates are 0.
1525+
coords_1d = []
1526+
for i in range(ndim):
1527+
c = np.linspace(self.lower_left[i], self.upper_right[i], dims[i] + 1)
1528+
coords_1d.append(c)
1529+
while len(coords_1d) < 3:
1530+
coords_1d.append(np.array([0.0]))
1531+
1532+
# np.meshgrid with indexing='ij' → axis 0 = x, axis 1 = y, axis 2 = z
1533+
xx, yy, zz = np.meshgrid(*coords_1d, indexing='ij')
1534+
# Flatten in Fortran (x-fastest) order for VTK point ordering
1535+
points = np.column_stack([
1536+
xx.ravel(order='F'),
1537+
yy.ravel(order='F'),
1538+
zz.ravel(order='F'),
1539+
]).astype(np.float64) # shape (n_points, 3)
1540+
1541+
with h5py.File(filename, "w") as f:
1542+
root = f.create_group("VTKHDF")
1543+
root.attrs["Version"] = (2, 1)
1544+
_type = "StructuredGrid".encode("ascii")
1545+
root.attrs.create(
1546+
"Type",
1547+
_type,
1548+
dtype=h5py.string_dtype("ascii", len(_type)),
1549+
)
1550+
root.create_dataset("Dimensions", data=vertex_dims, dtype="i8")
1551+
root.create_dataset("Points", data=points, dtype="f8")
1552+
1553+
cell_data_group = root.create_group("CellData")
1554+
1555+
if not datasets:
1556+
return
1557+
1558+
for name, data in datasets.items():
1559+
data = self._reshape_vtk_dataset(data)
1560+
if data.ndim > 1 and data.shape != self.dimension:
1561+
raise ValueError(
1562+
f'Cannot apply multidimensional dataset "{name}" with '
1563+
f"shape {data.shape} to mesh {self.id} "
1564+
f"with dimensions {self.dimension}"
1565+
)
1566+
if data.size != self.n_elements:
1567+
raise ValueError(
1568+
f"The size of the dataset '{name}' ({data.size}) should be"
1569+
f" equal to the number of mesh cells ({self.n_elements})"
1570+
)
1571+
1572+
if volume_normalization:
1573+
data = data / self.volumes
1574+
1575+
flat_data = data.T.ravel() if data.ndim > 1 else data.ravel()
1576+
cell_data_group.create_dataset(
1577+
name, data=flat_data, dtype="float64", chunks=True
1578+
)
1579+
14801580

14811581
def Mesh(*args, **kwargs):
14821582
warnings.warn("Mesh has been renamed RegularMesh. Future versions of "
@@ -1693,6 +1793,55 @@ def get_indices_at_coords(self, coords: Sequence[float]) -> tuple:
16931793
"get_indices_at_coords is not yet implemented for RectilinearMesh"
16941794
)
16951795

1796+
def _write_vtk_hdf5(self, filename, datasets, volume_normalization):
1797+
"""Write RectilinearMesh as VTK-HDF5 StructuredGrid format.
1798+
1799+
Note: vtkRectilinearGrid is not part of the VTKHDF spec yet, so
1800+
StructuredGrid with explicit point coordinates is used instead.
1801+
"""
1802+
nx, ny, nz = self.dimension
1803+
vertex_dims = [nx + 1, ny + 1, nz + 1]
1804+
1805+
vertices = np.stack(np.meshgrid(
1806+
self.x_grid, self.y_grid, self.z_grid, indexing='ij'
1807+
), axis=-1)
1808+
1809+
with h5py.File(filename, "w") as f:
1810+
root = f.create_group("VTKHDF")
1811+
root.attrs["Version"] = (2, 1)
1812+
root.attrs["Type"] = b"StructuredGrid"
1813+
root.create_dataset("Dimensions", data=vertex_dims, dtype="i8")
1814+
1815+
points = vertices.reshape(-1, 3)
1816+
root.create_dataset("Points", data=points.astype(np.float64), dtype="f8")
1817+
1818+
cell_data_group = root.create_group("CellData")
1819+
1820+
if not datasets:
1821+
return
1822+
1823+
for name, data in datasets.items():
1824+
data = self._reshape_vtk_dataset(data)
1825+
if data.ndim > 1 and data.shape != self.dimension:
1826+
raise ValueError(
1827+
f'Cannot apply dataset "{name}" with '
1828+
f"shape {data.shape} to mesh {self.id} "
1829+
f"with dimensions {self.dimension}"
1830+
)
1831+
if data.size != self.n_elements:
1832+
raise ValueError(
1833+
f"The size of the dataset '{name}' ({data.size}) should be"
1834+
f" equal to the number of mesh cells ({self.n_elements})"
1835+
)
1836+
1837+
if volume_normalization:
1838+
data = data / self.volumes
1839+
1840+
flat_data = data.T.ravel() if data.ndim > 1 else data.ravel()
1841+
cell_data_group.create_dataset(
1842+
name, data=flat_data, dtype="float64", chunks=True
1843+
)
1844+
16961845

16971846
class CylindricalMesh(StructuredMesh):
16981847
"""A 3D cylindrical mesh
@@ -2146,6 +2295,53 @@ def _convert_to_cartesian(arr, origin: Sequence[float]):
21462295
arr[..., 2] += origin[2]
21472296
return arr
21482297

2298+
def _write_vtk_hdf5(self, filename, datasets, volume_normalization):
2299+
"""Write CylindricalMesh as VTK-HDF5 StructuredGrid format."""
2300+
nr, nphi, nz = self.dimension
2301+
vertex_dims = [nr + 1, nphi + 1, nz + 1]
2302+
2303+
R, Phi, Z = np.meshgrid(self.r_grid, self.phi_grid, self.z_grid, indexing='ij')
2304+
X = R * np.cos(Phi) + self.origin[0]
2305+
Y = R * np.sin(Phi) + self.origin[1]
2306+
Z = Z + self.origin[2]
2307+
vertices = np.stack([X, Y, Z], axis=-1)
2308+
2309+
with h5py.File(filename, "w") as f:
2310+
root = f.create_group("VTKHDF")
2311+
root.attrs["Version"] = (2, 1)
2312+
root.attrs["Type"] = b"StructuredGrid"
2313+
root.create_dataset("Dimensions", data=vertex_dims, dtype="i8")
2314+
2315+
points = vertices.reshape(-1, 3)
2316+
root.create_dataset("Points", data=points.astype(np.float64), dtype="f8")
2317+
2318+
cell_data_group = root.create_group("CellData")
2319+
2320+
if not datasets:
2321+
return
2322+
2323+
for name, data in datasets.items():
2324+
data = self._reshape_vtk_dataset(data)
2325+
if data.ndim > 1 and data.shape != self.dimension:
2326+
raise ValueError(
2327+
f'Cannot apply dataset "{name}" with '
2328+
f"shape {data.shape} to mesh {self.id} "
2329+
f"with dimensions {self.dimension}"
2330+
)
2331+
if data.size != self.n_elements:
2332+
raise ValueError(
2333+
f"The size of the dataset '{name}' ({data.size}) should be"
2334+
f" equal to the number of mesh cells ({self.n_elements})"
2335+
)
2336+
2337+
if volume_normalization:
2338+
data = data / self.volumes
2339+
2340+
flat_data = data.T.ravel() if data.ndim > 1 else data.ravel()
2341+
cell_data_group.create_dataset(
2342+
name, data=flat_data, dtype="float64", chunks=True
2343+
)
2344+
21492345

21502346
class SphericalMesh(StructuredMesh):
21512347
"""A 3D spherical mesh
@@ -2533,6 +2729,55 @@ def get_indices_at_coords(self, coords: Sequence[float]) -> tuple:
25332729
"get_indices_at_coords is not yet implemented for SphericalMesh"
25342730
)
25352731

2732+
def _write_vtk_hdf5(self, filename, datasets, volume_normalization):
2733+
"""Write SphericalMesh as VTK-HDF5 StructuredGrid format."""
2734+
nr, ntheta, nphi = self.dimension
2735+
vertex_dims = [nr + 1, ntheta + 1, nphi + 1]
2736+
2737+
R, Theta, Phi = np.meshgrid(
2738+
self.r_grid, self.theta_grid, self.phi_grid, indexing='ij'
2739+
)
2740+
X = R * np.sin(Theta) * np.cos(Phi) + self.origin[0]
2741+
Y = R * np.sin(Theta) * np.sin(Phi) + self.origin[1]
2742+
Z = R * np.cos(Theta) + self.origin[2]
2743+
vertices = np.stack([X, Y, Z], axis=-1)
2744+
2745+
with h5py.File(filename, "w") as f:
2746+
root = f.create_group("VTKHDF")
2747+
root.attrs["Version"] = (2, 1)
2748+
root.attrs["Type"] = b"StructuredGrid"
2749+
root.create_dataset("Dimensions", data=vertex_dims, dtype="i8")
2750+
2751+
points = vertices.reshape(-1, 3)
2752+
root.create_dataset("Points", data=points.astype(np.float64), dtype="f8")
2753+
2754+
cell_data_group = root.create_group("CellData")
2755+
2756+
if not datasets:
2757+
return
2758+
2759+
for name, data in datasets.items():
2760+
data = self._reshape_vtk_dataset(data)
2761+
if data.ndim > 1 and data.shape != self.dimension:
2762+
raise ValueError(
2763+
f'Cannot apply dataset "{name}" with '
2764+
f"shape {data.shape} to mesh {self.id} "
2765+
f"with dimensions {self.dimension}"
2766+
)
2767+
if data.size != self.n_elements:
2768+
raise ValueError(
2769+
f"The size of the dataset '{name}' ({data.size}) should be"
2770+
f" equal to the number of mesh cells ({self.n_elements})"
2771+
)
2772+
2773+
if volume_normalization:
2774+
data = data / self.volumes
2775+
2776+
flat_data = data.T.ravel() if data.ndim > 1 else data.ravel()
2777+
cell_data_group.create_dataset(
2778+
name, data=flat_data, dtype="float64", chunks=True
2779+
)
2780+
25362781

25372782
def require_statepoint_data(func):
25382783
@wraps(func)
@@ -2989,6 +3234,10 @@ def _write_data_to_vtk_hdf5_format(
29893234
datasets: dict | None = None,
29903235
volume_normalization: bool = True,
29913236
):
3237+
"""Write UnstructuredMesh as VTK-HDF5 UnstructuredGrid format.
3238+
3239+
Supports linear tetrahedra and linear hexahedra elements.
3240+
"""
29923241
def append_dataset(dset, array):
29933242
"""Convenience function to append data to an HDF5 dataset"""
29943243
origLen = dset.shape[0]
@@ -3028,7 +3277,6 @@ def append_dataset(dset, array):
30283277
)
30293278

30303279
with h5py.File(filename, "w") as f:
3031-
30323280
root = f.create_group("VTKHDF")
30333281
vtk_file_format_version = (2, 1)
30343282
root.attrs["Version"] = vtk_file_format_version
@@ -3067,7 +3315,7 @@ def append_dataset(dset, array):
30673315
cell_data_group = root.create_group("CellData")
30683316

30693317
for name, data in datasets.items():
3070-
3318+
30713319
cell_data_group.create_dataset(
30723320
name, (0,), maxshape=(None,), dtype="float64", chunks=True
30733321
)
@@ -3205,4 +3453,4 @@ def _read_meshes(elem):
32053453
_HEX_MIDPOINT_CONN += ((2, (0, 0, 0)),
32063454
(2, (1, 0, 0)),
32073455
(2, (1, 1, 0)),
3208-
(2, (0, 1, 0)))
3456+
(2, (0, 1, 0)))

tests/unit_tests/conftest.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
1+
import os
2+
13
import openmc
24
import pytest
35

46
from tests.regression_tests import config
57

68

9+
@pytest.fixture
10+
def run_in_tmpdir(tmp_path):
11+
"""Run test in a temporary directory, restoring cwd afterwards."""
12+
orig = os.getcwd()
13+
os.chdir(tmp_path)
14+
yield tmp_path
15+
os.chdir(orig)
16+
17+
718
@pytest.fixture(scope='module')
819
def mpi_intracomm():
920
if config['mpi']:

0 commit comments

Comments
 (0)