|
10 | 10 | import h5py |
11 | 11 |
|
12 | 12 | 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 | +) |
14 | 17 | from openmc.tallies import Tallies |
15 | 18 | import openmc.checkvalue as cv |
16 | 19 | from openmc.checkvalue import PathLike |
@@ -335,11 +338,16 @@ def to_xml_element(self) -> ET.Element: |
335 | 338 | subelement = ET.SubElement(element, 'energy_bounds') |
336 | 339 | subelement.text = ' '.join(str(e) for e in self.energy_bounds) |
337 | 340 |
|
| 341 | + # '%.17g' round-trips IEEE 754 doubles through float() losslessly. |
| 342 | + def _arr_to_text(arr): |
| 343 | + return ' '.join(np.char.mod('%.17g', |
| 344 | + np.ascontiguousarray(arr.ravel('F')))) |
| 345 | + |
338 | 346 | subelement = ET.SubElement(element, 'lower_ww_bounds') |
339 | | - subelement.text = ' '.join(str(b) for b in self.lower_ww_bounds.ravel('F')) |
| 347 | + subelement.text = _arr_to_text(self.lower_ww_bounds) |
340 | 348 |
|
341 | 349 | subelement = ET.SubElement(element, 'upper_ww_bounds') |
342 | | - subelement.text = ' '.join(str(b) for b in self.upper_ww_bounds.ravel('F')) |
| 350 | + subelement.text = _arr_to_text(self.upper_ww_bounds) |
343 | 351 |
|
344 | 352 | subelement = ET.SubElement(element, 'survival_ratio') |
345 | 353 | subelement.text = str(self.survival_ratio) |
@@ -819,6 +827,51 @@ def hdf5_to_wws(path='weight_windows.h5') -> WeightWindowsList: |
819 | 827 | return WeightWindowsList.from_hdf5(path) |
820 | 828 |
|
821 | 829 |
|
| 830 | +def _write_mesh_group(meshes_grp: h5py.Group, mesh: MeshBase): |
| 831 | + """Write *mesh* into *meshes_grp* as a ``mesh <id>`` subgroup. |
| 832 | +
|
| 833 | + Matches the layout produced by ``Mesh::to_hdf5`` in src/mesh.cpp so the |
| 834 | + resulting file is readable by ``openmc_weight_windows_import``. |
| 835 | + """ |
| 836 | + grp = meshes_grp.create_group(f'mesh {mesh.id}') |
| 837 | + grp.attrs['id'] = np.int32(mesh.id) |
| 838 | + grp.create_dataset('name', data=np.bytes_(mesh.name or '')) |
| 839 | + |
| 840 | + if isinstance(mesh, RegularMesh): |
| 841 | + grp.create_dataset('type', data=np.bytes_('regular')) |
| 842 | + grp.create_dataset('dimension', |
| 843 | + data=np.asarray(mesh.dimension, dtype=np.int32)) |
| 844 | + grp.create_dataset('lower_left', |
| 845 | + data=np.asarray(mesh.lower_left, dtype=float)) |
| 846 | + grp.create_dataset('upper_right', |
| 847 | + data=np.asarray(mesh.upper_right, dtype=float)) |
| 848 | + grp.create_dataset('width', data=np.asarray(mesh.width, dtype=float)) |
| 849 | + elif isinstance(mesh, RectilinearMesh): |
| 850 | + grp.create_dataset('type', data=np.bytes_('rectilinear')) |
| 851 | + grp.create_dataset('x_grid', data=np.asarray(mesh.x_grid, dtype=float)) |
| 852 | + grp.create_dataset('y_grid', data=np.asarray(mesh.y_grid, dtype=float)) |
| 853 | + grp.create_dataset('z_grid', data=np.asarray(mesh.z_grid, dtype=float)) |
| 854 | + elif isinstance(mesh, CylindricalMesh): |
| 855 | + grp.create_dataset('type', data=np.bytes_('cylindrical')) |
| 856 | + grp.create_dataset('r_grid', data=np.asarray(mesh.r_grid, dtype=float)) |
| 857 | + grp.create_dataset('phi_grid', data=np.asarray(mesh.phi_grid, dtype=float)) |
| 858 | + grp.create_dataset('z_grid', data=np.asarray(mesh.z_grid, dtype=float)) |
| 859 | + grp.create_dataset('origin', data=np.asarray(mesh.origin, dtype=float)) |
| 860 | + elif isinstance(mesh, SphericalMesh): |
| 861 | + grp.create_dataset('type', data=np.bytes_('spherical')) |
| 862 | + grp.create_dataset('r_grid', data=np.asarray(mesh.r_grid, dtype=float)) |
| 863 | + grp.create_dataset('theta_grid', data=np.asarray(mesh.theta_grid, dtype=float)) |
| 864 | + grp.create_dataset('phi_grid', data=np.asarray(mesh.phi_grid, dtype=float)) |
| 865 | + grp.create_dataset('origin', data=np.asarray(mesh.origin, dtype=float)) |
| 866 | + elif isinstance(mesh, UnstructuredMesh): |
| 867 | + raise NotImplementedError( |
| 868 | + "Exporting weight windows on an UnstructuredMesh is not yet " |
| 869 | + "implemented in Python; use openmc.lib.export_weight_windows()." |
| 870 | + ) |
| 871 | + else: |
| 872 | + raise TypeError(f"Unknown mesh type: {type(mesh).__name__}") |
| 873 | + |
| 874 | + |
822 | 875 | class WeightWindowsList(list): |
823 | 876 | """A list of WeightWindows objects. |
824 | 877 |
|
@@ -1061,29 +1114,66 @@ def from_wwinp(cls, path: PathLike) -> Self: |
1061 | 1114 | def export_to_hdf5(self, path: PathLike = 'weight_windows.h5', **init_kwargs): |
1062 | 1115 | """Write weight windows to an HDF5 file. |
1063 | 1116 |
|
| 1117 | + Writes the file directly via :mod:`h5py`, mirroring the layout |
| 1118 | + produced by :func:`openmc.lib.export_weight_windows`. The previous |
| 1119 | + XML round-trip raised :class:`MemoryError` on multi-GB bound arrays |
| 1120 | + because of the intermediate ASCII allocation inside lxml. |
| 1121 | +
|
1064 | 1122 | Parameters |
1065 | 1123 | ---------- |
1066 | 1124 | path : PathLike |
1067 | 1125 | Path to the file to write weight windows to |
1068 | 1126 | **init_kwargs |
1069 | | - Keyword arguments passed to :func:`openmc.lib.init` |
1070 | | -
|
| 1127 | + Unused. Retained for backward compatibility (previously forwarded |
| 1128 | + to :func:`openmc.lib.init`). |
1071 | 1129 | """ |
1072 | | - import openmc.lib |
1073 | 1130 | 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 |
1085 | 1131 | path = Path(path).resolve() |
1086 | 1132 |
|
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) |
| 1133 | + with h5py.File(path, 'w') as f: |
| 1134 | + f.attrs['filetype'] = np.bytes_('weight_windows') |
| 1135 | + f.attrs['version'] = np.asarray([1, 0], dtype=np.int32) |
| 1136 | + |
| 1137 | + meshes_grp = f.create_group('meshes') |
| 1138 | + wws_grp = f.create_group('weight_windows') |
| 1139 | + |
| 1140 | + seen_mesh_ids = set() |
| 1141 | + ww_ids = [] |
| 1142 | + for ww in self: |
| 1143 | + if ww.mesh.id not in seen_mesh_ids: |
| 1144 | + _write_mesh_group(meshes_grp, ww.mesh) |
| 1145 | + seen_mesh_ids.add(ww.mesh.id) |
| 1146 | + |
| 1147 | + g = wws_grp.create_group(f'weight_windows_{ww.id}') |
| 1148 | + ww_ids.append(int(ww.id)) |
| 1149 | + |
| 1150 | + g.create_dataset('mesh', data=np.int32(ww.mesh.id)) |
| 1151 | + g.create_dataset('particle_type', |
| 1152 | + data=np.bytes_(str(ww.particle_type))) |
| 1153 | + g.create_dataset('energy_bounds', |
| 1154 | + data=np.asarray(ww.energy_bounds, dtype=float)) |
| 1155 | + |
| 1156 | + # 2D (ne, n_voxels) C-contiguous: the C++ reader expects this layout. |
| 1157 | + ne = ww.lower_ww_bounds.shape[-1] |
| 1158 | + lo = np.ascontiguousarray(ww.lower_ww_bounds.T).reshape(ne, -1) |
| 1159 | + up = np.ascontiguousarray(ww.upper_ww_bounds.T).reshape(ne, -1) |
| 1160 | + g.create_dataset('lower_ww_bounds', data=lo) |
| 1161 | + g.create_dataset('upper_ww_bounds', data=up) |
| 1162 | + |
| 1163 | + g.create_dataset('survival_ratio', |
| 1164 | + data=float(ww.survival_ratio)) |
| 1165 | + # max_lower_bound_ratio is read unconditionally by C++; default 1.0 when unset. |
| 1166 | + mlbr = ww.max_lower_bound_ratio |
| 1167 | + g.create_dataset( |
| 1168 | + 'max_lower_bound_ratio', |
| 1169 | + data=float(mlbr if mlbr is not None else 1.0), |
| 1170 | + ) |
| 1171 | + g.create_dataset('max_split', data=np.int32(ww.max_split)) |
| 1172 | + g.create_dataset('weight_cutoff', |
| 1173 | + data=float(ww.weight_cutoff)) |
| 1174 | + |
| 1175 | + wws_grp.attrs['ids'] = np.asarray(ww_ids, dtype=np.int32) |
| 1176 | + wws_grp.attrs['n_weight_windows'] = np.int32(len(ww_ids)) |
| 1177 | + meshes_grp.attrs['ids'] = np.asarray(sorted(seen_mesh_ids), |
| 1178 | + dtype=np.int32) |
| 1179 | + meshes_grp.attrs['n_meshes'] = np.int32(len(seen_mesh_ids)) |
0 commit comments