Source code for armi.reactor.grids.cartesian

# Copyright 2023 TerraPower, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import itertools
from typing import Optional, NoReturn, Tuple

import numpy

from armi.reactor import geometry

from armi.reactor.grids.locations import IJType
from armi.reactor.grids.structuredGrid import StructuredGrid


[docs]class CartesianGrid(StructuredGrid): """ Grid class representing a conformal Cartesian mesh. It is recommended to call :meth:`fromRectangle` to construct, rather than directly constructing with ``__init__`` Notes ----- In Cartesian, (i, j, k) indices map to (x, y, z) coordinates. In an axial plane (i, j) are as follows:: (-1, 1) ( 0, 1) ( 1, 1) (-1, 0) ( 0, 0) ( 1, 0) (-1,-1) ( 0,-1) ( 1,-1) The concepts of ring and position are a bit tricker in Cartesian grids than in Hex, because unlike in the Hex case, there is no guaranteed center location. For example, when using a CartesianGrid to lay out assemblies in a core, there is only a single central location if the number of assemblies in the core is odd-by-odd; in an even-by-even case, there are four center-most assemblies. Therefore, the number of locations per ring will vary depending on the "through center" nature of ``symmetry``. Furthermore, notice that in the "through center" (odd-by-odd) case, the central index location, (0,0) is typically centered at the origin (0.0, 0.0), whereas with the "not through center" (even-by-even) case, the (0,0) index location is offset, away from the origin. These concepts are illustrated in the example drawings below. .. figure:: ../.static/through-center.png :width: 400px :align: center Grid example where the axes pass through the "center assembly" (odd-by-odd). Note that ring 1 only has one location in it. .. figure:: ../.static/not-through-center.png :width: 400px :align: center Grid example where the axes lie between the "center assemblies" (even-by-even). Note that ring 1 has four locations, and that the center of the (0, 0)-index location is offset from the origin. """
[docs] @classmethod def fromRectangle( cls, width, height, numRings=5, symmetry="", isOffset=False, armiObject=None ): """ Build a finite step-based 2-D Cartesian grid based on a width and height in cm. Parameters ---------- width : float Width of the unit rectangle height : float Height of the unit rectangle numRings : int Number of rings that the grid should span symmetry : str The symmetry condition (see :py:mod:`armi.reactor.geometry`) isOffset : bool If True, the origin of the Grid's coordinate system will be placed at the bottom-left corner of the center-most cell. Otherwise, the origin will be placed at the center of the center-most cell. armiObject : ArmiObject An object in a Composite model that the Grid should be bound to. """ unitSteps = ((width, 0.0, 0.0), (0.0, height, 0.0), (0, 0, 0)) offset = numpy.array((width / 2.0, height / 2.0, 0.0)) if isOffset else None return cls( unitSteps=unitSteps, unitStepLimits=((-numRings, numRings), (-numRings, numRings), (0, 1)), offset=offset, armiObject=armiObject, symmetry=symmetry, )
[docs] def overlapsWhichSymmetryLine(self, indices: IJType) -> None: """Return lines of symmetry position at a given index can be found. .. warning:: This is not really implemented, but parts of ARMI need it to not fail, so it always returns None. """ return None
[docs] def getRingPos(self, indices): """ Return ring and position from indices. Ring is the Manhattan distance from (0, 0) to the passed indices. Position counts up around the ring counter-clockwise from the quadrant 1 diagonal, like this:: 7 6 5 4 3 2 1 8 | 24 9 | 23 10 -------|------ 22 11 | 21 12 | 20 13 14 15 16 17 18 19 Grids that split the central locations have 1 location in in inner-most ring, whereas grids without split central locations will have 4. Notes ----- This is needed to support GUI, but should not often be used. i, j (0-based) indices are much more useful. For example: >>> locator = core.spatialGrid[i, j, 0] # 3rd index is 0 for assembly >>> a = core.childrenByLocator[locator] >>> a = core.childrenByLocator[core.spatialGrid[i, j, 0]] # one liner """ i, j = indices[0:2] split = self._isThroughCenter() if not split: i += 0.5 j += 0.5 ring = max(abs(int(i)), abs(int(j))) if not split: ring += 0.5 if j == ring: # region 1 pos = -i + ring elif i == -ring: # region 2 pos = 3 * ring - j elif j == -ring: # region 3 pos = 5 * ring + i else: # region 4 pos = 7 * ring + j return (int(ring) + 1, int(pos) + 1)
[docs] @staticmethod def getIndicesFromRingAndPos(ring: int, pos: int) -> NoReturn: """Not implemented for Cartesian-see getRingPos notes.""" raise NotImplementedError( "Cartesian should not need need ring/pos, use i, j indices." "See getRingPos doc string notes for more information/example." )
[docs] def getMinimumRings(self, n: int) -> int: """Return the minimum number of rings needed to fit ``n`` objects.""" numPositions = 0 ring = 0 for ring in itertools.count(1): ringPositions = self.getPositionsInRing(ring) numPositions += ringPositions if numPositions >= n: break return ring
[docs] def getPositionsInRing(self, ring: int) -> int: """ Return the number of positions within a ring. Parameters ---------- ring : int Ring in question Notes ----- The number of positions within a ring will change depending on whether the central position in the grid is at origin, or if origin is the point where 4 positions meet (i.e., the ``_isThroughCenter`` method returns True). """ if ring == 1: ringPositions = 1 if self._isThroughCenter() else 4 else: ringPositions = (ring - 1) * 8 if not self._isThroughCenter(): ringPositions += 4 return ringPositions
[docs] def locatorInDomain(self, locator, symmetryOverlap: Optional[bool] = False): if self.symmetry.domain == geometry.DomainType.QUARTER_CORE: return locator.i >= 0 and locator.j >= 0 else: return True
[docs] def changePitch(self, xw: float, yw: float): """ Change the pitch of a Cartesian grid. This also scales the offset. """ xwOld = self._unitSteps[0][0] ywOld = self._unitSteps[1][1] self._unitSteps = numpy.array(((xw, 0.0, 0.0), (0.0, yw, 0.0), (0, 0, 0)))[ self._stepDims ] newOffsetX = self._offset[0] * xw / xwOld newOffsetY = self._offset[1] * yw / ywOld self._offset = numpy.array((newOffsetX, newOffsetY, 0.0))
[docs] def getSymmetricEquivalents(self, indices): symmetry = self.symmetry # construct the symmetry object once up top isRotational = symmetry.boundary == geometry.BoundaryType.PERIODIC i, j = indices[0:2] if symmetry.domain == geometry.DomainType.FULL_CORE: return [] elif symmetry.domain == geometry.DomainType.QUARTER_CORE: if symmetry.isThroughCenterAssembly: # some locations lie on the symmetric boundary if i == 0 and j == 0: # on the split corner, so the location is its own symmetric # equivalent return [] elif i == 0: if isRotational: return [(j, i), (i, -j), (-j, i)] else: return [(i, -j)] elif j == 0: if isRotational: return [(j, i), (-i, j), (j, -i)] else: return [(-i, j)] else: # Math is a bit easier for the split case, since there is an actual # center location for (0, 0) if isRotational: return [(-j, i), (-i, -j), (j, -i)] else: return [(-i, j), (-i, -j), (i, -j)] else: # most objects have 3 equivalents. the bottom-left corner of Quadrant I # is (0, 0), so to reflect, add one and negate each index in # combination. To rotate, first flip the indices for the Quadrant II and # Quadrant IV if isRotational: # rotational # QII QIII QIV return [(-j - 1, i), (-i - 1, -j - 1), (j, -i - 1)] else: # reflective # QII QIII QIV return [(-i - 1, j), (-i - 1, -j - 1), (i, -j - 1)] elif symmetry.domain == geometry.DomainType.EIGHTH_CORE: raise NotImplementedError( "Eighth-core symmetry isn't fully implemented for grids yet!" ) else: raise NotImplementedError( "Unhandled symmetry condition for {}: {}".format( type(self).__name__, symmetry.domain ) )
def _isThroughCenter(self): """Return whether the central cells are split through the middle for symmetry.""" return all(self._offset == [0, 0, 0]) @property def pitch(self) -> Tuple[float, float]: """Grid pitch in the x and y dimension. Returns ------- float x-pitch (cm) float y-pitch (cm) """ pitch = (self._unitSteps[0][0], self._unitSteps[1][1]) if pitch[0] == 0: raise ValueError(f"Grid {self} does not have a defined pitch.") return pitch