Skip to content

Commit fa0f126

Browse files
committed
Some reverting and small fix in filter.py
1 parent 75dcb51 commit fa0f126

3 files changed

Lines changed: 133 additions & 23 deletions

File tree

openmc/filter.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
)
3838

3939

40-
4140
class FilterMeta(ABCMeta):
4241
"""Metaclass for filters that ensures class names are appropriate."""
4342

@@ -1113,12 +1112,20 @@ def from_volumes(cls, mesh: openmc.MeshBase, volumes: openmc.MeshMaterialVolumes
11131112
A new MeshMaterialFilter instance
11141113
11151114
"""
1116-
# Get flat arrays of material IDs and element indices
1117-
mat_ids = volumes._materials[volumes._materials > -1]
1118-
elems, _ = np.where(volumes._materials > -1)
1119-
1120-
# Stack them into a 2D array of (element, material) pairs
1121-
bins = np.column_stack((elems, mat_ids))
1115+
# Build bins sorted by volume ascending (mat ID ascending as tiebreaker)
1116+
# so the bin ordering is deterministic regardless of the ray-traversal
1117+
# order stored in the raw _materials array.
1118+
bins = []
1119+
for i in range(volumes.num_elements):
1120+
entries = volumes.by_element(i)
1121+
# sort by (volume asc, mat_id asc), excluding void (None)
1122+
entries = sorted(
1123+
((mat_id, vol)
1124+
for mat_id, vol in entries if mat_id is not None),
1125+
key=lambda t: (t[1], t[0])
1126+
)
1127+
for mat_id, _ in entries:
1128+
bins.append((i, mat_id))
11221129
return cls(mesh, bins)
11231130

11241131
def __hash__(self):
@@ -1861,7 +1868,7 @@ def __init__(self, particles, energies=None, filter_id=None):
18611868
def __repr__(self):
18621869
string = type(self).__name__ + '\n'
18631870
string += '{: <16}=\t{}\n'.format('\tParticles',
1864-
[str(p) for p in self.particles])
1871+
[str(p) for p in self.particles])
18651872
if self.energies is not None:
18661873
string += '{: <16}=\t{}\n'.format('\tEnergies', self.energies)
18671874
string += '{: <16}=\t{}\n'.format('\tID', self.id)
@@ -2171,7 +2178,7 @@ def paths(self):
21712178
if self._paths is None:
21722179
if not hasattr(self, '_geometry'):
21732180
raise ValueError(
2174-
"Model must be exported before the 'paths' attribute is" \
2181+
"Model must be exported before the 'paths' attribute is"
21752182
"available for a DistribcellFilter.")
21762183

21772184
# Determine paths for cell instances

openmc/mesh.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,7 @@ def num_mesh_cells(self):
693693
def write_data_to_vtk(self,
694694
filename: PathLike,
695695
datasets: dict | None = None,
696-
volume_normalization: bool = False,
696+
volume_normalization: bool | None = None,
697697
curvilinear: bool = False):
698698
"""Creates a VTK object of the mesh
699699
@@ -709,9 +709,10 @@ def write_data_to_vtk(self,
709709
with structured indexing in "C" ordering. See the "expand_dims" flag
710710
of :meth:`~openmc.Tally.get_reshaped_data` on reshaping tally data when using
711711
:class:`~openmc.MeshFilter`'s.
712-
volume_normalization : bool, optional
712+
volume_normalization : bool or None, optional
713713
Whether or not to normalize the data by the volume of the mesh
714-
elements.
714+
elements. When None (default), the format-appropriate default is
715+
used: True for legacy ASCII .vtk files, False for .vtkhdf files.
715716
curvilinear : bool
716717
Whether or not to write curvilinear elements. Only applies to
717718
``SphericalMesh`` and ``CylindricalMesh``.
@@ -748,10 +749,14 @@ def write_data_to_vtk(self,
748749
if write_impl is None:
749750
raise NotImplementedError(
750751
f"VTKHDF output not implemented for {type(self).__name__}")
751-
# write_impl is a bound method – do NOT pass self again
752-
write_impl(filename, datasets, volume_normalization)
752+
# vtkhdf default is False; explicit value is respected
753+
norm = volume_normalization if volume_normalization is not None else False
754+
write_impl(filename, datasets, norm)
753755
return None
754756

757+
# legacy ASCII .vtk default is True
758+
norm = volume_normalization if volume_normalization is not None else True
759+
755760
# vtk is an optional dependency only needed for the legacy ASCII path
756761
import vtk
757762
from vtk.util import numpy_support as nps
@@ -769,20 +774,26 @@ def write_data_to_vtk(self,
769774
# maintain a list of the datasets as added to the VTK arrays to
770775
# ensure they persist in memory until the file is written
771776
datasets_out = []
777+
# Regular/Rectilinear meshes store data in C (ijk) order which
778+
# matches VTK's expected ordering directly — no transpose needed.
779+
# Curvilinear meshes (Cylindrical, Spherical) require a transpose
780+
# to convert from C ordering to the Fortran (kji) ordering that VTK
781+
# expects.
782+
# TODO: update to "C" ordering throughout
783+
needs_transpose = not isinstance(
784+
self, (RegularMesh, RectilinearMesh))
772785
for label, dataset in datasets.items():
773786
dataset = self._reshape_vtk_dataset(dataset)
774787
self._check_vtk_dataset(label, dataset)
775-
# If the array data is 3D, assume is in C ordering and transpose
776-
# before flattening to match the ordering expected by the VTK
777-
# array based on the way mesh indices are ordered in the Python
778-
# API
779-
# TODO: update to "C" ordering throughout
780788
if dataset.ndim == 3:
781-
dataset = dataset.ravel()
789+
dataset = dataset.T.ravel() if needs_transpose else dataset.ravel()
782790
datasets_out.append(dataset)
783791

784-
if volume_normalization:
785-
dataset /= self.volumes.ravel()
792+
if norm:
793+
if needs_transpose:
794+
dataset /= self.volumes.T.ravel()
795+
else:
796+
dataset /= self.volumes.ravel()
786797

787798
dataset_array = vtk.vtkDoubleArray()
788799
dataset_array.SetName(label)

tests/unit_tests/test_mesh.py

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from scipy.stats import chi2
88
import pytest
99
import openmc
10+
import random
11+
import itertools
1012
import openmc.lib
1113
from openmc.utility_funcs import change_directory
1214
from uncertainties.unumpy import uarray, nominal_values, std_devs
@@ -723,6 +725,48 @@ def test_mesh_material_volumes_serialize():
723725
assert new_volumes.by_element(2) == [(2, 0.5), (1, 0.5)]
724726
assert new_volumes.by_element(3) == [(2, 1.0)]
725727

728+
def test_mesh_material_volumes_serialize_with_bboxes():
729+
materials = np.array([
730+
[1, -1, -2],
731+
[-1, -2, -2],
732+
[2, 1, -2],
733+
[2, -2, -2]
734+
])
735+
volumes = np.array([
736+
[0.5, 0.5, 0.0],
737+
[1.0, 0.0, 0.0],
738+
[0.5, 0.5, 0.0],
739+
[1.0, 0.0, 0.0]
740+
])
741+
742+
# (xmin, ymin, zmin, xmax, ymax, zmax)
743+
bboxes = np.empty((4, 3, 6))
744+
bboxes[..., 0:3] = np.inf
745+
bboxes[..., 3:6] = -np.inf
746+
bboxes[0, 0] = [-1.0, -2.0, -3.0, 1.0, 2.0, 3.0] # material 1
747+
bboxes[0, 1] = [-5.0, -6.0, -7.0, 5.0, 6.0, 7.0] # void
748+
bboxes[1, 0] = [0.0, 0.0, 0.0, 10.0, 1.0, 2.0] # void
749+
bboxes[2, 0] = [-1.0, -1.0, -1.0, 0.0, 0.0, 0.0] # material 2
750+
bboxes[2, 1] = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0] # material 1
751+
bboxes[3, 0] = [-2.0, -2.0, -2.0, 2.0, 2.0, 2.0] # material 2
752+
753+
mmv = openmc.MeshMaterialVolumes(materials, volumes, bboxes)
754+
with TemporaryDirectory() as tmpdir:
755+
path = f'{tmpdir}/volumes_bboxes.npz'
756+
mmv.save(path)
757+
loaded = openmc.MeshMaterialVolumes.from_npz(path)
758+
759+
assert loaded.has_bounding_boxes
760+
first = loaded.by_element(0, include_bboxes=True)[0][2]
761+
assert isinstance(first, openmc.BoundingBox)
762+
np.testing.assert_array_equal(first.lower_left, (-1.0, -2.0, -3.0))
763+
np.testing.assert_array_equal(first.upper_right, (1.0, 2.0, 3.0))
764+
765+
second = loaded.by_element(0, include_bboxes=True)[1][2]
766+
assert isinstance(second, openmc.BoundingBox)
767+
np.testing.assert_array_equal(second.lower_left, (-5.0, -6.0, -7.0))
768+
np.testing.assert_array_equal(second.upper_right, (5.0, 6.0, 7.0))
769+
726770

727771
def test_mesh_material_volumes_boundary_conditions(sphere_model):
728772
"""Test the material volumes method using a regular mesh
@@ -751,6 +795,51 @@ def test_mesh_material_volumes_boundary_conditions(sphere_model):
751795
assert evaluated[0] == expected[0]
752796
assert evaluated[1] == pytest.approx(expected[1], rel=1e-2)
753797

798+
def test_mesh_material_volumes_bounding_boxes():
799+
# Create a model with 8 spherical cells at known locations with random radii
800+
box = openmc.model.RectangularParallelepiped(
801+
-10, 10, -10, 10, -10, 10, boundary_type='vacuum')
802+
803+
mat = openmc.Material()
804+
mat.add_nuclide('H1', 1.0)
805+
806+
sph_cells = []
807+
for x, y, z in itertools.product((-5., 5.), repeat=3):
808+
mat_i = mat.clone()
809+
sph = openmc.Sphere(x, y, z, r=random.uniform(0.5, 1.5))
810+
sph_cells.append(openmc.Cell(region=-sph, fill=mat_i))
811+
background = openmc.Cell(region=-box & openmc.Intersection([~c.region for c in sph_cells]))
812+
813+
model = openmc.Model()
814+
model.geometry = openmc.Geometry(sph_cells + [background])
815+
model.settings.particles = 1000
816+
model.settings.batches = 10
817+
818+
# Create a one-element mesh that encompasses the entire geometry
819+
mesh = openmc.RegularMesh()
820+
mesh.lower_left = (-10., -10., -10.)
821+
mesh.upper_right = (10., 10., 10.)
822+
mesh.dimension = (1, 1, 1)
823+
824+
# Run material volume calculation with bounding boxes
825+
n_samples = (400, 400, 400)
826+
mmv = mesh.material_volumes(model, n_samples, max_materials=10, bounding_boxes=True)
827+
assert mmv.has_bounding_boxes
828+
829+
# Create a mapping of material ID to bounding box
830+
bbox_by_mat = {
831+
mat_id: bbox
832+
for mat_id, vol, bbox in mmv.by_element(0, include_bboxes=True)
833+
if mat_id is not None and vol > 0.0
834+
}
835+
836+
# Match the mesh ray spacing used for the bounding box estimator.
837+
tol = 0.5 * mesh.bounding_box.width[0] / n_samples[0]
838+
for cell in sph_cells:
839+
bbox = bbox_by_mat[cell.fill.id]
840+
cell_bbox = cell.bounding_box
841+
np.testing.assert_allclose(bbox.lower_left, cell_bbox.lower_left, atol=tol)
842+
np.testing.assert_allclose(bbox.upper_right, cell_bbox.upper_right, atol=tol)
754843

755844
def test_raytrace_mesh_infinite_loop(run_in_tmpdir):
756845
# Create a model with one large spherical cell
@@ -1217,7 +1306,10 @@ def test_write_ascii_vtk_unchanged(run_in_tmpdir):
12171306

12181307
# This should work without requiring vtk module changes
12191308
vtkIOLegacy = pytest.importorskip("vtkmodules.vtkIOLegacy")
1220-
mesh.write_data_to_vtk(datasets={"data": ref_data}, filename=filename)
1309+
mesh.write_data_to_vtk(datasets={"data": ref_data},
1310+
filename=filename,
1311+
volume_normalization=False
1312+
)
12211313

12221314
assert Path(filename).exists()
12231315

0 commit comments

Comments
 (0)