From c88bd495976aefed998680046221df4645224e36 Mon Sep 17 00:00:00 2001 From: Ethan Peterson Date: Thu, 27 Feb 2020 12:31:15 -0500 Subject: [PATCH] finished unit tests --- openmc/region.py | 186 ++++++++++--------------------- openmc/surface.py | 96 +++++++++++++--- tests/unit_tests/test_region.py | 22 ++++ tests/unit_tests/test_surface.py | 9 ++ 4 files changed, 174 insertions(+), 139 deletions(-) diff --git a/openmc/region.py b/openmc/region.py index 2b2fce14d0..22718ac7b2 100644 --- a/openmc/region.py +++ b/openmc/region.py @@ -225,7 +225,6 @@ class Region(metaclass=ABCMeta): # at the end return output[0] - @abstractmethod def clone(self, memo=None): """Create a copy of this region - each of the surfaces in the region's nodes will be cloned and will have new unique IDs. @@ -242,9 +241,14 @@ class Region(metaclass=ABCMeta): The clone of this region """ - pass - @abstractmethod + if memo is None: + memo = {} + + clone = deepcopy(self) + clone[:] = [n.clone(memo) for n in self] + return clone + def translate(self, vector, memo=None): """Translate region in given direction @@ -262,7 +266,52 @@ class Region(metaclass=ABCMeta): Translated region """ - pass + + if memo is None: + memo = {} + return type(self)(n.translate(vector, memo) for n in self) + + def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False, + memo=None): + r"""Rotate surface by angles provided or by applying matrix directly. + + Parameters + ---------- + rotation : 3-tuple of float, or 3x3 iterable + A 3-tuple of angles :math:`(\phi, \theta, \psi)` in degrees where + the first element is the rotation about the x-axis in the fixed + laboratory frame, the second element is the rotation about the + y-axis in the fixed laboratory frame, and the third element is the + rotation about the z-axis in the fixed laboratory frame. The + rotations are active rotations. Additionally a 3x3 rotation matrix + can be specified directly either as a nested iterable or array. + pivot : iterable of float, optional + (x, y, z) coordinates for the point to rotate about. Defaults to + (0., 0., 0.) + order : str, optional + A string of 'x', 'y', and 'z' in some order specifying which + rotation to perform first, second, and third. Defaults to 'xyz' + which means, the rotation by angle :math:`\phi` about x will be + applied first, followed by :math:`\theta` about y and then + :math:`\psi` about z. This corresponds to an x-y-z extrinsic + rotation as well as a z-y'-x'' intrinsic rotation using Tait-Bryan + angles :math:`(\phi, \theta, \psi)`. + inplace : boolean + Whether or not to return a new instance of Surface or to modify the + coefficients of this Surface in place. Defaults to False. + memo : dict or None + Dictionary used for memoization + + Returns + ------- + openmc.Region + Translated region + + """ + if memo is None: + memo = {} + return type(self)(n.rotate(rotation, pivot=pivot, order=order, + inplace=inplace, memo=memo) for n in self) class Intersection(Region, MutableSequence): @@ -354,51 +403,6 @@ class Intersection(Region, MutableSequence): upper_right[:] = np.minimum(upper_right, upper_right_n) return lower_left, upper_right - def clone(self, memo=None): - """Create a copy of this region - each of the surfaces in the - intersection's nodes will be cloned and will have new unique IDs. - - Parameters - ---------- - memo : dict or None - A nested dictionary of previously cloned objects. This parameter - is used internally and should not be specified by the user. - - Returns - ------- - clone : openmc.Intersection - The clone of this intersection - - """ - - if memo is None: - memo = {} - - clone = deepcopy(self) - clone[:] = [n.clone(memo) for n in self] - return clone - - def translate(self, vector, memo=None): - """Translate region in given direction - - Parameters - ---------- - vector : iterable of float - Direction in which region should be translated - memo : dict or None - Dictionary used for memoization. This parameter is used internally - and should not be specified by the user. - - Returns - ------- - openmc.Intersection - Translated region - - """ - if memo is None: - memo = {} - return type(self)(n.translate(vector, memo) for n in self) - class Union(Region, MutableSequence): r"""Union of two or more regions. @@ -487,51 +491,6 @@ class Union(Region, MutableSequence): upper_right[:] = np.maximum(upper_right, upper_right_n) return lower_left, upper_right - def clone(self, memo=None): - """Create a copy of this region - each of the surfaces in the - union's nodes will be cloned and will have new unique IDs. - - Parameters - ---------- - memo : dict or None - A nested dictionary of previously cloned objects. This parameter - is used internally and should not be specified by the user. - - Returns - ------- - clone : openmc.Union - The clone of this union - - """ - - if memo is None: - memo = {} - - clone = deepcopy(self) - clone[:] = [n.clone(memo) for n in self] - return clone - - def translate(self, vector, memo=None): - """Translate region in given direction - - Parameters - ---------- - vector : iterable of float - Direction in which region should be translated - memo : dict or None - Dictionary used for memoization. This parameter is used internally - and should not be specified by the user. - - Returns - ------- - openmc.Union - Translated region - - """ - if memo is None: - memo = {} - return type(self)(n.translate(vector, memo) for n in self) - class Complement(Region): """Complement of a region. @@ -642,22 +601,6 @@ class Complement(Region): region.remove_redundant_surfaces(redundant_surfaces) def clone(self, memo=None): - """Create a copy of this region - each of the surfaces in the - complement's node will be cloned and will have new unique IDs. - - Parameters - ---------- - memo : dict or None - A nested dictionary of previously cloned objects. This parameter - is used internally and should not be specified by the user. - - Returns - ------- - clone : openmc.Complement - The clone of this complement - - """ - if memo is None: memo = {} @@ -666,22 +609,13 @@ class Complement(Region): return clone def translate(self, vector, memo=None): - """Translate region in given direction - - Parameters - ---------- - vector : iterable of float - Direction in which region should be translated - memo : dict or None - Dictionary used for memoization. This parameter is used internally - and should not be specified by the user. - - Returns - ------- - openmc.Complement - Translated region - - """ if memo is None: memo = {} return type(self)(self.node.translate(vector, memo)) + + def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False, + memo=None): + if memo is None: + memo = {} + return type(self)(self.node.rotate(rotation, pivot=pivot, order=order, + inplace=inplace, memo=memo)) diff --git a/openmc/surface.py b/openmc/surface.py index eedbd13170..f0198e02bc 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -39,13 +39,13 @@ def get_rotation_matrix(rotation, order='xyz'): laboratory frame, and the third element is the rotation about the z-axis in the fixed laboratory frame. The rotations are active rotations. - order : str, optinoal + order : str, optional A string of 'x', 'y', and 'z' in some order specifying which rotation to perform first, second, and third. Defaults to 'xyz' which means, the - rotation by angle phi about x will be applied first, followed by theta - about y and then phi about z. This corresponds to an x-y-z extrinsic - rotation as well as a z-y'-x'' intrinsic rotation using Tait-Bryan - angles :math:`(\phi, \theta, \psi)`. + rotation by angle :math:`\phi` about x will be applied first, followed + by :math:`\theta` about y and then :math:`\psi` about z. This + corresponds to an x-y-z extrinsic rotation as well as a z-y'-x'' + intrinsic rotation using Tait-Bryan angles :math:`(\phi, \theta, \psi)`. """ check_type('surface rotation', rotation, Iterable, Real) @@ -307,22 +307,29 @@ class Surface(IDManagerMixin, metaclass=ABCMeta): @abstractmethod def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False): - r"""Rotate surface by given Tait-Bryan angles + r"""Rotate surface by angles provided or by applying matrix directly. Parameters ---------- - rotation : iterable of float - Intrinsic Tait-Bryan angles in degrees used to rotate the surface + rotation : 3-tuple of float, or 3x3 iterable + A 3-tuple of angles :math:`(\phi, \theta, \psi)` in degrees where + the first element is the rotation about the x-axis in the fixed + laboratory frame, the second element is the rotation about the + y-axis in the fixed laboratory frame, and the third element is the + rotation about the z-axis in the fixed laboratory frame. The + rotations are active rotations. Additionally a 3x3 rotation matrix + can be specified directly either as a nested iterable or array. pivot : iterable of float, optional (x, y, z) coordinates for the point to rotate about. Defaults to (0., 0., 0.) order : str, optional A string of 'x', 'y', and 'z' in some order specifying which rotation to perform first, second, and third. Defaults to 'xyz' - which means, the rotation by angle phi about x will be applied - first, followed by theta about y and then phi about z. This - corresponds to an x-y-z extrinsic rotation as well as a z-y'-x'' - intrinsic rotation using Tait-Bryan angles :math:`(\phi, \theta, \psi)`. + which means, the rotation by angle :math:`\phi` about x will be + applied first, followed by :math:`\theta` about y and then + :math:`\psi` about z. This corresponds to an x-y-z extrinsic + rotation as well as a z-y'-x'' intrinsic rotation using Tait-Bryan + angles :math:`(\phi, \theta, \psi)`. inplace : boolean Whether or not to return a new instance of Surface or to modify the coefficients of this Surface in place. Defaults to False. @@ -1291,6 +1298,19 @@ class Cylinder(QuadricMixin, Surface): check_type('dz coefficient', dz, Real) self._coefficients['dz'] = dz + def bounding_box(self, side): + if side == '-': + r = self.r + ll = [xi - r if np.isclose(dxi, 0., rtol=0., atol=self._atol) + else -np.inf for xi, dxi in zip(self._origin, self._axis)] + ur = [xi + r if np.isclose(dxi, 0., rtol=0., atol=self._atol) + else np.inf for xi, dxi in zip(self._origin, self._axis)] + return (np.array(ll), np.array(ur)) + + elif side == '+': + return (np.array([-np.inf, -np.inf, -np.inf]), + np.array([np.inf, np.inf, np.inf])) + def _get_base_coeffs(self): # Get x, y, z coordinates of two points x1, y1, z1 = self._origin @@ -2772,9 +2792,59 @@ class Halfspace(Region): if key not in memo: memo[key] = self.surface.translate(vector) - # Return translated surface + # Return translated half-space return type(self)(memo[key], self.side) + def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False, + memo=None): + r"""Rotate surface by angles provided or by applying matrix directly. + + Parameters + ---------- + rotation : 3-tuple of float, or 3x3 iterable + A 3-tuple of angles :math:`(\phi, \theta, \psi)` in degrees where + the first element is the rotation about the x-axis in the fixed + laboratory frame, the second element is the rotation about the + y-axis in the fixed laboratory frame, and the third element is the + rotation about the z-axis in the fixed laboratory frame. The + rotations are active rotations. Additionally a 3x3 rotation matrix + can be specified directly either as a nested iterable or array. + pivot : iterable of float, optional + (x, y, z) coordinates for the point to rotate about. Defaults to + (0., 0., 0.) + order : str, optional + A string of 'x', 'y', and 'z' in some order specifying which + rotation to perform first, second, and third. Defaults to 'xyz' + which means, the rotation by angle :math:`\phi` about x will be + applied first, followed by :math:`\theta` about y and then + :math:`\psi` about z. This corresponds to an x-y-z extrinsic + rotation as well as a z-y'-x'' intrinsic rotation using Tait-Bryan + angles :math:`(\phi, \theta, \psi)`. + inplace : boolean + Whether or not to return a new instance of Surface or to modify the + coefficients of this Surface in place. Defaults to False. + memo : dict or None + Dictionary used for memoization + + Returns + ------- + openmc.Halfspace + Translated half-space + + """ + if memo is None: + memo = {} + + # If rotated surface not in memo, add it + key = (self.surface, tuple(rotation), tuple(pivot), order, inplace) + if key not in memo: + memo[key] = self.surface.rotate(rotation, pivot=pivot, order=order, + inplace=inplace) + + # Return rotated half-space + return type(self)(memo[key], self.side) + + _SURFACE_CLASSES = {cls._type: cls for cls in Surface.__subclasses__()} diff --git a/tests/unit_tests/test_region.py b/tests/unit_tests/test_region.py index f9656ebb4b..8b2e06ad1b 100644 --- a/tests/unit_tests/test_region.py +++ b/tests/unit_tests/test_region.py @@ -40,6 +40,13 @@ def test_union(reset): assert (6, 0, 0) not in regt assert (8, 0, 0) in regt + # rotate method + regr = region.rotate((0., 90., 0.)) + assert (-4, 0, 0) not in regr + assert (0, 0, 6) in regr + assert (0, 0, -6) in regr + assert (0, 0, 3) not in regr + def test_intersection(reset): s1 = openmc.XPlane(x0=5, surface_id=1) @@ -73,6 +80,13 @@ def test_intersection(reset): assert (6, 0, 0) in regt assert (8, 0, 0) not in regt + # rotate method + regr = region.rotate((0., 90., 0.)) + assert (-4, 0, 0) in regr + assert (0, 0, 6) not in regr + assert (0, 0, -6) not in regr + assert (0, 0, 3) in regr + def test_complement(reset): zcyl = openmc.ZCylinder(r=1., surface_id=1) @@ -106,6 +120,14 @@ def test_complement(reset): assert ll == pytest.approx((0., 0., -4.)) assert ur == pytest.approx((2., 2., 6.)) + # rotate method + inside_r = inside.rotate((90., 0., 0.)) + ll, ur = inside_r.bounding_box + assert (.5, 2, 0) in inside_r + assert (0, 0, 6) not in inside_r + assert ll == pytest.approx((-1., -5., -1.)) + assert ur == pytest.approx((1., 5., 1.)) + def test_get_surfaces(): s1 = openmc.XPlane() diff --git a/tests/unit_tests/test_surface.py b/tests/unit_tests/test_surface.py index 77c8cbe5a5..b5e4015494 100644 --- a/tests/unit_tests/test_surface.py +++ b/tests/unit_tests/test_surface.py @@ -533,6 +533,15 @@ def test_quadric(): assert (st.g, st.h, st.j) == (-2, -2, -2) assert st.k == s.k + 3 + # rotate method + x0, y0, z0, r2 = 2, 3, 4, 4 + dx, dy, dz = 1, -1, 1 + s = openmc.Cone(x0=x0, y0=y0, z0=z0, dx=dx, dy=dy, dz=dz, r2=r2) + q = openmc.Quadric(*s._get_base_coeffs()) + qr = q.rotate((45, 60, 30)) + sr = s.rotate((45, 60, 30)) + assert qr.is_equal(sr) + def test_cylinder_from_points(): seed(1) # Make random numbers reproducible