Skip to content

Commit 83a30f6

Browse files
GuyStenpaulromano
andauthored
Support arbitrary symmetry axis for CylindricalIndependent class (#3474)
Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
1 parent 139907c commit 83a30f6

7 files changed

Lines changed: 260 additions & 28 deletions

File tree

docs/source/io_formats/settings.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,7 @@ attributes/sub-elements:
814814

815815
For a "cylindrical" distribution, no parameters are specified. Instead,
816816
the ``r``, ``phi``, ``z``, and ``origin`` elements must be specified.
817+
Optionally, the ``r_dir`` and ``z_dir`` elements could be specified.
817818

818819
For a "spherical" distribution, no parameters are specified. Instead,
819820
the ``r``, ``theta``, ``phi``, and ``origin`` elements must be specified.
@@ -845,6 +846,10 @@ attributes/sub-elements:
845846
of a univariate probability distribution (see the description in
846847
:ref:`univariate`).
847848

849+
:r_dir:
850+
For "cylindrical" distributions, this element specifies the direction
851+
of the cylinder r-axis at phi=0. Defaults to (1.0, 0.0, 0.0).
852+
848853
:theta:
849854
For a "spherical" distribution, this element specifies the distribution
850855
of theta-coordinates. The necessary sub-elements/attributes are those of a
@@ -857,6 +862,10 @@ attributes/sub-elements:
857862
sub-elements/attributes are those of a univariate probability
858863
distribution (see the description in :ref:`univariate`).
859864

865+
:z_dir:
866+
For "cylindrical" distributions, this element specifies the direction
867+
of the cylinder z-axis. Defaults to (0.0, 0.0, 1.0).
868+
860869
:origin:
861870
For "cylindrical and "spherical" distributions, this element specifies
862871
the coordinates for the origin of the coordinate system.

docs/source/pythonapi/stats.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,4 @@ Spatial Distributions
6767
:template: myfunction.rst
6868

6969
openmc.stats.spherical_uniform
70+
openmc.stats.cylindrical_uniform

include/openmc/distribution_spatial.h

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,18 @@ class CylindricalIndependent : public SpatialDistribution {
6767
Distribution* phi() const { return phi_.get(); }
6868
Distribution* z() const { return z_.get(); }
6969
Position origin() const { return origin_; }
70+
Direction r_dir() const { return r_dir_; }
71+
Direction phi_dir() const { return phi_dir_; }
72+
Direction z_dir() const { return z_dir_; }
7073

7174
private:
72-
UPtrDist r_; //!< Distribution of r coordinates
73-
UPtrDist phi_; //!< Distribution of phi coordinates
74-
UPtrDist z_; //!< Distribution of z coordinates
75-
Position origin_; //!< Cartesian coordinates of the cylinder center
75+
UPtrDist r_; //!< Distribution of r coordinates
76+
UPtrDist phi_; //!< Distribution of phi coordinates
77+
UPtrDist z_; //!< Distribution of z coordinates
78+
Position origin_; //!< Cartesian coordinates of the cylinder center
79+
Direction r_dir_; //!< Direction of r-axis at phi=0
80+
Direction phi_dir_; //!< Direction of phi-axis at phi=0
81+
Direction z_dir_; //!< Direction of z-axis
7682
};
7783

7884
//==============================================================================

openmc/stats/multivariate.py

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import openmc.checkvalue as cv
1313
from .._xml import get_elem_list, get_text
1414
from ..mesh import MeshBase
15-
from .univariate import PowerLaw, Uniform, Univariate
15+
from .univariate import PowerLaw, Uniform, Univariate, delta_function
1616

1717

1818
class UnitSphere(ABC):
@@ -610,6 +610,10 @@ class CylindricalIndependent(Spatial):
610610
origin: Iterable of float, optional
611611
coordinates (x0, y0, z0) of the center of the cylindrical reference
612612
frame. Defaults to (0.0, 0.0, 0.0)
613+
r_dir : Iterable of float, optional
614+
Unit vector of the cylinder r axis at phi=0.
615+
z_dir : Iterable of float, optional
616+
Unit vector of the cylinder z axis direction.
613617
614618
Attributes
615619
----------
@@ -623,14 +627,21 @@ class CylindricalIndependent(Spatial):
623627
origin: Iterable of float, optional
624628
coordinates (x0, y0, z0) of the center of the cylindrical reference
625629
frame. Defaults to (0.0, 0.0, 0.0)
630+
r_dir : Iterable of float, optional
631+
Unit vector of the cylinder r axis at phi=0.
632+
z_dir : Iterable of float, optional
633+
Unit vector of the cylinder z axis direction.
626634
627635
"""
628636

629-
def __init__(self, r, phi, z, origin=(0.0, 0.0, 0.0)):
637+
def __init__(self, r, phi, z, origin=(0.0, 0.0, 0.0), r_dir=(1.0, 0.0, 0.0),
638+
z_dir=(0.0, 0.0, 1.0)):
630639
self.r = r
631640
self.phi = phi
632641
self.z = z
633642
self.origin = origin
643+
self.z_dir = z_dir
644+
self.r_dir = r_dir
634645

635646
@property
636647
def r(self):
@@ -669,6 +680,33 @@ def origin(self, origin):
669680
origin = np.asarray(origin)
670681
self._origin = origin
671682

683+
@property
684+
def z_dir(self):
685+
return self._z_dir
686+
687+
@z_dir.setter
688+
def z_dir(self, z_dir):
689+
cv.check_type('z-axis direction', z_dir, Iterable, Real)
690+
z_dir = np.array(z_dir)
691+
norm = np.linalg.norm(z_dir)
692+
cv.check_greater_than('z-axis direction magnitude', norm, 0.0)
693+
z_dir /= norm
694+
self._z_dir = z_dir
695+
696+
@property
697+
def r_dir(self):
698+
return self._r_dir
699+
700+
@r_dir.setter
701+
def r_dir(self, r_dir):
702+
cv.check_type('r-axis direction', r_dir, Iterable, Real)
703+
r_dir = np.array(r_dir)
704+
r_dir -= np.dot(r_dir, self.z_dir) * self.z_dir
705+
norm = np.linalg.norm(r_dir)
706+
cv.check_greater_than('r-axis direction magnitude', norm, 0.0)
707+
r_dir /= norm
708+
self._r_dir = r_dir
709+
672710
def to_xml_element(self):
673711
"""Return XML representation of the spatial distribution
674712
@@ -683,7 +721,12 @@ def to_xml_element(self):
683721
element.append(self.r.to_xml_element('r'))
684722
element.append(self.phi.to_xml_element('phi'))
685723
element.append(self.z.to_xml_element('z'))
686-
element.set("origin", ' '.join(map(str, self.origin)))
724+
if not np.allclose(self.origin, [0., 0., 0.]):
725+
element.set("origin", ' '.join(map(str, self.origin)))
726+
if not np.allclose(self.r_dir, [1., 0., 0.]):
727+
element.set("r_dir", ' '.join(map(str, self.r_dir)))
728+
if not np.allclose(self.z_dir, [0., 0., 1.]):
729+
element.set("z_dir", ' '.join(map(str, self.z_dir)))
687730
return element
688731

689732
@classmethod
@@ -704,8 +747,10 @@ def from_xml_element(cls, elem: ET.Element):
704747
r = Univariate.from_xml_element(elem.find('r'))
705748
phi = Univariate.from_xml_element(elem.find('phi'))
706749
z = Univariate.from_xml_element(elem.find('z'))
707-
origin = get_elem_list(elem, "origin", float)
708-
return cls(r, phi, z, origin=origin)
750+
origin = get_elem_list(elem, "origin", float) or [0.0, 0.0, 0.0]
751+
r_dir = get_elem_list(elem, "r_dir", float) or [1.0, 0.0, 0.0]
752+
z_dir = get_elem_list(elem, "z_dir", float) or [0.0, 0.0, 1.0]
753+
return cls(r, phi, z, origin=origin, r_dir=r_dir, z_dir=z_dir)
709754

710755

711756
class MeshSpatial(Spatial):
@@ -1219,3 +1264,49 @@ def spherical_uniform(
12191264
phis_dist = Uniform(phis[0], phis[1])
12201265

12211266
return SphericalIndependent(r_dist, cos_thetas_dist, phis_dist, origin)
1267+
1268+
1269+
def cylindrical_uniform(
1270+
r_outer: float,
1271+
height: float,
1272+
r_inner: float = 0.0,
1273+
phis: Sequence[float] = (0., 2*pi),
1274+
**kwargs,
1275+
):
1276+
"""Return a uniform spatial distribution over a cylindrical shell.
1277+
1278+
This function provides a uniform spatial distribution over a cylindrical
1279+
shell between `r_inner` and `r_outer`. When `height` is zero, a delta
1280+
function is used for the z-distribution, giving a uniform distribution over
1281+
a flat ring (annulus) at z=0 in the local coordinate frame. Optionally, the
1282+
range of angles can be restricted by the `phis` argument.
1283+
1284+
.. versionadded:: 0.15.4
1285+
1286+
Parameters
1287+
----------
1288+
r_outer : float
1289+
Outer radius of the cylindrical shell in [cm]
1290+
height : float
1291+
Height of the cylindrical shell in [cm]. When 0, the distribution is a
1292+
flat ring at z=0 in the local frame.
1293+
r_inner : float
1294+
Inner radius of the cylindrical shell in [cm]
1295+
phis : iterable of float
1296+
Starting and ending phi coordinates (azimuthal angle) in radians in a
1297+
reference frame centered at `origin`.
1298+
**kwargs
1299+
Keyword arguments passed directly to
1300+
:class:`~openmc.stats.CylindricalIndependent` (e.g., ``origin``,
1301+
``r_dir``, ``z_dir``).
1302+
1303+
Returns
1304+
-------
1305+
openmc.stats.CylindricalIndependent
1306+
Uniform distribution over the cylindrical shell
1307+
"""
1308+
1309+
r_dist = PowerLaw(r_inner, r_outer, 1)
1310+
phis_dist = Uniform(phis[0], phis[1])
1311+
z_dist = delta_function(0.0) if height == 0.0 else Uniform(-height/2, height/2)
1312+
return CylindricalIndependent(r_dist, phis_dist, z_dist, **kwargs)

src/distribution_spatial.cpp

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,17 +141,50 @@ CylindricalIndependent::CylindricalIndependent(pugi::xml_node node)
141141
// If no coordinates were specified, default to (0, 0, 0)
142142
origin_ = {0.0, 0.0, 0.0};
143143
}
144+
145+
// Read cylinder z_dir
146+
if (check_for_node(node, "z_dir")) {
147+
auto z_dir = get_node_array<double>(node, "z_dir");
148+
if (z_dir.size() == 3) {
149+
z_dir_ = z_dir;
150+
z_dir_ /= z_dir_.norm();
151+
} else {
152+
fatal_error("z_dir for cylindrical source distribution must be length 3");
153+
}
154+
} else {
155+
// If no z_dir was specified, default to (0, 0, 1)
156+
z_dir_ = {0.0, 0.0, 1.0};
157+
}
158+
159+
// Read cylinder r_dir
160+
if (check_for_node(node, "r_dir")) {
161+
auto r_dir = get_node_array<double>(node, "r_dir");
162+
if (r_dir.size() == 3) {
163+
r_dir_ = r_dir;
164+
r_dir_ /= r_dir_.norm();
165+
} else {
166+
fatal_error("r_dir for cylindrical source distribution must be length 3");
167+
}
168+
} else {
169+
// If no r_dir was specified, default to (1, 0, 0)
170+
r_dir_ = {1.0, 0.0, 0.0};
171+
}
172+
173+
if (r_dir_.dot(z_dir_) > 1e-12)
174+
fatal_error("r_dir must be perpendicular to z_dir");
175+
176+
auto phi_dir = z_dir_.cross(r_dir_);
177+
phi_dir /= phi_dir.norm();
178+
phi_dir_ = phi_dir;
144179
}
145180

146181
std::pair<Position, double> CylindricalIndependent::sample(uint64_t* seed) const
147182
{
148183
auto [r, r_wgt] = r_->sample(seed);
149184
auto [phi, phi_wgt] = phi_->sample(seed);
150185
auto [z, z_wgt] = z_->sample(seed);
151-
double x = r * cos(phi) + origin_.x;
152-
double y = r * sin(phi) + origin_.y;
153-
z += origin_.z;
154-
Position xi {x, y, z};
186+
Position xi =
187+
r * (cos(phi) * r_dir_ + sin(phi) * phi_dir_) + z * z_dir_ + origin_;
155188
return {xi, r_wgt * phi_wgt * z_wgt};
156189
}
157190

tests/unit_tests/test_source.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,6 @@ def test_source():
3535
assert src.strength == 1.0
3636

3737

38-
def test_spherical_uniform():
39-
r_outer = 2.0
40-
r_inner = 1.0
41-
thetas = (0.0, pi/2)
42-
phis = (0.0, pi)
43-
origin = (0.0, 1.0, 2.0)
44-
45-
sph_indep_function = openmc.stats.spherical_uniform(r_outer,
46-
r_inner,
47-
thetas,
48-
phis,
49-
origin)
50-
51-
assert isinstance(sph_indep_function, openmc.stats.SphericalIndependent)
52-
5338
def test_point_cloud():
5439
positions = [(1, 0, 2), (0, 1, 0), (0, 0, 3), (4, 9, 2)]
5540
strengths = [1, 2, 3, 4]

0 commit comments

Comments
 (0)