-
Notifications
You must be signed in to change notification settings - Fork 644
wwinp files: Fix MemoryError in WeightWindowsList.export_to_hdf5 and speed up from_wwinp. Alternative Approach #3951
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
aa90915
6c4ad09
9817c8e
cf1c44f
40539f1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -306,6 +306,29 @@ def from_hdf5(cls, group: h5py.Group): | |
| else: | ||
| raise ValueError('Unrecognized mesh type: "' + mesh_type + '"') | ||
|
|
||
| def to_hdf5(self, group: h5py.Group) -> h5py.Group: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be declared as an abstractmethod. That way it is clear that each mesh type should implement this method. |
||
| """Write this mesh into *group* as a subgroup named ``mesh <id>``. | ||
|
|
||
| Subclasses override this method to call ``super().to_hdf5(group)``, | ||
| write a ``type`` dataset, and append type-specific grid data. | ||
|
|
||
| .. versionadded:: 0.15.4 | ||
|
|
||
| Parameters | ||
| ---------- | ||
| group : h5py.Group | ||
| Parent HDF5 group (typically ``/meshes``). | ||
|
|
||
| Returns | ||
| ------- | ||
| mesh_group : h5py.Group | ||
| The created ``mesh <id>`` subgroup, ready for subclass extension. | ||
| """ | ||
| mesh_group = group.create_group(f'mesh {self.id}') | ||
| mesh_group.attrs['id'] = np.int32(self.id) | ||
| mesh_group.create_dataset('name', data=np.bytes_(self.name or '')) | ||
| return mesh_group | ||
|
|
||
| def to_xml_element(self): | ||
| """Return XML representation of the mesh | ||
|
|
||
|
|
@@ -1231,6 +1254,18 @@ def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str): | |
|
|
||
| return mesh | ||
|
|
||
| def to_hdf5(self, group: h5py.Group): | ||
| mesh_group = super().to_hdf5(group) | ||
| mesh_group.create_dataset('type', data=np.bytes_('regular')) | ||
| mesh_group.create_dataset( | ||
| 'dimension', data=np.asarray(self.dimension, dtype=np.int32)) | ||
| mesh_group.create_dataset( | ||
| 'lower_left', data=np.asarray(self.lower_left, dtype=float)) | ||
| mesh_group.create_dataset( | ||
| 'upper_right', data=np.asarray(self.upper_right, dtype=float)) | ||
| mesh_group.create_dataset( | ||
| 'width', data=np.asarray(self.width, dtype=float)) | ||
|
|
||
| @classmethod | ||
| def from_rect_lattice( | ||
| cls, | ||
|
|
@@ -1708,6 +1743,16 @@ def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str): | |
|
|
||
| return mesh | ||
|
|
||
| def to_hdf5(self, group: h5py.Group): | ||
| mesh_group = super().to_hdf5(group) | ||
| mesh_group.create_dataset('type', data=np.bytes_('rectilinear')) | ||
| mesh_group.create_dataset( | ||
| 'x_grid', data=np.asarray(self.x_grid, dtype=float)) | ||
| mesh_group.create_dataset( | ||
| 'y_grid', data=np.asarray(self.y_grid, dtype=float)) | ||
| mesh_group.create_dataset( | ||
| 'z_grid', data=np.asarray(self.z_grid, dtype=float)) | ||
|
|
||
| @classmethod | ||
| def from_xml_element(cls, elem: ET.Element): | ||
| """Generate a rectilinear mesh from an XML element | ||
|
|
@@ -2104,6 +2149,18 @@ def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str): | |
|
|
||
| return mesh | ||
|
|
||
| def to_hdf5(self, group: h5py.Group): | ||
| mesh_group = super().to_hdf5(group) | ||
| mesh_group.create_dataset('type', data=np.bytes_('cylindrical')) | ||
| mesh_group.create_dataset( | ||
| 'r_grid', data=np.asarray(self.r_grid, dtype=float)) | ||
| mesh_group.create_dataset( | ||
| 'phi_grid', data=np.asarray(self.phi_grid, dtype=float)) | ||
| mesh_group.create_dataset( | ||
| 'z_grid', data=np.asarray(self.z_grid, dtype=float)) | ||
| mesh_group.create_dataset( | ||
| 'origin', data=np.asarray(self.origin, dtype=float)) | ||
|
|
||
| @classmethod | ||
| def from_bounding_box( | ||
| cls, | ||
|
|
@@ -2476,6 +2533,18 @@ def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str): | |
|
|
||
| return mesh | ||
|
|
||
| def to_hdf5(self, group: h5py.Group): | ||
| mesh_group = super().to_hdf5(group) | ||
| mesh_group.create_dataset('type', data=np.bytes_('spherical')) | ||
| mesh_group.create_dataset( | ||
| 'r_grid', data=np.asarray(self.r_grid, dtype=float)) | ||
| mesh_group.create_dataset( | ||
| 'theta_grid', data=np.asarray(self.theta_grid, dtype=float)) | ||
| mesh_group.create_dataset( | ||
| 'phi_grid', data=np.asarray(self.phi_grid, dtype=float)) | ||
| mesh_group.create_dataset( | ||
| 'origin', data=np.asarray(self.origin, dtype=float)) | ||
|
|
||
| @classmethod | ||
| def from_bounding_box( | ||
| cls, | ||
|
|
@@ -3220,6 +3289,14 @@ def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str): | |
|
|
||
| return mesh | ||
|
|
||
| def to_hdf5(self, group: h5py.Group): | ||
| # Raise before super() so no half-built 'mesh <id>' group is left on disk. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no super afterwards so I think this comment is not needed. |
||
| raise NotImplementedError( | ||
| "UnstructuredMesh.to_hdf5 is not implemented in Python. " | ||
| "Use openmc.lib.export_weight_windows() to export weight " | ||
| "windows on unstructured meshes." | ||
| ) | ||
|
|
||
| def to_xml_element(self): | ||
| """Return XML representation of the mesh | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -140,9 +140,7 @@ def __init__( | |
| "upper_bound_ratio must be present.") | ||
|
|
||
| if upper_bound_ratio: | ||
| self.upper_ww_bounds = [ | ||
| lb * upper_bound_ratio for lb in self.lower_ww_bounds | ||
| ] | ||
| self.upper_ww_bounds = self.lower_ww_bounds * upper_bound_ratio | ||
|
|
||
| if upper_ww_bounds is not None: | ||
| self.upper_ww_bounds = upper_ww_bounds | ||
|
|
@@ -1063,29 +1061,83 @@ def from_wwinp(cls, path: PathLike) -> Self: | |
| def export_to_hdf5(self, path: PathLike = 'weight_windows.h5', **init_kwargs): | ||
| """Write weight windows to an HDF5 file. | ||
|
|
||
| Structured-mesh weight windows are written directly via :mod:`h5py`, | ||
| avoiding the multi-GB ASCII intermediate that previously caused | ||
| :class:`MemoryError` on large wwinp inputs. | ||
|
|
||
| Weight windows on an :class:`~openmc.UnstructuredMesh` require | ||
| LibMesh or MOAB (loaded by :func:`openmc.lib.init`) to materialize | ||
| the mesh's vertex and connectivity data; for those, this method | ||
| falls back to :func:`openmc.lib.export_weight_windows`. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| path : PathLike | ||
| Path to the file to write weight windows to | ||
| **init_kwargs | ||
| Keyword arguments passed to :func:`openmc.lib.init` | ||
|
|
||
| Forwarded to :func:`openmc.lib.init` when the UnstructuredMesh | ||
| fallback path is used. Unused for the direct h5py path. | ||
| """ | ||
| import openmc.lib | ||
| cv.check_type('path', path, PathLike) | ||
|
|
||
| # Create a temporary model with the weight windows | ||
| model = openmc.Model() | ||
| sph = openmc.Sphere(boundary_type='vacuum') | ||
| cell = openmc.Cell(region=-sph) | ||
| model.geometry = openmc.Geometry([cell]) | ||
| model.settings.weight_windows = self | ||
| model.settings.particles = 100 | ||
| model.settings.batches = 1 | ||
|
|
||
| # Get absolute path before moving to temporary directory | ||
| path = Path(path).resolve() | ||
|
|
||
| # Load the model with openmc.lib and then export it to an HDF5 file | ||
| with openmc.lib.TemporarySession(model, **init_kwargs): | ||
| openmc.lib.export_weight_windows(path) | ||
| # Any unstructured mesh forces the whole list onto the lib fallback. | ||
| if any(isinstance(ww.mesh, UnstructuredMesh) for ww in self): | ||
| import openmc.lib | ||
| model = openmc.Model() | ||
| sph = openmc.Sphere(boundary_type='vacuum') | ||
| model.geometry = openmc.Geometry([openmc.Cell(region=-sph)]) | ||
| model.settings.weight_windows = self | ||
| model.settings.particles = 100 | ||
| model.settings.batches = 1 | ||
| with openmc.lib.TemporarySession(model, **init_kwargs): | ||
| openmc.lib.export_weight_windows(path) | ||
| return | ||
|
Comment on lines
+1084
to
+1095
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think a cleaner solution will be to implement UnstructuredMesh.to_hdf5 using openmc.lib.export_weight_windows under the hood. What do you think? |
||
|
|
||
| with h5py.File(path, 'w') as f: | ||
| f.attrs['filetype'] = np.bytes_('weight_windows') | ||
| f.attrs['version'] = np.asarray([1, 0], dtype=np.int32) | ||
|
|
||
| meshes_grp = f.create_group('meshes') | ||
| wws_grp = f.create_group('weight_windows') | ||
|
|
||
| seen_mesh_ids = set() | ||
| ww_ids = [] | ||
| for ww in self: | ||
| if ww.mesh.id not in seen_mesh_ids: | ||
| ww.mesh.to_hdf5(meshes_grp) | ||
| seen_mesh_ids.add(ww.mesh.id) | ||
|
|
||
| g = wws_grp.create_group(f'weight_windows_{ww.id}') | ||
| ww_ids.append(int(ww.id)) | ||
|
|
||
| g.create_dataset('mesh', data=np.int32(ww.mesh.id)) | ||
| g.create_dataset('particle_type', | ||
| data=np.bytes_(str(ww.particle_type))) | ||
| g.create_dataset('energy_bounds', | ||
| data=np.asarray(ww.energy_bounds, dtype=float)) | ||
|
|
||
| # 2D (ne, n_voxels) C-contiguous: the C++ reader expects this layout. | ||
| ne = ww.lower_ww_bounds.shape[-1] | ||
| lo = np.ascontiguousarray(ww.lower_ww_bounds.T).reshape(ne, -1) | ||
| up = np.ascontiguousarray(ww.upper_ww_bounds.T).reshape(ne, -1) | ||
| g.create_dataset('lower_ww_bounds', data=lo) | ||
| g.create_dataset('upper_ww_bounds', data=up) | ||
|
|
||
| g.create_dataset('survival_ratio', | ||
| data=float(ww.survival_ratio)) | ||
| # max_lower_bound_ratio is read unconditionally by C++; default 1.0 when unset. | ||
| mlbr = ww.max_lower_bound_ratio | ||
| g.create_dataset( | ||
| 'max_lower_bound_ratio', | ||
| data=float(mlbr if mlbr is not None else 1.0), | ||
| ) | ||
| g.create_dataset('max_split', data=np.int32(ww.max_split)) | ||
| g.create_dataset('weight_cutoff', | ||
| data=float(ww.weight_cutoff)) | ||
|
|
||
| wws_grp.attrs['ids'] = np.asarray(ww_ids, dtype=np.int32) | ||
| wws_grp.attrs['n_weight_windows'] = np.int32(len(ww_ids)) | ||
| meshes_grp.attrs['ids'] = np.asarray(sorted(seen_mesh_ids), | ||
| dtype=np.int32) | ||
| meshes_grp.attrs['n_meshes'] = np.int32(len(seen_mesh_ids)) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should separate the complex and float cases.
Because if someone expects a Real iterable and get numpy array with complex dtype it should error out.
Or maybe we should get rid altogether of the complex case. I do not know of a place in openmc when we use complex numbers. Do you know of a use case?