Support rotation in MeshFilter (#3176)

Co-authored-by: Jonathan Shimwell <drshimwell@gmail.com>
Co-authored-by: Patrick Shriwise <pshriwise@gmail.com>
This commit is contained in:
Zoe Prieto 2025-12-20 01:56:13 -03:00 committed by GitHub
parent a230b86128
commit a2fd6cc57e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 594 additions and 2 deletions

View file

@ -51,6 +51,12 @@ public:
virtual bool translated() const { return translated_; }
virtual void set_rotation(const vector<double>& rotation);
virtual const vector<double>& rotation() const { return rotation_; }
virtual bool rotated() const { return rotated_; }
protected:
//----------------------------------------------------------------------------
// Data members
@ -58,6 +64,8 @@ protected:
int32_t mesh_; //!< Index of the mesh
bool translated_ {false}; //!< Whether or not the filter is translated
Position translation_ {0.0, 0.0, 0.0}; //!< Filter translation
bool rotated_ {false}; //!< Whether or not the filter is rotated
vector<double> rotation_; //!< Filter rotation
};
} // namespace openmc

View file

@ -833,6 +833,25 @@ class MeshFilter(Filter):
translation : Iterable of float
This array specifies a vector that is used to translate (shift) the mesh
for this filter
rotation : Iterable of float
This array specifies the angles in degrees about the x, y, and z axes
that the mesh should be rotated. The rotation applied is an intrinsic
rotation with specified Tait-Bryan angles. That is to say, if the angles
are :math:`(\phi, \theta, \psi)`, then the rotation matrix applied is
:math:`R_z(\psi) R_y(\theta) R_x(\phi)` or
.. math::
\left [ \begin{array}{ccc} \cos\theta \cos\psi & -\cos\phi \sin\psi
+ \sin\phi \sin\theta \cos\psi & \sin\phi \sin\psi + \cos\phi
\sin\theta \cos\psi \\ \cos\theta \sin\psi & \cos\phi \cos\psi +
\sin\phi \sin\theta \sin\psi & -\sin\phi \cos\psi + \cos\phi
\sin\theta \sin\psi \\ -\sin\theta & \sin\phi \cos\theta & \cos\phi
\cos\theta \end{array} \right ]
A rotation matrix can also be specified directly by setting this
attribute to a nested list (or 2D numpy array) that specifies each
element of the matrix.
bins : list of tuple
A list of mesh indices for each filter bin, e.g. [(1, 1, 1), (2, 1, 1),
...]
@ -845,6 +864,7 @@ class MeshFilter(Filter):
self.mesh = mesh
self.id = filter_id
self._translation = None
self._rotation = None
def __hash__(self):
string = type(self).__name__ + '\n'
@ -856,6 +876,7 @@ class MeshFilter(Filter):
string += '{: <16}=\t{}\n'.format('\tMesh ID', self.mesh.id)
string += '{: <16}=\t{}\n'.format('\tID', self.id)
string += '{: <16}=\t{}\n'.format('\tTranslation', self.translation)
string += '{: <16}=\t{}\n'.format('\tRotation', self.rotation)
return string
@classmethod
@ -879,6 +900,10 @@ class MeshFilter(Filter):
if translation:
out.translation = translation[()]
rotation = group.get('rotation')
if rotation:
out.rotation = rotation[()]
return out
@property
@ -911,6 +936,15 @@ class MeshFilter(Filter):
cv.check_length('mesh filter translation', t, 3)
self._translation = np.asarray(t)
@property
def rotation(self):
return self._rotation
@rotation.setter
def rotation(self, rotation):
cv.check_length('mesh filter rotation', rotation, 3)
self._rotation = np.asarray(rotation)
def can_merge(self, other):
# Mesh filters cannot have more than one bin
return False
@ -996,6 +1030,8 @@ class MeshFilter(Filter):
subelement.text = str(self.mesh.id)
if self.translation is not None:
element.set('translation', ' '.join(map(str, self.translation)))
if self.rotation is not None:
element.set('rotation', ' '.join(map(str, self.rotation.ravel())))
return element
@classmethod
@ -1008,6 +1044,13 @@ class MeshFilter(Filter):
translation = get_elem_list(elem, "translation", float) or []
if translation:
out.translation = translation
rotation = get_elem_list(elem, 'rotation', float) or []
if rotation:
if len(rotation) == 3:
out.rotation = rotation
elif len(rotation) == 9:
out.rotation = np.array(rotation).reshape(3, 3)
return out

View file

@ -97,6 +97,14 @@ _dll.openmc_mesh_filter_get_translation.errcheck = _error_handler
_dll.openmc_mesh_filter_set_translation.argtypes = [c_int32, POINTER(c_double*3)]
_dll.openmc_mesh_filter_set_translation.restype = c_int
_dll.openmc_mesh_filter_set_translation.errcheck = _error_handler
_dll.openmc_mesh_filter_get_rotation.argtypes = [c_int32, POINTER(c_double),
POINTER(c_size_t)]
_dll.openmc_mesh_filter_get_rotation.restype = c_int
_dll.openmc_mesh_filter_get_rotation.errcheck = _error_handler
_dll.openmc_mesh_filter_set_rotation.argtypes = [
c_int32, POINTER(c_double), c_size_t]
_dll.openmc_mesh_filter_set_rotation.restype = c_int
_dll.openmc_mesh_filter_set_rotation.errcheck = _error_handler
_dll.openmc_meshborn_filter_get_mesh.argtypes = [c_int32, POINTER(c_int32)]
_dll.openmc_meshborn_filter_get_mesh.restype = c_int
_dll.openmc_meshborn_filter_get_mesh.errcheck = _error_handler
@ -393,6 +401,10 @@ class MeshFilter(Filter):
Mesh used for the filter
translation : Iterable of float
3-D coordinates of the translation vector
rotation : Iterable of float
The rotation matrix or angles of the filter mesh. This can either be
a fully specified 3 x 3 rotation matrix or an Iterable of length 3
with the angles in degrees about the x, y, and z axes, respectively.
"""
filter_type = 'mesh'
@ -422,6 +434,34 @@ class MeshFilter(Filter):
def translation(self, translation):
_dll.openmc_mesh_filter_set_translation(self._index, (c_double*3)(*translation))
@property
def rotation(self):
rotation_data = np.zeros(12)
rot_size = c_size_t()
_dll.openmc_mesh_filter_get_rotation(
self._index, rotation_data.ctypes.data_as(POINTER(c_double)),
rot_size)
rot_size = rot_size.value
if rot_size == 9:
return rotation_data[:rot_size].shape(3, 3)
elif rot_size in (0, 12):
# If size is 0, rotation_data[9:] will be zeros. This indicates no
# rotation and is the most straightforward way to always return
# an iterable of floats
return rotation_data[9:]
else:
raise ValueError(
f'Invalid size of rotation matrix: {rot_size}')
@rotation.setter
def rotation(self, rotation_data):
flat_rotation = np.asarray(rotation_data, dtype=float).flatten()
_dll.openmc_mesh_filter_set_rotation(
self._index, flat_rotation.ctypes.data_as(POINTER(c_double)),
c_size_t(len(flat_rotation)))
class MeshBornFilter(Filter):
"""MeshBorn filter stored internally.

View file

@ -57,7 +57,7 @@ void Cell::set_rotation(const vector<double>& rot)
fatal_error(fmt::format("Non-3D rotation vector applied to cell {}", id_));
}
// Compute and store the rotation matrix.
// Compute and store the inverse rotation matrix for the angles given.
rotation_.clear();
rotation_.reserve(rot.size() == 9 ? 9 : 12);
if (rot.size() == 3) {

View file

@ -6,6 +6,7 @@
#include "openmc/constants.h"
#include "openmc/error.h"
#include "openmc/mesh.h"
#include "openmc/position.h"
#include "openmc/xml_interface.h"
namespace openmc {
@ -30,6 +31,10 @@ void MeshFilter::from_xml(pugi::xml_node node)
if (check_for_node(node, "translation")) {
set_translation(get_node_array<double>(node, "translation"));
}
// Read the rotation transform.
if (check_for_node(node, "rotation")) {
set_rotation(get_node_array<double>(node, "rotation"));
}
}
void MeshFilter::get_all_bins(
@ -45,6 +50,12 @@ void MeshFilter::get_all_bins(
last_r -= translation();
r -= translation();
}
// apply rotation if present
if (!rotation_.empty()) {
last_r = last_r.rotate(rotation_);
r = r.rotate(rotation_);
u = u.rotate(rotation_);
}
if (estimator != TallyEstimator::TRACKLENGTH) {
auto bin = model::meshes[mesh_]->get_bin(r);
@ -65,6 +76,9 @@ void MeshFilter::to_statepoint(hid_t filter_group) const
if (translated_) {
write_dataset(filter_group, "translation", translation_);
}
if (rotated_) {
write_dataset(filter_group, "rotation", rotation_);
}
}
std::string MeshFilter::text_label(int bin) const
@ -93,6 +107,40 @@ void MeshFilter::set_translation(const double translation[3])
this->set_translation({translation[0], translation[1], translation[2]});
}
void MeshFilter::set_rotation(const vector<double>& rot)
{
rotated_ = true;
// Compute and store the inverse rotation matrix for the angles given.
rotation_.clear();
rotation_.reserve(rot.size() == 9 ? 9 : 12);
if (rot.size() == 3) {
double phi = -rot[0] * PI / 180.0;
double theta = -rot[1] * PI / 180.0;
double psi = -rot[2] * PI / 180.0;
rotation_.push_back(std::cos(theta) * std::cos(psi));
rotation_.push_back(-std::cos(phi) * std::sin(psi) +
std::sin(phi) * std::sin(theta) * std::cos(psi));
rotation_.push_back(std::sin(phi) * std::sin(psi) +
std::cos(phi) * std::sin(theta) * std::cos(psi));
rotation_.push_back(std::cos(theta) * std::sin(psi));
rotation_.push_back(std::cos(phi) * std::cos(psi) +
std::sin(phi) * std::sin(theta) * std::sin(psi));
rotation_.push_back(-std::sin(phi) * std::cos(psi) +
std::cos(phi) * std::sin(theta) * std::sin(psi));
rotation_.push_back(-std::sin(theta));
rotation_.push_back(std::sin(phi) * std::cos(theta));
rotation_.push_back(std::cos(phi) * std::cos(theta));
// When user specifies angles, write them at end of vector
rotation_.push_back(rot[0]);
rotation_.push_back(rot[1]);
rotation_.push_back(rot[2]);
} else {
std::copy(rot.begin(), rot.end(), std::back_inserter(rotation_));
}
}
//==============================================================================
// C-API functions
//==============================================================================
@ -201,4 +249,48 @@ extern "C" int openmc_mesh_filter_set_translation(
return 0;
}
//! Return the rotation matrix of a mesh filter
extern "C" int openmc_mesh_filter_get_rotation(
int32_t index, double rot[], size_t* n)
{
// Make sure this is a valid index to an allocated filter
if (int err = verify_filter(index))
return err;
// Check the filter type
const auto& filter = model::tally_filters[index];
if (filter->type() != FilterType::MESH) {
set_errmsg("Tried to get a rotation from a non-mesh filter.");
return OPENMC_E_INVALID_TYPE;
}
// Get rotation from the mesh filter and set value
auto mesh_filter = dynamic_cast<MeshFilter*>(filter.get());
*n = mesh_filter->rotation().size();
std::memcpy(rot, mesh_filter->rotation().data(),
*n * sizeof(mesh_filter->rotation()[0]));
return 0;
}
//! Set the flattened rotation matrix of a mesh filter
extern "C" int openmc_mesh_filter_set_rotation(
int32_t index, const double rot[], size_t rot_len)
{
// Make sure this is a valid index to an allocated filter
if (int err = verify_filter(index))
return err;
const auto& filter = model::tally_filters[index];
// Check the filter type
if (filter->type() != FilterType::MESH) {
set_errmsg("Tried to set a rotation from a non-mesh filter.");
return OPENMC_E_INVALID_TYPE;
}
// Get a pointer to the filter and downcast
auto mesh_filter = dynamic_cast<MeshFilter*>(filter.get());
std::vector<double> vec_rot(rot, rot + rot_len);
mesh_filter->set_rotation(vec_rot);
return 0;
}
} // namespace openmc

View file

@ -933,7 +933,8 @@ void WeightWindowsGenerator::create_tally()
for (const auto& f : model::tally_filters) {
if (f->type() == FilterType::MESH) {
const auto* mesh_filter = dynamic_cast<MeshFilter*>(f.get());
if (mesh_filter->mesh() == mesh_idx && !mesh_filter->translated()) {
if (mesh_filter->mesh() == mesh_idx && !mesh_filter->translated() &&
!mesh_filter->rotated()) {
ww_tally->add_filter(f.get());
found_mesh_filter = true;
break;

View file

@ -0,0 +1,59 @@
<?xml version='1.0' encoding='utf-8'?>
<model>
<materials>
<material id="1" depletable="true">
<density value="10.0" units="g/cm3"/>
<nuclide name="U235" ao="1.0"/>
</material>
<material id="2">
<density value="1.0" units="g/cm3"/>
<nuclide name="Zr90" ao="1.0"/>
</material>
</materials>
<geometry>
<cell id="1" material="1" region="1 -2 3 -4 10 -9" universe="1"/>
<cell id="2" material="2" region="(-1 | 2 | -3 | 4) (5 -6 7 -8) 10 -9" universe="1"/>
<surface id="1" name="minimum x" type="x-plane" coeffs="-5.0"/>
<surface id="2" name="maximum x" type="x-plane" coeffs="5.0"/>
<surface id="3" name="minimum y" type="y-plane" coeffs="-5.0"/>
<surface id="4" name="maximum y" type="y-plane" coeffs="5.0"/>
<surface id="5" name="minimum x" type="x-plane" boundary="reflective" coeffs="-10.0"/>
<surface id="6" name="maximum x" type="x-plane" boundary="reflective" coeffs="10.0"/>
<surface id="7" name="minimum y" type="y-plane" boundary="reflective" coeffs="-10.0"/>
<surface id="8" name="maximum y" type="y-plane" boundary="reflective" coeffs="10.0"/>
<surface id="9" type="z-plane" boundary="vacuum" coeffs="10.0"/>
<surface id="10" type="z-plane" boundary="vacuum" coeffs="-10.0"/>
</geometry>
<settings>
<run_mode>eigenvalue</run_mode>
<particles>1000</particles>
<batches>5</batches>
<inactive>0</inactive>
</settings>
<tallies>
<mesh id="1">
<dimension>3 4 5</dimension>
<lower_left>-9 -9 -9</lower_left>
<upper_right>9 9 9</upper_right>
</mesh>
<mesh id="2">
<dimension>3 4 5</dimension>
<lower_left>-9 -9 -9</lower_left>
<upper_right>9 9 9</upper_right>
</mesh>
<filter id="1" type="mesh">
<bins>1</bins>
</filter>
<filter id="2" type="mesh" rotation="0 0 10">
<bins>2</bins>
</filter>
<tally id="1">
<filters>1</filters>
<scores>total</scores>
</tally>
<tally id="2">
<filters>2</filters>
<scores>total</scores>
</tally>
</tallies>
</model>

View file

@ -0,0 +1,244 @@
k-combined:
7.729082E-01 3.775399E-02
tally 1:
5.296804E-02
5.661701E-04
8.356446E-02
1.412139E-03
5.041335E-02
5.143568E-04
1.299348E-01
3.467618E-03
3.929702E-01
3.147038E-02
1.379707E-01
3.888484E-03
1.405034E-01
4.473799E-03
3.785796E-01
2.940585E-02
1.422010E-01
4.113723E-03
5.647073E-02
6.735251E-04
7.911154E-02
1.329137E-03
5.160755E-02
5.361448E-04
6.669424E-02
9.090832E-04
1.008621E-01
2.134534E-03
6.808932E-02
9.355993E-04
1.873006E-01
7.135961E-03
6.221575E-01
7.819842E-02
1.856653E-01
6.954762E-03
2.014929E-01
8.327845E-03
5.853251E-01
6.945708E-02
1.709645E-01
5.917124E-03
7.214913E-02
1.058962E-03
1.027720E-01
2.138475E-03
6.099853E-02
7.493941E-04
6.892071E-02
9.630680E-04
1.035459E-01
2.173883E-03
6.973870E-02
9.904237E-04
2.125703E-01
9.112659E-03
9.012205E-01
2.163546E-01
2.066426E-01
8.617414E-03
2.258950E-01
1.039607E-02
9.476792E-01
2.350708E-01
2.225585E-01
1.017898E-02
7.111503E-02
1.036847E-03
1.117012E-01
2.530040E-03
6.870474E-02
9.551035E-04
5.738897E-02
6.699030E-04
9.522335E-02
1.835769E-03
6.570917E-02
8.656870E-04
1.945592E-01
7.593336E-03
5.514753E-01
6.122981E-02
2.144202E-01
9.421739E-03
1.971631E-01
7.944046E-03
6.088996E-01
7.442954E-02
1.965447E-01
7.765628E-03
7.005494E-02
1.012891E-03
1.010633E-01
2.084095E-03
6.145926E-02
7.694351E-04
4.999479E-02
5.129164E-04
7.238243E-02
1.062921E-03
4.902309E-02
4.852193E-04
1.324655E-01
3.642431E-03
3.305312E-01
2.265726E-02
1.332993E-01
3.728385E-03
1.547469E-01
4.894837E-03
3.625944E-01
2.747313E-02
1.435761E-01
4.334405E-03
5.789603E-02
7.065383E-04
7.589559E-02
1.205386E-03
5.210018E-02
5.790843E-04
tally 2:
4.597932E-02
4.246849E-04
8.494738E-02
1.467238E-03
5.155072E-02
5.373129E-04
1.479395E-01
4.468164E-03
3.931843E-01
3.141943E-02
1.264081E-01
3.243689E-03
1.229742E-01
3.276501E-03
3.768427E-01
2.927971E-02
1.574908E-01
5.079304E-03
5.621044E-02
6.877866E-04
8.490616E-02
1.510673E-03
4.709600E-02
4.576632E-04
5.382674E-02
5.890600E-04
1.116560E-01
2.599817E-03
6.648634E-02
8.862677E-04
1.996861E-01
8.137198E-03
6.218616E-01
7.805532E-02
1.672379E-01
5.667797E-03
1.841275E-01
6.936896E-03
5.887874E-01
7.031665E-02
1.872574E-01
7.086584E-03
7.574698E-02
1.160992E-03
1.100173E-01
2.453972E-03
5.427866E-02
5.955556E-04
5.644318E-02
6.503634E-04
1.058628E-01
2.254338E-03
8.012794E-02
1.297032E-03
2.292757E-01
1.064490E-02
9.001874E-01
2.153097E-01
1.921874E-01
7.537343E-03
2.058642E-01
8.537136E-03
9.442653E-01
2.359241E-01
2.419900E-01
1.202201E-02
7.563876E-02
1.169874E-03
1.165428E-01
2.750405E-03
5.646354E-02
6.408683E-04
4.963879E-02
4.975915E-04
1.005478E-01
2.046400E-03
7.041191E-02
1.002590E-03
1.959123E-01
7.727902E-03
5.625596E-01
6.366248E-02
1.987313E-01
8.108832E-03
1.922194E-01
7.602752E-03
6.062527E-01
7.384124E-02
2.039506E-01
8.379010E-03
7.019905E-02
1.034239E-03
1.081383E-01
2.344040E-03
5.125142E-02
5.430397E-04
4.230495E-02
3.666730E-04
7.722275E-02
1.208856E-03
4.998937E-02
5.049355E-04
1.461632E-01
4.456332E-03
3.315459E-01
2.288314E-02
1.238273E-01
3.205844E-03
1.355317E-01
3.754923E-03
3.646520E-01
2.766067E-02
1.543013E-01
5.044487E-03
6.082173E-02
7.636985E-04
8.490569E-02
1.506309E-03
4.046126E-02
3.495832E-04

View file

@ -0,0 +1,72 @@
import numpy as np
import openmc
import pytest
from tests.testing_harness import PyAPITestHarness
@pytest.fixture
def model():
model = openmc.model.Model()
fuel = openmc.Material()
fuel.set_density('g/cm3', 10.0)
fuel.add_nuclide('U235', 1.0)
zr = openmc.Material()
zr.set_density('g/cm3', 1.0)
zr.add_nuclide('Zr90', 1.0)
model.materials.extend([fuel, zr])
box1 = openmc.model.RectangularPrism(10.0, 10.0)
box2 = openmc.model.RectangularPrism(20.0, 20.0, boundary_type='reflective')
top = openmc.ZPlane(z0=10.0, boundary_type='vacuum')
bottom = openmc.ZPlane(z0=-10.0, boundary_type='vacuum')
cell1 = openmc.Cell(fill=fuel, region=-box1 & +bottom & -top)
cell2 = openmc.Cell(fill=zr, region=+box1 & -box2 & +bottom & -top)
model.geometry = openmc.Geometry([cell1, cell2])
model.settings.batches = 5
model.settings.inactive = 0
model.settings.particles = 1000
rotation = np.array((0, 0, 10))
llc = np.array([-9, -9, -9])
urc = np.array([9, 9, 9])
mesh_dims = (3, 4, 5)
filters = []
# un-rotated meshes
reg_mesh = openmc.RegularMesh()
reg_mesh.dimension = mesh_dims
reg_mesh.lower_left = llc
reg_mesh.upper_right = urc
filters.append(openmc.MeshFilter(reg_mesh))
# rotated meshes
rotated_reg_mesh = openmc.RegularMesh()
rotated_reg_mesh.dimension = mesh_dims
rotated_reg_mesh.lower_left = llc
rotated_reg_mesh.upper_right = urc
filters.append(openmc.MeshFilter(rotated_reg_mesh))
filters[-1].rotation = rotation
# Create tallies
for f in filters:
tally = openmc.Tally()
tally.filters = [f]
tally.scores = ['total']
model.tallies.append(tally)
return model
def test_filter_mesh_rotations(model):
harness = PyAPITestHarness('statepoint.5.h5', model)
harness.main()

View file

@ -259,3 +259,29 @@ def test_get_reshaped_data(run_in_tmpdir):
assert data1.shape == (2, 19*3*2, 1, 1)
assert data2.shape == (2, 19, 3, 2, 1, 1)
def test_mesh_filter_rotation_roundtrip(run_in_tmpdir):
"""Test that MeshFilter rotation works as expected"""
mesh = openmc.RegularMesh()
mesh.lower_left = [-10, -10, -10]
mesh.upper_right = [10, 10, 10]
mesh.dimension = [2, 3, 4]
# check that rotatoin is round-tripped correctly for a set of angles
mesh_filter = openmc.MeshFilter(mesh)
mesh_filter.rotation = [0, 0, 90] # Rotate around z-axis by 90 degrees
elem = mesh_filter.to_xml_element()
mesh_filter_xml = openmc.MeshFilter.from_xml_element(elem, meshes={mesh.id: mesh})
assert all(mesh_filter_xml.rotation == mesh_filter.rotation)
# check that rotation matrix is round-tripped correctly for a rotation matrix
mesh_filter.rotation = np.array([[0.7071, 0, 0.7071],
[0, 1, 0],
[-0.7071, 0, 0.7071]])
elem = mesh_filter.to_xml_element()
mesh_filter_xml = openmc.MeshFilter.from_xml_element(elem, meshes={mesh.id: mesh})
assert np.allclose(mesh_filter_xml.rotation, mesh_filter.rotation)

View file

@ -608,6 +608,13 @@ def test_regular_mesh(lib_init):
assert isinstance(mesh, openmc.lib.RegularMesh)
assert mesh_id == mesh.id
rotation = (180.0, 0.0, 0.0)
mf = openmc.lib.MeshFilter(mesh)
assert mf.mesh == mesh
mf.rotation = rotation
assert np.allclose(mf.rotation, rotation)
translation = (1.0, 2.0, 3.0)
mf = openmc.lib.MeshFilter(mesh)