From b5d30ff553d2d5379635cee04bb21d70653d6f2e Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 18 Apr 2023 22:04:46 -0500 Subject: [PATCH] Refactor BoundingBox implementation --- docs/source/pythonapi/base.rst | 1 + openmc/bounding_box.py | 67 +++++++++++++------------- openmc/cell.py | 6 ++- openmc/geometry.py | 2 +- openmc/lib/cell.py | 4 +- openmc/region.py | 10 ++-- openmc/surface.py | 68 +++++++++++++-------------- openmc/universe.py | 6 +-- tests/unit_tests/test_bounding_box.py | 36 +++++--------- 9 files changed, 96 insertions(+), 104 deletions(-) diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index 72d2534ea..d379fb4bb 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -87,6 +87,7 @@ Building geometry openmc.Intersection openmc.Union openmc.Complement + openmc.BoundingBox openmc.Cell openmc.Universe openmc.DAGMCUniverse diff --git a/openmc/bounding_box.py b/openmc/bounding_box.py index e297cc225..a4a695b99 100644 --- a/openmc/bounding_box.py +++ b/openmc/bounding_box.py @@ -1,54 +1,57 @@ +from typing import Iterable + import numpy as np -from .checkvalue import check_type, check_length + +from .checkvalue import check_length class BoundingBox(tuple): """Axis-aligned bounding box. + .. versionadded:: 0.13.4 + Parameters ---------- - corners : 2-tuple of numpy.array - Lower-left and upper-right coordinates of an axis-aligned bounding box - of the domain. + lower_left : iterable of float + The x, y, z coordinates of the lower left corner of the bounding box in [cm] + upper_right : iterable of float + The x, y, z coordinates of the upper right corner of the bounding box in [cm] Attributes ---------- - center: numpy.array - x, y, z coordinates of the center of the bounding box in cm. - lower_left: numpy.array - The x, y, z coordinates of the lower left corner of the bounding box - upper_right - The x, y, z coordinates of the upper right corner of the bounding box - volume: float - The volume of the bounding box in cm3 + center : numpy.ndarray + x, y, z coordinates of the center of the bounding box in [cm]. + lower_left : numpy.ndarray + The x, y, z coordinates of the lower left corner of the bounding box in [cm] + upper_right : numpy.ndarray + The x, y, z coordinates of the upper right corner of the bounding box in [cm] + volume : float + The volume of the bounding box in [cm^3] """ - def __init__(self, corners): + def __new__(cls, lower_left: Iterable[float], upper_right: Iterable[float]): + check_length("lower_left", lower_left, 3, 3) + check_length("upper_right", upper_right, 3, 3) + lower_left = np.array(lower_left, dtype=float) + upper_right = np.array(upper_right, dtype=float) + return tuple.__new__(cls, (lower_left, upper_right)) - check_type("corners", corners, tuple) - check_type("corners", corners[0], np.ndarray) - check_type("corners", corners[1], np.ndarray) - check_length("corners", corners, 2, 2) - check_length("corners", corners[0], 3, 3) - check_length("corners", corners[1], 3, 3) - self.corners = corners + def __repr__(self) -> str: + return "BoundingBox(lower_left={}, upper_right={})".format( + tuple(self.lower_left), tuple(self.upper_right)) @property - def center(self): - """The center x, y, z coordinates of the bounding box""" - return (self.corners[0] + self.corners[1]) / 2 + def center(self) -> np.ndarray: + return (self[0] + self[1]) / 2 @property - def lower_left(self): - """The x, y, z coordinates of the lower left corner of the bounding box""" - return self.corners[0] + def lower_left(self) -> np.ndarray: + return self[0] @property - def upper_right(self): - """The x, y, z coordinates of the upper right corner of the bounding box""" - return self.corners[1] + def upper_right(self) -> np.ndarray: + return self[1] @property - def volume(self): - """The volume of the bounding box""" - return np.abs(np.prod(self.corners[1] - self.corners[0])) + def volume(self) -> float: + return np.abs(np.prod(self[1] - self[0])) diff --git a/openmc/cell.py b/openmc/cell.py index db8ffd48b..67735ef5e 100644 --- a/openmc/cell.py +++ b/openmc/cell.py @@ -92,6 +92,8 @@ class Cell(IDManagerMixin): fill. For example, {'U235': 1.0e22, 'U238': 5.0e22, ...}. .. versionadded:: 0.12 + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the cell """ @@ -250,8 +252,8 @@ class Cell(IDManagerMixin): if self.region is not None: return self.region.bounding_box else: - return BoundingBox((np.array([-np.inf, -np.inf, -np.inf]), - np.array([np.inf, np.inf, np.inf]))) + return BoundingBox(np.array([-np.inf, -np.inf, -np.inf]), + np.array([np.inf, np.inf, np.inf])) @property def num_instances(self): diff --git a/openmc/geometry.py b/openmc/geometry.py index ad86cc56b..c3fd427e8 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -25,7 +25,7 @@ class Geometry: ---------- root_universe : openmc.UniverseBase Root universe which contains all others - bounding_box : 2-tuple of numpy.array + bounding_box : openmc.BoundingBox Lower-left and upper-right coordinates of an axis-aligned bounding box of the universe. merge_surfaces : bool diff --git a/openmc/lib/cell.py b/openmc/lib/cell.py index c88d6da51..3394643ae 100644 --- a/openmc/lib/cell.py +++ b/openmc/lib/cell.py @@ -103,7 +103,7 @@ class Cell(_FortranObjectWithID): Name of the cell num_instances : int Number of unique cell instances - bounding_box : 2-tuple of numpy.ndarray + bounding_box : openmc.BoundingBox Lower-left and upper-right coordinates of bounding box translation : Iterable of float 3-D coordinates of the translation vector @@ -291,7 +291,7 @@ class Cell(_FortranObjectWithID): llc[llc == -inf] = -np.inf urc[urc == -inf] = -np.inf - return BoundingBox((llc, urc)) + return BoundingBox(llc, urc) class _CellMapping(Mapping): diff --git a/openmc/region.py b/openmc/region.py index d8935a070..af9df1ba1 100644 --- a/openmc/region.py +++ b/openmc/region.py @@ -356,7 +356,7 @@ class Intersection(Region, MutableSequence): Attributes ---------- - bounding_box : tuple of numpy.array + bounding_box : openmc.BoundingBox Lower-left and upper-right coordinates of an axis-aligned bounding box """ @@ -419,7 +419,7 @@ class Intersection(Region, MutableSequence): lower_left_n, upper_right_n = n.bounding_box lower_left[:] = np.maximum(lower_left, lower_left_n) upper_right[:] = np.minimum(upper_right, upper_right_n) - return BoundingBox((lower_left, upper_right)) + return BoundingBox(lower_left, upper_right) class Union(Region, MutableSequence): @@ -444,7 +444,7 @@ class Union(Region, MutableSequence): Attributes ---------- - bounding_box : 2-tuple of numpy.array + bounding_box : openmc.BoundingBox Lower-left and upper-right coordinates of an axis-aligned bounding box """ @@ -507,7 +507,7 @@ class Union(Region, MutableSequence): lower_left_n, upper_right_n = n.bounding_box lower_left[:] = np.minimum(lower_left, lower_left_n) upper_right[:] = np.maximum(upper_right, upper_right_n) - return BoundingBox((lower_left, upper_right)) + return BoundingBox(lower_left, upper_right) class Complement(Region): @@ -534,7 +534,7 @@ class Complement(Region): ---------- node : openmc.Region Regions to take the complement of - bounding_box : tuple of numpy.array + bounding_box : openmc.BoundingBox Lower-left and upper-right coordinates of an axis-aligned bounding box """ diff --git a/openmc/surface.py b/openmc/surface.py index 98ced13c6..2912cfd7e 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -234,10 +234,10 @@ class Surface(IDManagerMixin, ABC): desired half-space """ - return BoundingBox(( + return BoundingBox( np.array([-np.inf, -np.inf, -np.inf]), np.array([np.inf, np.inf, np.inf]) - )) + ) def clone(self, memo=None): """Create a copy of this surface with a new unique ID. @@ -542,7 +542,7 @@ class PlaneMixin: else: ur = np.array([v if not np.isnan(v) else np.inf for v in vals]) - return BoundingBox((ll, ur)) + return BoundingBox(ll, ur) def evaluate(self, point): """Evaluate the surface equation at a given point. @@ -1199,10 +1199,10 @@ class Cylinder(QuadricMixin, Surface): return (np.array(ll), np.array(ur)) elif side == '+': - return BoundingBox(( + return BoundingBox( 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 @@ -1364,15 +1364,15 @@ class XCylinder(QuadricMixin, Surface): def bounding_box(self, side): if side == '-': - return BoundingBox(( + return BoundingBox( np.array([-np.inf, self.y0 - self.r, self.z0 - self.r]), np.array([np.inf, self.y0 + self.r, self.z0 + self.r]) - )) + ) elif side == '+': - return BoundingBox(( + return BoundingBox( np.array([-np.inf, -np.inf, -np.inf]), np.array([np.inf, np.inf, np.inf]) - )) + ) def evaluate(self, point): y = point[1] - self.y0 @@ -1459,15 +1459,15 @@ class YCylinder(QuadricMixin, Surface): def bounding_box(self, side): if side == '-': - return BoundingBox(( + return BoundingBox( np.array([self.x0 - self.r, -np.inf, self.z0 - self.r]), np.array([self.x0 + self.r, np.inf, self.z0 + self.r]) - )) + ) elif side == '+': - return BoundingBox(( + return BoundingBox( np.array([-np.inf, -np.inf, -np.inf]), np.array([np.inf, np.inf, np.inf]) - )) + ) def evaluate(self, point): x = point[0] - self.x0 @@ -1554,15 +1554,15 @@ class ZCylinder(QuadricMixin, Surface): def bounding_box(self, side): if side == '-': - return BoundingBox(( + return BoundingBox( np.array([self.x0 - self.r, self.y0 - self.r, -np.inf]), np.array([self.x0 + self.r, self.y0 + self.r, np.inf]) - )) + ) elif side == '+': - return BoundingBox(( + return BoundingBox( np.array([-np.inf, -np.inf, -np.inf]), np.array([np.inf, np.inf, np.inf]) - )) + ) def evaluate(self, point): x = point[0] - self.x0 @@ -1648,15 +1648,15 @@ class Sphere(QuadricMixin, Surface): def bounding_box(self, side): if side == '-': - return BoundingBox(( + return BoundingBox( np.array([self.x0 - self.r, self.y0 - self.r, self.z0 - self.r]), np.array([self.x0 + self.r, self.y0 + self.r, self.z0 + self.r]) - )) + ) elif side == '+': - return BoundingBox(( + return BoundingBox( np.array([-np.inf, -np.inf, -np.inf]), np.array([np.inf, np.inf, np.inf]) - )) + ) def evaluate(self, point): x = point[0] - self.x0 @@ -2272,15 +2272,15 @@ class XTorus(TorusMixin, Surface): x0, y0, z0 = self.x0, self.y0, self.z0 a, b, c = self.a, self.b, self.c if side == '-': - return BoundingBox(( + return BoundingBox( np.array([x0 - b, y0 - a - c, z0 - a - c]), np.array([x0 + b, y0 + a + c, z0 + a + c]) - )) + ) elif side == '+': - return BoundingBox(( + return BoundingBox( np.array([-np.inf, -np.inf, -np.inf]), np.array([np.inf, np.inf, np.inf]) - )) + ) class YTorus(TorusMixin, Surface): r"""A torus of the form :math:`(y - y_0)^2/B^2 + (\sqrt{(x - x_0)^2 + (z - @@ -2347,15 +2347,15 @@ class YTorus(TorusMixin, Surface): x0, y0, z0 = self.x0, self.y0, self.z0 a, b, c = self.a, self.b, self.c if side == '-': - return BoundingBox(( + return BoundingBox( np.array([x0 - a - c, y0 - b, z0 - a - c]), np.array([x0 + a + c, y0 + b, z0 + a + c]) - )) + ) elif side == '+': - return BoundingBox(( + return BoundingBox( np.array([-np.inf, -np.inf, -np.inf]), np.array([np.inf, np.inf, np.inf]) - )) + ) class ZTorus(TorusMixin, Surface): @@ -2423,15 +2423,15 @@ class ZTorus(TorusMixin, Surface): x0, y0, z0 = self.x0, self.y0, self.z0 a, b, c = self.a, self.b, self.c if side == '-': - return BoundingBox(( + return BoundingBox( np.array([x0 - a - c, y0 - a - c, z0 - b]), np.array([x0 + a + c, y0 + a + c, z0 + b]) - )) + ) elif side == '+': - return BoundingBox(( + return BoundingBox( np.array([-np.inf, -np.inf, -np.inf]), np.array([np.inf, np.inf, np.inf]) - )) + ) class Halfspace(Region): @@ -2466,7 +2466,7 @@ class Halfspace(Region): Surface which divides Euclidean space. side : {'+', '-'} Indicates whether the positive or negative half-space is used. - bounding_box : tuple of numpy.ndarray + bounding_box : openmc.BoundingBox Lower-left and upper-right coordinates of an axis-aligned bounding box """ diff --git a/openmc/universe.py b/openmc/universe.py index 2ed96f4bc..a8d0a0b3a 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -202,7 +202,7 @@ class Universe(UniverseBase): Volume of the universe in cm^3. This can either be set manually or calculated in a stochastic volume calculation and added via the :meth:`Universe.add_volume_information` method. - bounding_box : 2-tuple of numpy.array + bounding_box : openmc.BoundingBox Lower-left and upper-right coordinates of an axis-aligned bounding box of the universe. @@ -662,7 +662,7 @@ class DAGMCUniverse(UniverseBase): auto_mat_ids : bool Set IDs automatically on initialization (True) or report overlaps in ID space between OpenMC and UWUW materials (False) - bounding_box : 2-tuple of numpy.array + bounding_box : openmc.BoundingBox Lower-left and upper-right coordinates of an axis-aligned bounding box of the universe. @@ -972,4 +972,4 @@ class DAGMCUniverse(UniverseBase): clone.volume = self.volume clone.auto_geom_ids = self.auto_geom_ids clone.auto_mat_ids = self.auto_mat_ids - return clone \ No newline at end of file + return clone diff --git a/tests/unit_tests/test_bounding_box.py b/tests/unit_tests/test_bounding_box.py index 419ada653..f053bb2a1 100644 --- a/tests/unit_tests/test_bounding_box.py +++ b/tests/unit_tests/test_bounding_box.py @@ -3,15 +3,9 @@ import openmc import pytest -test_bb_1 = openmc.BoundingBox( - (np.array([-10.0, -20.0, -30.0]), np.array([1.0, 2.0, 3.0])) -) -test_bb_2 = openmc.BoundingBox( - (np.array([1.0, 2.0, 3.0]), np.array([11.0, 22.0, 33.0])) -) -test_bb_3 = openmc.BoundingBox( - (np.array([-10.0, -20.0, -30.0]), np.array([-1.0, -2.0, -3.0])) -) +test_bb_1 = openmc.BoundingBox((-10.0, -20.0, -30.0), (1.0, 2.0, 3.0)) +test_bb_2 = openmc.BoundingBox((1.0, 2.0, 3.0), (11.0, 22.0, 33.0)) +test_bb_3 = openmc.BoundingBox((-10.0, -20.0, -30.0), (-1.0, -2.0, -3.0)) @pytest.mark.parametrize( @@ -63,29 +57,21 @@ def test_bounding_box_center(bb, expected): def test_bounding_box_input_checking(): - # checks that a list is not accepted instead of np.array + # checks that only passing lower_left is not accepted with pytest.raises(TypeError): - openmc.BoundingBox(([-10, -20, -30], [1, 2, 3])) - # checks that a list is not accepted instead of tuple - with pytest.raises(TypeError): - openmc.BoundingBox([np.array([-10, -20, -30]), np.array([1, 2, 3])]) - # checks that a tuple with one entry is not accepted - with pytest.raises(TypeError): - openmc.BoundingBox((np.array([-10, -20, -30]))) + openmc.BoundingBox((-10, -20, -3)) # checks that a tuple with three entry is not accepted - with pytest.raises(ValueError): - openmc.BoundingBox( - (np.array([-1, -2, -3]), np.array([-1, -2, -3]), np.array([-1, -2, -3])) - ) + with pytest.raises(TypeError): + openmc.BoundingBox((-1, -2, -3), (-1, -2, -3), (-1, -2, -3)) # checks that a numpy array with two entries is not accepted with pytest.raises(ValueError): - openmc.BoundingBox((np.array([-10, -30]), np.array([1, 2, 3]))) + openmc.BoundingBox(np.array([-10, -30]), np.array([1, 2, 3])) # checks that a numpy array with two entries is not accepted with pytest.raises(ValueError): - openmc.BoundingBox((np.array([-10, -20, -30]), np.array([1, 3]))) + openmc.BoundingBox(np.array([-10, -20, -30]), np.array([1, 3])) # checks that a numpy array with four entries is not accepted with pytest.raises(ValueError): - openmc.BoundingBox((np.array([-10, -20, -3, -4]), np.array([1, 2, 3]))) + openmc.BoundingBox(np.array([-10, -20, -3, -4]), np.array([1, 2, 3])) # checks that a numpy array with four entries is not accepted with pytest.raises(ValueError): - openmc.BoundingBox((np.array([-10, -20, -4]), np.array([1, 2, 3, 4]))) + openmc.BoundingBox(np.array([-10, -20, -4]), np.array([1, 2, 3, 4]))