Skip to content

Commit d3124ee

Browse files
committed
Bypass XML round-trip in WeightWindowsList.export_to_hdf5
The XML serialization raised MemoryError on bound arrays >~200M elements -- lxml's intermediate ASCII allocation fails before the text node can be built. Write HDF5 directly via h5py, mirroring the C++ WeightWindows::to_hdf5 writer. Critical details for C++ compatibility: - Bounds are 2D (ne, n_voxels) on disk (4D would segfault the C++ tensor::Tensor<double> reader). - max_lower_bound_ratio is written unconditionally (default 1.0). - Root attrs filetype and version are required by openmc_weight_windows_import. Mesh writing is handled by a private _write_mesh_group helper in weight_windows.py that dispatches by mesh type, matching the reference implementation. UnstructuredMesh raises NotImplementedError (wwinp cannot produce one).
1 parent aa90915 commit d3124ee

2 files changed

Lines changed: 126 additions & 18 deletions

File tree

openmc/weight_windows.py

Lines changed: 103 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
import h5py
1111

1212
import openmc
13-
from openmc.mesh import MeshBase, RectilinearMesh, CylindricalMesh, SphericalMesh, UnstructuredMesh
13+
from openmc.mesh import (
14+
MeshBase, RegularMesh, RectilinearMesh, CylindricalMesh, SphericalMesh,
15+
UnstructuredMesh,
16+
)
1417
from openmc.tallies import Tallies
1518
import openmc.checkvalue as cv
1619
from openmc.checkvalue import PathLike
@@ -819,6 +822,51 @@ def hdf5_to_wws(path='weight_windows.h5') -> WeightWindowsList:
819822
return WeightWindowsList.from_hdf5(path)
820823

821824

825+
def _write_mesh_group(meshes_grp: h5py.Group, mesh: MeshBase):
826+
"""Write *mesh* into *meshes_grp* as a ``mesh <id>`` subgroup.
827+
828+
Matches the layout produced by ``Mesh::to_hdf5`` in src/mesh.cpp so the
829+
resulting file is readable by ``openmc_weight_windows_import``.
830+
"""
831+
grp = meshes_grp.create_group(f'mesh {mesh.id}')
832+
grp.attrs['id'] = np.int32(mesh.id)
833+
grp.create_dataset('name', data=np.bytes_(mesh.name or ''))
834+
835+
if isinstance(mesh, RegularMesh):
836+
grp.create_dataset('type', data=np.bytes_('regular'))
837+
grp.create_dataset('dimension',
838+
data=np.asarray(mesh.dimension, dtype=np.int32))
839+
grp.create_dataset('lower_left',
840+
data=np.asarray(mesh.lower_left, dtype=float))
841+
grp.create_dataset('upper_right',
842+
data=np.asarray(mesh.upper_right, dtype=float))
843+
grp.create_dataset('width', data=np.asarray(mesh.width, dtype=float))
844+
elif isinstance(mesh, RectilinearMesh):
845+
grp.create_dataset('type', data=np.bytes_('rectilinear'))
846+
grp.create_dataset('x_grid', data=np.asarray(mesh.x_grid, dtype=float))
847+
grp.create_dataset('y_grid', data=np.asarray(mesh.y_grid, dtype=float))
848+
grp.create_dataset('z_grid', data=np.asarray(mesh.z_grid, dtype=float))
849+
elif isinstance(mesh, CylindricalMesh):
850+
grp.create_dataset('type', data=np.bytes_('cylindrical'))
851+
grp.create_dataset('r_grid', data=np.asarray(mesh.r_grid, dtype=float))
852+
grp.create_dataset('phi_grid', data=np.asarray(mesh.phi_grid, dtype=float))
853+
grp.create_dataset('z_grid', data=np.asarray(mesh.z_grid, dtype=float))
854+
grp.create_dataset('origin', data=np.asarray(mesh.origin, dtype=float))
855+
elif isinstance(mesh, SphericalMesh):
856+
grp.create_dataset('type', data=np.bytes_('spherical'))
857+
grp.create_dataset('r_grid', data=np.asarray(mesh.r_grid, dtype=float))
858+
grp.create_dataset('theta_grid', data=np.asarray(mesh.theta_grid, dtype=float))
859+
grp.create_dataset('phi_grid', data=np.asarray(mesh.phi_grid, dtype=float))
860+
grp.create_dataset('origin', data=np.asarray(mesh.origin, dtype=float))
861+
elif isinstance(mesh, UnstructuredMesh):
862+
raise NotImplementedError(
863+
"Exporting weight windows on an UnstructuredMesh is not yet "
864+
"implemented in Python; use openmc.lib.export_weight_windows()."
865+
)
866+
else:
867+
raise TypeError(f"Unknown mesh type: {type(mesh).__name__}")
868+
869+
822870
class WeightWindowsList(list):
823871
"""A list of WeightWindows objects.
824872
@@ -1061,29 +1109,66 @@ def from_wwinp(cls, path: PathLike) -> Self:
10611109
def export_to_hdf5(self, path: PathLike = 'weight_windows.h5', **init_kwargs):
10621110
"""Write weight windows to an HDF5 file.
10631111
1112+
Writes the file directly via :mod:`h5py`, mirroring the layout
1113+
produced by :func:`openmc.lib.export_weight_windows`. The previous
1114+
XML round-trip raised :class:`MemoryError` on multi-GB bound arrays
1115+
because of the intermediate ASCII allocation inside lxml.
1116+
10641117
Parameters
10651118
----------
10661119
path : PathLike
10671120
Path to the file to write weight windows to
10681121
**init_kwargs
1069-
Keyword arguments passed to :func:`openmc.lib.init`
1070-
1122+
Unused. Retained for backward compatibility (previously forwarded
1123+
to :func:`openmc.lib.init`).
10711124
"""
1072-
import openmc.lib
10731125
cv.check_type('path', path, PathLike)
1074-
1075-
# Create a temporary model with the weight windows
1076-
model = openmc.Model()
1077-
sph = openmc.Sphere(boundary_type='vacuum')
1078-
cell = openmc.Cell(region=-sph)
1079-
model.geometry = openmc.Geometry([cell])
1080-
model.settings.weight_windows = self
1081-
model.settings.particles = 100
1082-
model.settings.batches = 1
1083-
1084-
# Get absolute path before moving to temporary directory
10851126
path = Path(path).resolve()
10861127

1087-
# Load the model with openmc.lib and then export it to an HDF5 file
1088-
with openmc.lib.TemporarySession(model, **init_kwargs):
1089-
openmc.lib.export_weight_windows(path)
1128+
with h5py.File(path, 'w') as f:
1129+
f.attrs['filetype'] = np.bytes_('weight_windows')
1130+
f.attrs['version'] = np.asarray([1, 0], dtype=np.int32)
1131+
1132+
meshes_grp = f.create_group('meshes')
1133+
wws_grp = f.create_group('weight_windows')
1134+
1135+
seen_mesh_ids = set()
1136+
ww_ids = []
1137+
for ww in self:
1138+
if ww.mesh.id not in seen_mesh_ids:
1139+
_write_mesh_group(meshes_grp, ww.mesh)
1140+
seen_mesh_ids.add(ww.mesh.id)
1141+
1142+
g = wws_grp.create_group(f'weight_windows_{ww.id}')
1143+
ww_ids.append(int(ww.id))
1144+
1145+
g.create_dataset('mesh', data=np.int32(ww.mesh.id))
1146+
g.create_dataset('particle_type',
1147+
data=np.bytes_(str(ww.particle_type)))
1148+
g.create_dataset('energy_bounds',
1149+
data=np.asarray(ww.energy_bounds, dtype=float))
1150+
1151+
# 2D (ne, n_voxels) C-contiguous: the C++ reader expects this layout.
1152+
ne = ww.lower_ww_bounds.shape[-1]
1153+
lo = np.ascontiguousarray(ww.lower_ww_bounds.T).reshape(ne, -1)
1154+
up = np.ascontiguousarray(ww.upper_ww_bounds.T).reshape(ne, -1)
1155+
g.create_dataset('lower_ww_bounds', data=lo)
1156+
g.create_dataset('upper_ww_bounds', data=up)
1157+
1158+
g.create_dataset('survival_ratio',
1159+
data=float(ww.survival_ratio))
1160+
# max_lower_bound_ratio is read unconditionally by C++; default 1.0 when unset.
1161+
mlbr = ww.max_lower_bound_ratio
1162+
g.create_dataset(
1163+
'max_lower_bound_ratio',
1164+
data=float(mlbr if mlbr is not None else 1.0),
1165+
)
1166+
g.create_dataset('max_split', data=np.int32(ww.max_split))
1167+
g.create_dataset('weight_cutoff',
1168+
data=float(ww.weight_cutoff))
1169+
1170+
wws_grp.attrs['ids'] = np.asarray(ww_ids, dtype=np.int32)
1171+
wws_grp.attrs['n_weight_windows'] = np.int32(len(ww_ids))
1172+
meshes_grp.attrs['ids'] = np.asarray(sorted(seen_mesh_ids),
1173+
dtype=np.int32)
1174+
meshes_grp.attrs['n_meshes'] = np.int32(len(seen_mesh_ids))

tests/unit_tests/weightwindows/test_ww_list.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import h5py
12
import openmc
23

34

@@ -21,3 +22,25 @@ def test_ww_roundtrip(request, run_in_tmpdir):
2122
assert ww.max_split == ww_new.max_split
2223
assert ww.weight_cutoff == ww_new.weight_cutoff
2324
assert ww.mesh.id == ww_new.mesh.id
25+
26+
27+
def test_export_hdf5_format(request, run_in_tmpdir):
28+
# C++ openmc_weight_windows_import expects this on-disk layout.
29+
wws = openmc.WeightWindowsList.from_wwinp(request.path.with_name('wwinp_n'))
30+
wws.export_to_hdf5('ww.h5')
31+
with h5py.File('ww.h5') as f:
32+
assert f.attrs['filetype'] == b'weight_windows'
33+
assert list(f.attrs['version']) == [1, 0]
34+
wws_grp = f['weight_windows']
35+
assert int(wws_grp.attrs['n_weight_windows']) == len(wws)
36+
for ww in wws:
37+
g = wws_grp[f'weight_windows_{ww.id}']
38+
# 2D shape is required by the C++ tensor::Tensor<double> reader.
39+
assert g['lower_ww_bounds'].ndim == 2
40+
assert g['lower_ww_bounds'].shape[0] == ww.num_energy_bins
41+
# max_lower_bound_ratio is read unconditionally by C++.
42+
assert 'max_lower_bound_ratio' in g
43+
m_grp = f['meshes']
44+
for name in m_grp:
45+
assert 'id' in m_grp[name].attrs
46+
assert 'type' in m_grp[name]

0 commit comments

Comments
 (0)