Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rotate more parameters in HexBlock.rotate #1877

Open
wants to merge 46 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
71ca23d
Fix docstring for parameterDefinitions.Category
drewj-tp Sep 13, 2024
2221d28
Move Block rotate tests to separate TestCase
drewj-tp Sep 13, 2024
acf46f6
Add test for HexBlock rotate boundary parameters
drewj-tp Sep 13, 2024
4162cd7
Provide Parameter.hasCategory
drewj-tp Sep 13, 2024
c1e638a
Add parameter Category.rotatable
drewj-tp Sep 13, 2024
4215ce7
Move Category docstrings to attributes
drewj-tp Sep 13, 2024
dea16bc
Add some type hints to parameter methods
drewj-tp Sep 13, 2024
8fcd775
Mark pointsEdgeDpa and pointsCornerDpa as rotatable parameters
drewj-tp Sep 13, 2024
cac6b6f
Only rotate pointsCornerDpa and pointsEdgeDpa in HexBlock.rotate
drewj-tp Sep 13, 2024
2cdcd77
Add release note about HexBlock.rotate only rotating rotatable parame…
drewj-tp Sep 13, 2024
7ff0914
Revert "Provide Parameter.hasCategory"
drewj-tp Sep 18, 2024
ba32f6f
Revert "Add parameter Category.rotatable"
drewj-tp Sep 18, 2024
0474b59
Revert "Mark pointsEdgeDpa and pointsCornerDpa as rotatable parameters"
drewj-tp Sep 18, 2024
e8ded77
Revert "Only rotate pointsCornerDpa and pointsEdgeDpa in HexBlock.rot…
drewj-tp Sep 18, 2024
4549227
Revert "Add release note about HexBlock.rotate only rotating rotatabl…
drewj-tp Sep 18, 2024
e047b1c
Use iterator of parameters for rotating block boundary params
drewj-tp Sep 18, 2024
1378938
Add test for block rotated pin parameters
drewj-tp Sep 18, 2024
6662389
Skeleton of rotating block pin parameters with ParamLocation.CHILDREN
drewj-tp Sep 18, 2024
af9afb2
Type hint HexBlock.rotate args
drewj-tp Sep 18, 2024
d7feb0f
Fix docstring for pin data rotate test
drewj-tp Sep 19, 2024
61fabbb
Call out HexBlock not Block in rotate tests
drewj-tp Sep 19, 2024
b8e7516
Start working on rotating data defined on hex lattices
drewj-tp Sep 20, 2024
f0b9c1b
Use hexagon.rotateHexCellData in HexBlock pin param rotation
drewj-tp Sep 20, 2024
82f11de
Single line skip for scalars and not-defined unrotatable pin data
drewj-tp Sep 20, 2024
ea77cc5
Show rotating of vector data assigned at hex cell points
drewj-tp Sep 20, 2024
50df31c
Refactor hex cell data rotate tests to reduce duplicate code
drewj-tp Sep 20, 2024
4c939e7
Check two ring hex rotation data against wide variety of rotations
drewj-tp Sep 20, 2024
5f63878
Improve docstrings for TestHexCellRotate
drewj-tp Sep 20, 2024
2ebfa57
Add a maybe silly test that matrix data at hex cells can be rotated
drewj-tp Sep 20, 2024
fdd8c43
Test rotation of hex data for three rings
drewj-tp Sep 20, 2024
3494e4d
Change a helper test method name to be more explicit
drewj-tp Sep 20, 2024
bd5e662
Fix rotateHexCellData to account for number of cells per edge
drewj-tp Sep 20, 2024
0e5c200
Test rotating hex data with many rings
drewj-tp Sep 20, 2024
a580b3a
Fix rotatedHexCellData to cound entries along first axis
drewj-tp Sep 20, 2024
bcbac9c
Add test that hex cell data in lists can also be rotated
drewj-tp Sep 20, 2024
0c944b4
Docs for rotateHexCellData
drewj-tp Sep 20, 2024
1c174e7
rotateHexCellData no longer takes cells entry
drewj-tp Sep 20, 2024
de16d2a
Flush out more Block.rotate checks for pin data
drewj-tp Sep 20, 2024
28d16d8
Add release note for HexBlock.rotate rotating ParamLocation.CHILDREN …
drewj-tp Sep 20, 2024
14bb890
Add docstring for Category.thermalHydraulics
drewj-tp Sep 20, 2024
63a4095
Add docstring for ParamLocation.CHILDREN
drewj-tp Sep 20, 2024
4741505
Allow rotateHexCellData to do nothing on empty data
drewj-tp Sep 20, 2024
ca410f0
Add more useful exception if block parameter fails to be rotated
drewj-tp Sep 20, 2024
586b850
Add test for invalid rotatable data types
drewj-tp Sep 20, 2024
52f7d50
Merge branch 'main' into drewj/rotate-block-params/1860
drewj-tp Sep 20, 2024
f893079
Apply suggestions from code review
drewj-tp Sep 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions armi/reactor/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

Assemblies are made of blocks. Blocks are made of components.
"""
from typing import Optional, Type, Tuple, ClassVar
from typing import Optional, Type, Tuple, ClassVar, Callable
import collections
import copy
import math
Expand Down Expand Up @@ -2004,7 +2004,7 @@ def setPinPowers(self, powers, powerKeySuffix=""):
else:
self.p.linPowByPin = self.p[powerKey]

def rotate(self, rad):
def rotate(self, rad: float):
"""
Rotates a block's spatially varying parameters by a specified angle in the
counter-clockwise direction.
Expand All @@ -2027,6 +2027,11 @@ def rotate(self, rad):
self._rotatePins(rotNum)
self._rotateBoundaryParameters(rotNum)
self._rotateDisplacement(rad)
self._rotatePinParameters(rotNum)

def _getParamsWhere(self, f: Callable[[parameters.Parameter], bool]):
john-science marked this conversation as resolved.
Show resolved Hide resolved
"""Produce an iterator of parameters that match a condition."""
return filter(f, self.pDefs)

def _rotateBoundaryParameters(self, rotNum: int):
"""Rotate any parameters defined on the corners or edge of bounding hexagon.
Expand All @@ -2038,9 +2043,12 @@ def _rotateBoundaryParameters(self, rotNum: int):
rotations have taken place.

"""
names = self.p.paramDefs.atLocation(ParamLocation.CORNERS).names
names += self.p.paramDefs.atLocation(ParamLocation.EDGES).names
for name in names:
params = self._getParamsWhere(
lambda p: p.atLocation(ParamLocation.CORNERS)
or p.atLocation(ParamLocation.EDGES)
)
for param in params:
name = param.name
original = self.p[name]
if isinstance(original, (list, np.ndarray)):
if len(original) == 6:
Expand Down Expand Up @@ -2167,6 +2175,25 @@ def _rotatePins(self, rotNum, justCompute=False):

return rotateIndexLookup

def _rotatePinParameters(self, rotNum: int):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use a one-line comment. WE had to talk about this a lot, so there must be something interesting to say about it.

Up to you though; not a deal breaker.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good with me

params = self._getParamsWhere(lambda pd: pd.atLocation(ParamLocation.CHILDREN))
for param in params:
name = param.name
original = self.p[name]
if isinstance(original, (list, np.ndarray)):
try:
newData = hexagon.rotateHexCellData(original, rotNum)
self.p[name] = newData
except Exception as ee:
raise RuntimeError(
f"Failed to rotate parameter {name=} with data={original}"
) from ee
# Doesn't make sense to rotate scalar data nor data that isn't defined
elif isinstance(original, (int, float)) or original is None:
pass
else:
raise TypeError(f"{name=} :: {original=}")

def verifyBlockDims(self):
"""Perform some checks on this type of block before it is assembled."""
try:
Expand Down
48 changes: 22 additions & 26 deletions armi/reactor/parameters/parameterDefinitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,38 +55,33 @@


class Category:
"""
A "namespace" for storing parameter categories.

Notes
-----
* `cumulative` parameters are accumulated over many time steps
* `pinQuantities` parameters are defined on the pin level within a block
* `multiGroupQuantities` parameters have group dependence (often a 1D numpy array)
* `fluxQuantities` parameters are related to neutron or gamma flux
* `neutronics` parameters are calculated in a neutronics global flux solve
* `gamma` parameters are calculated in a fixed-source gamma solve
* `detailedAxialExpansion` parameters are marked as such so that they are mapped from the
uniform mesh back to the non-uniform mesh
* `reactivity coefficients` parameters are related to reactivity coefficient or kinetics
parameters for kinetics solutions
* `thermal hydraulics` parameters come from a thermal hydraulics physics plugin (e.g., flow
rates, temperatures, etc.)
"""
"""A "namespace" for storing parameter categories."""

depletion = "depletion"
"""Parameters used in or calculated by a depletion plugin."""
cumulative = "cumulative"
"""Parameters are accumulated over many time steps"""
cumulativeOverCycle = "cumulative over cycle"
"""Parameters that are reset at beginning of cycle and accumulated over each cycle."""
assignInBlueprints = "assign in blueprints"
"""Parameters that should be assigned in blueprints (e.g., control rod elevation)"""
retainOnReplacement = "retain on replacement"
pinQuantities = "pinQuantities"
"""Parameters are defined on the pin level within a block"""
fluxQuantities = "fluxQuantities"
"""Parameters are related to neutron or gamma flux"""
Comment on lines -58 to +72
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This came up in a chat so wanted to put my motivation here)

The change here is to make these docstrings attached to the class constants. They were already documented in the Category docstring (some of them at least) but I found two flaws

  1. They were documented in the Notes section but would make more sense to be properly documented as class attributes
  2. Attaching the docstring to the object itself means tools like python, ipython, vscode, etc. can provide context where you are about the thing you're looking at. You don't need to separately pull up the ARMI docs.

I'm not going to die on this hill if people have stronger opinions. This is just my rationale for this change.

multiGroupQuantities = "multi-group quantities"
"""Parameters have group dependence (often a 1D numpy array)"""
neutronics = "neutronics"
"""Parameters are calculated in a neutronics global flux solve"""
gamma = "gamma"
"""Parameters are calculated in a fixed-source gamma solve"""
detailedAxialExpansion = "detailedAxialExpansion"
"""Parameters that are mapped from the uniform mesh back to the non-uniform mesh"""
reactivityCoefficients = "reactivity coefficients"
"""Parameters are related to reactivity coefficient or kinetics parameters for kinetics solutions"""
thermalHydraulics = "thermal hydraulics"
"""Parameters related to thermal hydraulics"""


class ParamLocation(enum.Flag):
Expand All @@ -100,7 +95,8 @@ class ParamLocation(enum.Flag):
CORNERS = 32
EDGES = 64
VOLUME_INTEGRATED = 128
CHILDREN = 256 # on some child of a composite, like a pin
CHILDREN = 256
"""Parameter defined on some child of a composite, like a pin."""


class NoDefault:
Expand Down Expand Up @@ -417,7 +413,7 @@ def restoreBackup(self, paramsToApply):
else:
self._backup, self.assigned = self._backup

def atLocation(self, loc):
def atLocation(self, loc: ParamLocation) -> bool:
"""True if parameter is defined at location."""
return self.location and self.location & loc

Expand All @@ -438,7 +434,7 @@ class ParameterDefinitionCollection:
__slots__ = ("_paramDefs", "_paramDefDict", "_representedTypes", "_locked")

def __init__(self):
self._paramDefs = list()
self._paramDefs: list[Parameter] = list()
self._paramDefDict = dict()
self._representedTypes = set()
self._locked = False
Expand Down Expand Up @@ -505,14 +501,14 @@ def extend(self, other):
for pd in other:
self.add(pd)

def inCategory(self, categoryName):
def inCategory(self, categoryName: str):
"""
Create a :py:class:`ParameterDefinitionCollection` that contains definitions that are in a
specific category.
"""
return self._filter(lambda pd: categoryName in pd.categories)

def atLocation(self, paramLoc):
def atLocation(self, paramLoc: ParamLocation):
"""
Make a param definition collection with all defs defined at a specific location.

Expand Down Expand Up @@ -571,22 +567,22 @@ def byNameAndCollectionType(self, name, collectionType):
return self._paramDefDict[name, collectionType]

@property
def categories(self):
def categories(self) -> set[str]:
"""Get the categories of all the :py:class:`~Parameter` instances within this collection."""
categories = set()
for paramDef in self:
categories |= paramDef.categories
return categories

@property
def names(self):
def names(self) -> list[str]:
return [pd.name for pd in self]

def lock(self):
self._locked = True

@property
def locked(self):
def locked(self) -> bool:
return self._locked

def toWriteToDB(self, assignedMask: Optional[int] = None):
Expand Down
153 changes: 121 additions & 32 deletions armi/reactor/tests/test_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from armi.reactor.flags import Flags
from armi.reactor.tests.test_assemblies import makeTestAssembly
from armi.tests import ISOAA_PATH, TEST_ROOT
from armi.utils import hexagon, units
from armi.utils import hexagon, units, iterables
from armi.utils.units import MOLES_PER_CC_TO_ATOMS_PER_BARN_CM

NUM_PINS_IN_TEST_BLOCK = 217
Expand Down Expand Up @@ -1447,37 +1447,6 @@ def test_106_getAreaFractions(self):

self.assertAlmostEqual(sum(fracs.values()), sum([a for c, a in cur]))

def test_rotatePins(self):
b = self.block
b.setRotationNum(0)
index = b._rotatePins(0, justCompute=True)
self.assertEqual(b.getRotationNum(), 0)
self.assertEqual(index[5], 5)
self.assertEqual(index[2], 2) # pin 1 is center and never rotates.

index = b._rotatePins(1)
self.assertEqual(b.getRotationNum(), 1)
self.assertEqual(index[2], 3)
self.assertEqual(b.p.pinLocation[1], 3)

index = b._rotatePins(1)
self.assertEqual(b.getRotationNum(), 2)
self.assertEqual(index[2], 4)
self.assertEqual(b.p.pinLocation[1], 4)

index = b._rotatePins(2)
index = b._rotatePins(4) # over-rotate to check modulus
self.assertEqual(b.getRotationNum(), 2)
self.assertEqual(index[2], 4)
self.assertEqual(index[6], 2)
self.assertEqual(b.p.pinLocation[1], 4)
self.assertEqual(b.p.pinLocation[5], 2)

self.assertRaises(ValueError, b._rotatePins, -1)
self.assertRaises(ValueError, b._rotatePins, 10)
self.assertRaises((ValueError, TypeError), b._rotatePins, None)
self.assertRaises((ValueError, TypeError), b._rotatePins, "a")

def test_expandElementalToIsotopics(self):
r"""Tests the expand to elementals capability."""
initialN = {}
Expand Down Expand Up @@ -1766,6 +1735,126 @@ def test_getReactionRates(self):
)


class BlockRotateTests(unittest.TestCase):
"""Tests for the ability for a block to be rotated."""

BOUNDARY_PARAMS = [
"cornerFastFlux",
"pointsCornerDpa",
"pointsCornerDpaRate",
"pointsCornerFastFluxFr",
"pointsEdgeDpa",
"pointsEdgeDpaRate",
"pointsEdgeFastFluxFr",
]
BOUNDARY_DATA = np.arange(6, dtype=float) * 10

PIN_PARAMS = [
"percentBuByPin",
]
PIN_DATA = np.arange(NUM_PINS_IN_TEST_BLOCK, dtype=float)
"""Scalar data for each pin.

Data at index ``i`` corresponds to pin number ``i`` in the ARMI
numbering scheme.

*. ``0`` is center pin, (ring, pos) = (1, 1)
*. ``1`` (ring, pos) = (2, 1)
*. ``2`` (ring, pos) = (2, 2)
*. ...
*. ``7`` (ring, pos) = (2, 6)
*. ``8`` (ring, pos) = (3, 1)
*. ...

When we rotate a HexBlock a single 60 degree rotation counter clock wise,
the data that was originally at (r, p) = (2, 1) is now at (r, p) = (2, 2)
and should be at index ``2`` in the post-rotated array.
"""

def setUp(self):
self.block = loadTestBlock()
for name in self.BOUNDARY_PARAMS:
self.block.p[name] = self.BOUNDARY_DATA
for name in self.PIN_PARAMS:
self.block.p[name] = self.PIN_DATA

def test_rotatePins(self):
"""Test rotate pins updates pin locations."""
b = self.block
b.setRotationNum(0)
index = b._rotatePins(0, justCompute=True)
self.assertEqual(b.getRotationNum(), 0)
self.assertEqual(index[5], 5)
self.assertEqual(index[2], 2) # pin 1 is center and never rotates.

index = b._rotatePins(1)
self.assertEqual(b.getRotationNum(), 1)
self.assertEqual(index[2], 3)
self.assertEqual(b.p.pinLocation[1], 3)

index = b._rotatePins(1)
self.assertEqual(b.getRotationNum(), 2)
self.assertEqual(index[2], 4)
self.assertEqual(b.p.pinLocation[1], 4)

index = b._rotatePins(2)
index = b._rotatePins(4) # over-rotate to check modulus
self.assertEqual(b.getRotationNum(), 2)
self.assertEqual(index[2], 4)
self.assertEqual(index[6], 2)
self.assertEqual(b.p.pinLocation[1], 4)
self.assertEqual(b.p.pinLocation[5], 2)

self.assertRaises(ValueError, b._rotatePins, -1)
self.assertRaises(ValueError, b._rotatePins, 10)
self.assertRaises((ValueError, TypeError), b._rotatePins, None)
self.assertRaises((ValueError, TypeError), b._rotatePins, "a")

def test_rotateBoundaryParameters(self):
"""Test that boundary parameters are correctly rotated."""
# 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, expected)
# undo rotation to restore state for next test
self.block._rotateBoundaryParameters(6 - rotNum)
# Six rotations of 60 degrees puts us back to the original layout
self._rotateAndCompareBoundaryParams(6, self.BOUNDARY_DATA)

def _rotateAndCompareBoundaryParams(self, rotNum: int, expected: np.ndarray):
self.block._rotateBoundaryParameters(rotNum)
for name in self.BOUNDARY_PARAMS:
data = self.block.p[name]
msg = f"{name=} :: {rotNum=} :: {data=}"
np.testing.assert_array_equal(data, expected, err_msg=msg)

def test_rotatedPinParameters(self):
"""Test that block parameters that reflect pin data are rotated.

Pre-rotate pin layout -> Post-rotate layout::

2 1 1 6
3 0 6 -> 2 0 5
4 5 3 4

- Pre-rotate data: ``[0, 1, 2, 3, 4, 5, 6, ...]``
- Post-rotate data: ``[0, 6, 1, 2, 3, 4, 5, ...]``
"""
self.block.rotate(math.radians(60))
preRotate = self.PIN_DATA
postRotate = self.block.p["percentBuByPin"]
# Center location should be the same
self.assertEqual(postRotate[0], preRotate[0])
# First ring should be shifted one index
self.assertEqual(postRotate[1], preRotate[6])
self.assertEqual(postRotate[2], preRotate[1])
self.assertEqual(postRotate[3], preRotate[2])
self.assertEqual(postRotate[4], preRotate[3])
self.assertEqual(postRotate[5], preRotate[4])
self.assertEqual(postRotate[6], preRotate[5])


class BlockEnergyDepositionConstants(unittest.TestCase):
"""Tests the energy deposition methods.

Expand Down
Loading
Loading