Source code for armi.reactor.tests.test_hexBlockRotate

# Copyright 2024 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.
"""Tests for the ability to rotate a hexagonal block."""

import copy
import math
import unittest

import numpy as np

from armi.reactor.blocks import HexBlock
from armi.reactor.components import Component
from armi.reactor.grids import (
    CoordinateLocation,
    HexGrid,
    IndexLocation,
    MultiIndexLocation,
)
from armi.reactor.tests.test_blocks import NUM_PINS_IN_TEST_BLOCK, loadTestBlock
from armi.utils import iterables


[docs] class HexBlockRotateTests(unittest.TestCase): """Tests for various rotation aspects of a hex block.""" BOUNDARY_PARAMS = [ "cornerFastFlux", "pointsCornerDpa", "pointsCornerDpaRate", "pointsCornerFastFluxFr", "pointsEdgeDpa", "pointsEdgeDpaRate", "pointsEdgeFastFluxFr", "THedgeTemp", "THcornTemp", ] BOUNDARY_DATA = np.arange(6, dtype=float) * 10 PIN_PARAMS = [ "percentBuByPin", "linPowByPin", ] PIN_DATA = np.arange(NUM_PINS_IN_TEST_BLOCK, dtype=float) def setUp(self): self.baseBlock = loadTestBlock() self._assignParamData(self.BOUNDARY_PARAMS, self.BOUNDARY_DATA) self._assignParamData(self.PIN_PARAMS, self.PIN_DATA) def _assignParamData(self, names: list[str], referenceData: np.ndarray): """Assign initial rotatable pararameter data. Make some arrays, some lists to make sure we have good coverage of usage. """ # Yes we're putting the variable type in the name but that's why this method exists listData = referenceData.tolist() for ix, name in enumerate(names): self.baseBlock.p[name] = referenceData if (ix % 2) else listData
[docs] def test_orientationVector(self): """Test the z-value in the orientation vector matches rotation. .. test:: Demonstrate that a HexBlock can be rotated in 60 degree increments, and the resultant orientation parameter reflects the current rotation. :id: T_ARMI_ROTATE_HEX_BLOCK :tests: R_ARMI_ROTATE_HEX """ for nRotations in range(-10, 10): rotationAmount = 60 * nRotations fresh = copy.deepcopy(self.baseBlock) self.assertEqual(fresh.p.orientation[2], 0.0, msg=nRotations) fresh.rotate(math.radians(rotationAmount)) # Ensure rotation is bounded [0, 360) postRotationOrientation = fresh.p.orientation[2] self.assertTrue(0 <= postRotationOrientation < 360, msg=nRotations) # Trim off any extra rotation if beyond 360 or negative # What is the effective counter clockwise rotation? expectedOrientation = rotationAmount % 360 self.assertEqual( postRotationOrientation, expectedOrientation, msg=nRotations )
[docs] def test_rotateBoundaryParameters(self): """Test that boundary parameters are correctly rotated. .. test:: Rotating a hex block updates parameters on the boundary of the hexagon. :id: T_ARMI_ROTATE_HEX_BOUNDARY :tests: R_ARMI_ROTATE_HEX """ # No rotation == no changes to data self._rotateAndCompareBoundaryParams(0, self.BOUNDARY_DATA) for rotNum in range(1, 6): expected = iterables.pivot(self.BOUNDARY_DATA, -rotNum) self._rotateAndCompareBoundaryParams(rotNum * 60, expected) # Six rotations of 60 degrees puts us back to the original layout self._rotateAndCompareBoundaryParams(360, self.BOUNDARY_DATA)
def _rotateAndCompareBoundaryParams(self, degrees: float, expected: np.ndarray): fresh = copy.deepcopy(self.baseBlock) fresh.rotate(math.radians(degrees)) for name in self.BOUNDARY_PARAMS: data = fresh.p[name] msg = f"{name=} :: {degrees=} :: {data=}" np.testing.assert_array_equal(data, expected, err_msg=msg)
[docs] def assertIndexLocationEquivalent( self, actual: IndexLocation, expected: IndexLocation ): """More flexible equivalency check on index locations. Specifically focused on locations on hex grids because this file is testing things on hex blocks. Checks that 1. ``i``, ``j``, and ``k`` are equal 2. Grids are both hex grid 3. Grids have same pitch and orientation. """ self.assertEqual(actual.i, expected.i) self.assertEqual(actual.j, expected.j) self.assertEqual(actual.k, expected.k) self.assertIsInstance(actual.grid, HexGrid) self.assertIsInstance(expected.grid, HexGrid) self.assertEqual(actual.grid.cornersUp, expected.grid.cornersUp) self.assertEqual(actual.grid.pitch, expected.grid.pitch)
[docs] def test_pinRotationLocations(self): """Test that pin locations are updated through rotation. .. test:: HexBlock.getPinLocations is consistent with rotation. :id: T_ARMI_ROTATE_HEX_PIN_LOCS :tests: R_ARMI_ROTATE_HEX """ preRotation = self.baseBlock.getPinLocations() for nRotations in range(-10, 10): degrees = 60 * nRotations fresh = copy.deepcopy(self.baseBlock) g = fresh.spatialGrid fresh.rotate(math.radians(degrees)) postRotation = fresh.getPinLocations() self.assertEqual(len(preRotation), len(postRotation)) for pre, post in zip(preRotation, postRotation): expected = g.rotateIndex(pre, nRotations) self.assertIndexLocationEquivalent(post, expected)
[docs] def test_pinRotationCoordinates(self): """Test that pin coordinates are updated through rotation. .. test:: HexBlock.getPinCoordinates is consistent through rotation. :id: T_ARMI_ROTATE_HEX_PIN_COORDS :tests: R_ARMI_ROTATE_HEX """ preRotation = self.baseBlock.getPinCoordinates() # Over- and under-rotate to make sure we can handle clockwise and counter # clockwise rotations, and cases that wrap around a full rotation for degrees in range(-600, 600, 60): fresh = copy.deepcopy(self.baseBlock) rads = math.radians(degrees) fresh.rotate(rads) rotationMatrix = np.array( [ [math.cos(rads), -math.sin(rads)], [math.sin(rads), math.cos(rads)], ] ) postRotation = fresh.getPinCoordinates() self.assertEqual(len(preRotation), len(postRotation)) for pre, post in zip(preRotation, postRotation): start = pre[:2] finish = post[:2] if np.allclose(start, 0): np.testing.assert_equal(start, finish) continue expected = rotationMatrix.dot(start) np.testing.assert_allclose(expected, finish, atol=1e-8)
[docs] def test_updateChildLocations(self): """Test that locations of all children are updated through rotation. .. test:: Rotating a hex block updates the spatial coordinates on contained objects. :id: T_ARMI_ROTATE_HEX_CHILD_LOCS :tests: R_ARMI_ROTATE_HEX """ for nRotations in range(-10, 10): fresh = copy.deepcopy(self.baseBlock) degrees = 60 * nRotations rads = math.radians(degrees) fresh.rotate(rads) for originalC, newC in zip(self.baseBlock, fresh): self._compareComponentLocationsAfterRotation( originalC, newC, nRotations, rads )
def _compareComponentLocationsAfterRotation( self, original: Component, updated: Component, nRotations: int, radians: float ): if isinstance(original.spatialLocator, MultiIndexLocation): for originalLoc, newLoc in zip( original.spatialLocator, updated.spatialLocator ): expected = originalLoc.grid.rotateIndex(originalLoc, nRotations) self.assertIndexLocationEquivalent(newLoc, expected) elif isinstance(original.spatialLocator, CoordinateLocation): ox, oy, oz = original.spatialLocator.getLocalCoordinates() nx, ny, nz = updated.spatialLocator.getLocalCoordinates() self.assertEqual(nz, oz, msg=f"{original=} :: {radians=}") rotationMatrix = np.array( [ [math.cos(radians), -math.sin(radians)], [math.sin(radians), math.cos(radians)], ] ) expectedX, expectedY = rotationMatrix.dot((ox, oy)) np.testing.assert_allclose( (nx, ny), (expectedX, expectedY), err_msg=f"{original=} :: {radians=}" )
[docs] def test_pinParametersUnmodified(self): """Test that pin data are not modified through rotation. Reinforces the idea that data like ``linPowByPin[i]`` are assigned to pin ``i``, wherever it may be. Locations are defined instead by ``getPinCoordinates()[i]``. """ fresh = copy.deepcopy(self.baseBlock) fresh.rotate(math.radians(60)) for paramName in self.PIN_PARAMS: actual = fresh.p[paramName] np.testing.assert_equal(actual, self.PIN_DATA, err_msg=paramName)
[docs] class EmptyBlockRotateTest(unittest.TestCase): """Rotation tests on an empty hexagonal block. Useful for enforcing rotation works on blocks without pins. """ def setUp(self): self.block = HexBlock("empty")
[docs] def test_orientation(self): """Test the orientation parameter is updated on a rotated empty block.""" rotDegrees = 60 preRotateOrientation = self.block.p.orientation[2] self.block.rotate(math.radians(rotDegrees)) postRotationOrientation = self.block.p.orientation[2] self.assertNotEqual(preRotateOrientation, postRotationOrientation) self.assertEqual(postRotationOrientation, rotDegrees)