Skip to content

Commit bc13485

Browse files
Generalize RotationalPeriodicBC for X-, Y-, or Z-axis (#3591)
Co-authored-by: GuySten <62616591+GuySten@users.noreply.github.com>
1 parent a9dc84f commit bc13485

12 files changed

Lines changed: 265 additions & 80 deletions

File tree

docs/source/io_formats/geometry.rst

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,9 @@ Each ``<surface>`` element can have the following attributes or sub-elements:
3838

3939
:boundary:
4040
The boundary condition for the surface. This can be "transmission",
41-
"vacuum", "reflective", or "periodic". Periodic boundary conditions can
42-
only be applied to x-, y-, and z-planes. Only axis-aligned periodicity is
43-
supported, i.e., x-planes can only be paired with x-planes. Specify which
44-
planes are periodic and the code will automatically identify which planes
45-
are paired together.
41+
"vacuum", "reflective", or "periodic". Specify which planes are
42+
periodic and the code will automatically identify which planes are
43+
paired together.
4644

4745
*Default*: "transmission"
4846

docs/source/usersguide/geometry.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ Otherwise it is necessary to specify pairs explicitly using the
192192
Both rotational and translational periodic boundary conditions are specified in
193193
the same fashion. If both planes have the same normal vector, a translational
194194
periodicity is assumed; rotational periodicity is assumed otherwise. Currently,
195-
only rotations about the :math:`z`-axis are supported.
195+
rotations must be about the :math:`x`-, :math:`y`-, or :math:`z`-axis.
196196

197197
For a rotational periodic BC, the normal vectors of each surface must point
198198
inwards---towards the valid geometry. For example, a :class:`XPlane` and

include/openmc/boundary_condition.h

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,18 +138,26 @@ class TranslationalPeriodicBC : public PeriodicBC {
138138
//==============================================================================
139139
//! A BC that rotates particles about a global axis.
140140
//
141-
//! Currently only rotations about the z-axis are supported.
141+
//! Only rotations about the x, y, and z axes are supported.
142142
//==============================================================================
143143

144144
class RotationalPeriodicBC : public PeriodicBC {
145145
public:
146-
RotationalPeriodicBC(int i_surf, int j_surf);
147-
146+
enum PeriodicAxis { x, y, z };
147+
RotationalPeriodicBC(int i_surf, int j_surf, PeriodicAxis axis);
148+
double compute_periodic_rotation(
149+
double rise_1, double run_1, double rise_2, double run_2) const;
148150
void handle_particle(Particle& p, const Surface& surf) const override;
149151

150152
protected:
151153
//! Angle about the axis by which particle coordinates will be rotated
152154
double angle_;
155+
//! Ensure that choice of axes is right handed. axis_1_idx_ corresponds to the
156+
//! independent axis and axis_2_idx_ corresponds to the dependent axis in the
157+
//! 2D plane perpendicular to the planes' axis of rotation
158+
int zero_axis_idx_;
159+
int axis_1_idx_;
160+
int axis_2_idx_;
153161
};
154162

155163
} // namespace openmc

openmc/surface.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,8 @@ class Surface(IDManagerMixin, ABC):
123123
boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional
124124
Boundary condition that defines the behavior for particles hitting the
125125
surface. Defaults to transmissive boundary condition where particles
126-
freely pass through the surface. Note that periodic boundary conditions
127-
can only be applied to x-, y-, and z-planes, and only axis-aligned
128-
periodicity is supported.
126+
freely pass through the surface. Note that only axis-aligned
127+
periodicity is supported around the x-, y-, and z-axes.
129128
albedo : float, optional
130129
Albedo of the surfaces as a ratio of particle weight after interaction
131130
with the surface to the initial weight. Values must be positive. Only
@@ -822,8 +821,7 @@ class XPlane(PlaneMixin, Surface):
822821
boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional
823822
Boundary condition that defines the behavior for particles hitting the
824823
surface. Defaults to transmissive boundary condition where particles
825-
freely pass through the surface. Only axis-aligned periodicity is
826-
supported, i.e., x-planes can only be paired with x-planes.
824+
freely pass through the surface.
827825
albedo : float, optional
828826
Albedo of the surfaces as a ratio of particle weight after interaction
829827
with the surface to the initial weight. Values must be positive. Only
@@ -887,8 +885,7 @@ class YPlane(PlaneMixin, Surface):
887885
boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional
888886
Boundary condition that defines the behavior for particles hitting the
889887
surface. Defaults to transmissive boundary condition where particles
890-
freely pass through the surface. Only axis-aligned periodicity is
891-
supported, i.e., y-planes can only be paired with y-planes.
888+
freely pass through the surface.
892889
albedo : float, optional
893890
Albedo of the surfaces as a ratio of particle weight after interaction
894891
with the surface to the initial weight. Values must be positive. Only
@@ -952,8 +949,7 @@ class ZPlane(PlaneMixin, Surface):
952949
boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional
953950
Boundary condition that defines the behavior for particles hitting the
954951
surface. Defaults to transmissive boundary condition where particles
955-
freely pass through the surface. Only axis-aligned periodicity is
956-
supported, i.e., z-planes can only be paired with z-planes.
952+
freely pass through the surface.
957953
albedo : float, optional
958954
Albedo of the surfaces as a ratio of particle weight after interaction
959955
with the surface to the initial weight. Values must be positive. Only

src/boundary_condition.cpp

Lines changed: 54 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -158,63 +158,44 @@ void TranslationalPeriodicBC::handle_particle(
158158
// RotationalPeriodicBC implementation
159159
//==============================================================================
160160

161-
RotationalPeriodicBC::RotationalPeriodicBC(int i_surf, int j_surf)
161+
RotationalPeriodicBC::RotationalPeriodicBC(
162+
int i_surf, int j_surf, PeriodicAxis axis)
162163
: PeriodicBC(i_surf, j_surf)
163164
{
164165
Surface& surf1 {*model::surfaces[i_surf_]};
165166
Surface& surf2 {*model::surfaces[j_surf_]};
166167

167-
// Check the type of the first surface
168-
bool surf1_is_xyplane;
169-
if (const auto* ptr = dynamic_cast<const SurfaceXPlane*>(&surf1)) {
170-
surf1_is_xyplane = true;
171-
} else if (const auto* ptr = dynamic_cast<const SurfaceYPlane*>(&surf1)) {
172-
surf1_is_xyplane = true;
173-
} else if (const auto* ptr = dynamic_cast<const SurfacePlane*>(&surf1)) {
174-
surf1_is_xyplane = false;
175-
} else {
176-
throw std::invalid_argument(fmt::format(
177-
"Surface {} is an invalid type for "
178-
"rotational periodic BCs. Only x-planes, y-planes, or general planes "
179-
"(that are perpendicular to z) are supported for these BCs.",
180-
surf1.id_));
181-
}
182-
183-
// Check the type of the second surface
184-
bool surf2_is_xyplane;
185-
if (const auto* ptr = dynamic_cast<const SurfaceXPlane*>(&surf2)) {
186-
surf2_is_xyplane = true;
187-
} else if (const auto* ptr = dynamic_cast<const SurfaceYPlane*>(&surf2)) {
188-
surf2_is_xyplane = true;
189-
} else if (const auto* ptr = dynamic_cast<const SurfacePlane*>(&surf2)) {
190-
surf2_is_xyplane = false;
191-
} else {
192-
throw std::invalid_argument(fmt::format(
193-
"Surface {} is an invalid type for "
194-
"rotational periodic BCs. Only x-planes, y-planes, or general planes "
195-
"(that are perpendicular to z) are supported for these BCs.",
196-
surf2.id_));
168+
// below convention for right handed coordinate system
169+
switch (axis) {
170+
case x:
171+
zero_axis_idx_ = 0; // x component of plane must be zero
172+
axis_1_idx_ = 1; // y component independent
173+
axis_2_idx_ = 2; // z component dependent
174+
break;
175+
case y:
176+
// for a right handed coordinate system, z should be the independent axis
177+
// but this would cause the y-rotation case to be different than the other
178+
// two. using a left handed coordinate system and a negative rotation the
179+
// compute angle and rotation matrix behavior mimics that of the x and z
180+
// cases
181+
zero_axis_idx_ = 1; // y component of plane must be zero
182+
axis_1_idx_ = 0; // x component independent
183+
axis_2_idx_ = 2; // z component dependent
184+
break;
185+
case z:
186+
zero_axis_idx_ = 2; // z component of plane must be zero
187+
axis_1_idx_ = 0; // x component independent
188+
axis_2_idx_ = 1; // y component dependent
189+
break;
190+
default:
191+
throw std::invalid_argument(
192+
fmt::format("You've specified an axis that is not x, y, or z."));
197193
}
198194

199195
// Compute the surface normal vectors and make sure they are perpendicular
200-
// to the z-axis
196+
// to the correct axis
201197
Direction norm1 = surf1.normal({0, 0, 0});
202198
Direction norm2 = surf2.normal({0, 0, 0});
203-
if (std::abs(norm1.z) > FP_PRECISION) {
204-
throw std::invalid_argument(fmt::format(
205-
"Rotational periodic BCs are only "
206-
"supported for rotations about the z-axis, but surface {} is not "
207-
"perpendicular to the z-axis.",
208-
surf1.id_));
209-
}
210-
if (std::abs(norm2.z) > FP_PRECISION) {
211-
throw std::invalid_argument(fmt::format(
212-
"Rotational periodic BCs are only "
213-
"supported for rotations about the z-axis, but surface {} is not "
214-
"perpendicular to the z-axis.",
215-
surf2.id_));
216-
}
217-
218199
// Make sure both surfaces intersect the origin
219200
if (std::abs(surf1.evaluate({0, 0, 0})) > FP_COINCIDENT) {
220201
throw std::invalid_argument(fmt::format(
@@ -231,15 +212,8 @@ RotationalPeriodicBC::RotationalPeriodicBC(int i_surf, int j_surf)
231212
surf2.id_));
232213
}
233214

234-
// Compute the BC rotation angle. Here it is assumed that both surface
235-
// normal vectors point inwards---towards the valid geometry region.
236-
// Consequently, the rotation angle is not the difference between the two
237-
// normals, but is instead the difference between one normal and one
238-
// anti-normal. (An incident ray on one surface must be an outgoing ray on
239-
// the other surface after rotation hence the anti-normal.)
240-
double theta1 = std::atan2(norm1.y, norm1.x);
241-
double theta2 = std::atan2(norm2.y, norm2.x) + PI;
242-
angle_ = theta2 - theta1;
215+
angle_ = compute_periodic_rotation(norm1[axis_2_idx_], norm1[axis_1_idx_],
216+
norm2[axis_2_idx_], norm2[axis_1_idx_]);
243217

244218
// Warn the user if the angle does not evenly divide a circle
245219
double rem = std::abs(std::remainder((2 * PI / angle_), 1.0));
@@ -251,6 +225,20 @@ RotationalPeriodicBC::RotationalPeriodicBC(int i_surf, int j_surf)
251225
}
252226
}
253227

228+
double RotationalPeriodicBC::compute_periodic_rotation(
229+
double rise_1, double run_1, double rise_2, double run_2) const
230+
{
231+
// Compute the BC rotation angle. Here it is assumed that both surface
232+
// normal vectors point inwards---towards the valid geometry region.
233+
// Consequently, the rotation angle is not the difference between the two
234+
// normals, but is instead the difference between one normal and one
235+
// anti-normal. (An incident ray on one surface must be an outgoing ray on
236+
// the other surface after rotation hence the anti-normal.)
237+
double theta1 = std::atan2(rise_1, run_1);
238+
double theta2 = std::atan2(rise_2, run_2) + PI;
239+
return theta2 - theta1;
240+
}
241+
254242
void RotationalPeriodicBC::handle_particle(
255243
Particle& p, const Surface& surf) const
256244
{
@@ -278,10 +266,16 @@ void RotationalPeriodicBC::handle_particle(
278266
Direction u = p.u();
279267
double cos_theta = std::cos(theta);
280268
double sin_theta = std::sin(theta);
281-
Position new_r = {
282-
cos_theta * r.x - sin_theta * r.y, sin_theta * r.x + cos_theta * r.y, r.z};
283-
Direction new_u = {
284-
cos_theta * u.x - sin_theta * u.y, sin_theta * u.x + cos_theta * u.y, u.z};
269+
270+
Position new_r;
271+
new_r[zero_axis_idx_] = r[zero_axis_idx_];
272+
new_r[axis_1_idx_] = cos_theta * r[axis_1_idx_] - sin_theta * r[axis_2_idx_];
273+
new_r[axis_2_idx_] = sin_theta * r[axis_1_idx_] + cos_theta * r[axis_2_idx_];
274+
275+
Direction new_u;
276+
new_u[zero_axis_idx_] = u[zero_axis_idx_];
277+
new_u[axis_1_idx_] = cos_theta * u[axis_1_idx_] - sin_theta * u[axis_2_idx_];
278+
new_u[axis_2_idx_] = sin_theta * u[axis_1_idx_] + cos_theta * u[axis_2_idx_];
285279

286280
// Handle the effects of the surface albedo on the particle's weight.
287281
BoundaryCondition::handle_albedo(p, surf);

src/surface.cpp

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1334,8 +1334,44 @@ void read_surfaces(pugi::xml_node node)
13341334
surf1.bc_ = make_unique<TranslationalPeriodicBC>(i_surf, j_surf);
13351335
surf2.bc_ = make_unique<TranslationalPeriodicBC>(i_surf, j_surf);
13361336
} else {
1337-
surf1.bc_ = make_unique<RotationalPeriodicBC>(i_surf, j_surf);
1338-
surf2.bc_ = make_unique<RotationalPeriodicBC>(i_surf, j_surf);
1337+
// check that both normals have at least one 0 component
1338+
if (std::abs(norm1.x) > FP_PRECISION &&
1339+
std::abs(norm1.y) > FP_PRECISION &&
1340+
std::abs(norm1.z) > FP_PRECISION) {
1341+
fatal_error(fmt::format(
1342+
"The normal ({}) of the periodic surface ({}) does not contain any "
1343+
"component with a zero value. A RotationalPeriodicBC requires one "
1344+
"component which is zero for both plane normals.",
1345+
norm1, i_surf));
1346+
}
1347+
if (std::abs(norm2.x) > FP_PRECISION &&
1348+
std::abs(norm2.y) > FP_PRECISION &&
1349+
std::abs(norm2.z) > FP_PRECISION) {
1350+
fatal_error(fmt::format(
1351+
"The normal ({}) of the periodic surface ({}) does not contain any "
1352+
"component with a zero value. A RotationalPeriodicBC requires one "
1353+
"component which is zero for both plane normals.",
1354+
norm2, j_surf));
1355+
}
1356+
// find common zero component, which indicates the periodic axis
1357+
RotationalPeriodicBC::PeriodicAxis axis;
1358+
if (std::abs(norm1.x) <= FP_PRECISION &&
1359+
std::abs(norm2.x) <= FP_PRECISION) {
1360+
axis = RotationalPeriodicBC::PeriodicAxis::x;
1361+
} else if (std::abs(norm1.y) <= FP_PRECISION &&
1362+
std::abs(norm2.y) <= FP_PRECISION) {
1363+
axis = RotationalPeriodicBC::PeriodicAxis::y;
1364+
} else if (std::abs(norm1.z) <= FP_PRECISION &&
1365+
std::abs(norm2.z) <= FP_PRECISION) {
1366+
axis = RotationalPeriodicBC::PeriodicAxis::z;
1367+
} else {
1368+
fatal_error(fmt::format(
1369+
"There is no component which is 0.0 in both normal vectors. This "
1370+
"indicates that the two planes are not periodic about the X, Y, or Z "
1371+
"axis, which is not supported."));
1372+
}
1373+
surf1.bc_ = make_unique<RotationalPeriodicBC>(i_surf, j_surf, axis);
1374+
surf2.bc_ = make_unique<RotationalPeriodicBC>(i_surf, j_surf, axis);
13391375
}
13401376

13411377
// If albedo data is present in albedo map, set the boundary albedo.

tests/regression_tests/periodic_cyls/__init__.py

Whitespace-only changes.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import openmc
2+
import numpy as np
3+
import pytest
4+
from openmc.utility_funcs import change_directory
5+
from tests.testing_harness import PyAPITestHarness
6+
7+
8+
@pytest.fixture
9+
def xcyl_model():
10+
model = openmc.Model()
11+
# Define materials
12+
fuel = openmc.Material()
13+
fuel.add_nuclide('U235', 0.2)
14+
fuel.add_nuclide('U238', 0.8)
15+
fuel.set_density('g/cc', 19.1)
16+
model.materials = openmc.Materials([fuel])
17+
18+
# Define geometry
19+
# finite cylinder
20+
x_min = openmc.XPlane(x0=0.0, boundary_type='reflective')
21+
x_max = openmc.XPlane(x0=20.0, boundary_type='reflective')
22+
x_cyl = openmc.XCylinder(r=20.0,boundary_type='vacuum')
23+
# slice cylinder for periodic BC
24+
periodic_bounding_yplane = openmc.YPlane(y0=0, boundary_type='periodic')
25+
periodic_bounding_plane = openmc.Plane(
26+
a=0.0, b=-np.sqrt(3) / 3, c=1, boundary_type='periodic',
27+
)
28+
sixth_cyl_cell = openmc.Cell(1, fill=fuel, region =
29+
+x_min &- x_max & -x_cyl & +periodic_bounding_yplane & +periodic_bounding_plane)
30+
periodic_bounding_yplane.periodic_surface = periodic_bounding_plane
31+
periodic_bounding_plane.periodic_surface = periodic_bounding_yplane
32+
33+
model.geometry = openmc.Geometry([sixth_cyl_cell])
34+
35+
36+
# Define settings
37+
model.settings.particles = 1000
38+
model.settings.batches = 4
39+
model.settings.inactive = 0
40+
model.settings.source = openmc.IndependentSource(space=openmc.stats.Box(
41+
(0, 0, 0), (20, 20, 20))
42+
)
43+
return model
44+
45+
@pytest.fixture
46+
def ycyl_model():
47+
model = openmc.Model()
48+
# Define materials
49+
fuel = openmc.Material()
50+
fuel.add_nuclide('U235', 0.2)
51+
fuel.add_nuclide('U238', 0.8)
52+
fuel.set_density('g/cc', 19.1)
53+
model.materials = openmc.Materials([fuel])
54+
55+
# Define geometry
56+
# finite cylinder
57+
y_min = openmc.YPlane(y0=0.0, boundary_type='reflective')
58+
y_max = openmc.YPlane(y0=20.0, boundary_type='reflective')
59+
y_cyl = openmc.YCylinder(r=20.0,boundary_type='vacuum')
60+
# slice cylinder for periodic BC
61+
periodic_bounding_xplane = openmc.XPlane(x0=0, boundary_type='periodic')
62+
periodic_bounding_plane = openmc.Plane(
63+
a=-np.sqrt(3) / 3, b=0.0, c=1, boundary_type='periodic',
64+
)
65+
sixth_cyl_cell = openmc.Cell(1, fill=fuel, region =
66+
+y_min &- y_max & -y_cyl & +periodic_bounding_xplane & +periodic_bounding_plane)
67+
periodic_bounding_xplane.periodic_surface = periodic_bounding_plane
68+
periodic_bounding_plane.periodic_surface = periodic_bounding_xplane
69+
model.geometry = openmc.Geometry([sixth_cyl_cell])
70+
71+
72+
# Define settings
73+
model.settings.particles = 1000
74+
model.settings.batches = 4
75+
model.settings.inactive = 0
76+
model.settings.source = openmc.IndependentSource(space=openmc.stats.Box(
77+
(0, 0, 0), (20, 20, 20))
78+
)
79+
return model
80+
81+
def test_xcyl(xcyl_model):
82+
with change_directory("xcyl_model"):
83+
openmc.reset_auto_ids()
84+
harness = PyAPITestHarness('statepoint.4.h5', xcyl_model)
85+
harness.main()
86+
87+
def test_ycyl(ycyl_model):
88+
with change_directory("ycyl_model"):
89+
openmc.reset_auto_ids()
90+
harness = PyAPITestHarness('statepoint.4.h5', ycyl_model)
91+
harness.main()

0 commit comments

Comments
 (0)