Skip to content

Commit

Permalink
Merge branch 'drewj/bu-eq-rotate' into drewj/bu-rotate-with-pin-dep
Browse files Browse the repository at this point in the history
* drewj/bu-eq-rotate:
  Dont try to check burnup on new assembly if fresh from load queue
  Better comment for skipping aPrev not in core in rotation
  Check against Component.p.pinPercentBu for burnup equalizing rotation
  • Loading branch information
drewj-tp committed Oct 31, 2024
2 parents 5b8e76a + 0e48270 commit f069641
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 5 deletions.
8 changes: 6 additions & 2 deletions armi/physics/fuelCycle/assemblyRotationAlgorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,15 @@ def buReducingAssemblyRotation(fh):
runLog.info("Algorithmically rotating assemblies to minimize burnup")
numRotated = 0
for aPrev in fh.moved:
# If the assembly was out of the core, no need to rotate an assembly
# that is outside the core.
# If the assembly was out of the core, it will not have pin powers.
# No rotation information to be gained.
if aPrev.lastLocationLabel in Assembly.NOT_IN_CORE:
continue
aNow = fh.r.core.getAssemblyWithStringLocation(aPrev.lastLocationLabel)
# An assembly in the SFP could have burnup but if it's coming from the load
# queue it's totally fresh. Skip a check over all pins in the model
if aNow.lastLocationLabel == Assembly.LOAD_QUEUE:
continue
# no point in rotation if there's no pin detail
if assemblyHasFuelPinPowers(aPrev) and assemblyHasFuelPinBurnup(aNow):
_rotateByComparingLocations(aNow, aPrev)
Expand Down
56 changes: 55 additions & 1 deletion armi/physics/fuelCycle/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from armi.reactor.blocks import Block
from armi.reactor.components import Circle
from armi.reactor.flags import Flags
from armi.reactor.grids import IndexLocation
from armi.reactor.grids import IndexLocation, MultiIndexLocation
from armi.physics.fuelCycle import utils


Expand Down Expand Up @@ -54,6 +54,35 @@ def test_maxBurnupPinLocationBlockParameter(self):
actual = utils.maxBurnupFuelPinLocation(self.block)
self.assertIs(actual, expected)

def test_maxBurnupLocationFromComponents(self):
"""Test that the ``Component.p.pinPercentBu`` parameter can reveal max burnup location."""
self.fuel.spatialLocator = MultiIndexLocation(None)
locations = []
for i in range(self.N_PINS):
loc = IndexLocation(i, 0, 0, None)
self.fuel.spatialLocator.append(loc)
locations.append(loc)
self.fuel.p.pinPercentBu = np.ones(self.N_PINS, dtype=float)

# Pick an arbitrary index for the pin with the most burnup
maxBuIndex = self.N_PINS // 3
self.fuel.p.pinPercentBu[maxBuIndex] *= 2
expectedLoc = locations[maxBuIndex]
actual = utils.maxBurnupFuelPinLocation(self.block)
self.assertEqual(actual, expectedLoc)

def test_singleLocatorWithBurnup(self):
"""Test that a single component with burnup can be used to find the highest burnup."""
freeComp = Circle(
"free fuel", material="UO2", Tinput=200, Thot=200, id=0, od=1, mult=1
)
freeComp.spatialLocator = IndexLocation(2, 4, 0, None)
freeComp.p.pinPercentBu = [
0.01,
]
loc = utils.getMaxBurnupLocationFromChildren([freeComp])
self.assertIs(loc, freeComp.spatialLocator)

def test_assemblyHasPinPower(self):
"""Test the ability to check if an assembly has fuel pin powers."""
fakeAssem = [self.block]
Expand All @@ -71,3 +100,28 @@ def test_assemblyHasPinPower(self):
# Yes fuel blocks, yes pin power assigned but all zeros => no pin powers
self.block.p.linPowByPin = np.zeros(self.N_PINS, dtype=float)
self.assertFalse(utils.assemblyHasFuelPinPowers(fakeAssem))

def test_assemblyHasPinBurnups(self):
"""Test the ability to check if an assembly has fuel pin burnup."""
fakeAssem = [self.block]
# No fuel components => no assembly burnups
self.assertFalse(self.block.getChildrenWithFlags(Flags.FUEL))
self.assertFalse(utils.assemblyHasFuelPinBurnup(fakeAssem))
# No fuel with burnup => no assembly burnups
self.block.p.flags |= Flags.FUEL
self.fuel.p.flags |= Flags.FUEL
self.assertFalse(utils.assemblyHasFuelPinBurnup(fakeAssem))
# Fuel pin has burnup => yes assembly burnup
self.fuel.p.pinPercentBu = np.arange(self.N_PINS, dtype=float)
self.assertTrue(utils.assemblyHasFuelPinBurnup(fakeAssem))
# Fuel pin has empty burnup => no assembly burnup
self.fuel.p.pinPercentBu = np.zeros(self.N_PINS)
self.assertFalse(utils.assemblyHasFuelPinBurnup(fakeAssem))
# Yes burnup but no fuel flags => no assembly burnup
self.fuel.p.flags ^= Flags.FUEL
self.assertFalse(self.fuel.hasFlags(Flags.FUEL))
self.fuel.p.pinPercentBu = np.arange(self.N_PINS, dtype=float)
self.assertFalse(utils.assemblyHasFuelPinBurnup(fakeAssem))
# No pin component burnup, but a pin burnup location parameter => yes assembly burnup
self.block.p.percentBuMaxPinLocation = 3
self.assertTrue(utils.assemblyHasFuelPinBurnup(fakeAssem))
81 changes: 79 additions & 2 deletions armi/physics/fuelCycle/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@
# limitations under the License.
"""Geometric agnostic routines that are useful for fuel cycle analysis."""

import contextlib
import typing

import numpy as np

from armi.reactor.flags import Flags
from armi.reactor.grids import IndexLocation
from armi.reactor.grids import IndexLocation, MultiIndexLocation

if typing.TYPE_CHECKING:
from armi.reactor.components import Component
from armi.reactor.blocks import Block


Expand Down Expand Up @@ -62,11 +64,24 @@ def assemblyHasFuelPinBurnup(a: typing.Iterable["Block"]) -> bool:
bool
If a block with pin burnup was found.
Notes
-----
Checks two parameters on a fuel block to determine if there is burnup:
1. ``Block.p.percentBuMaxPinLocation``, or
2. ``Component.p.pinPercentBu`` on a fuel component in the block.
"""
# Avoid using Assembly.getChildrenWithFlags(Flags.FUEL)
# because that creates an entire list where we may just need the first
# fuel block. Same for avoiding Block.getChildrenWithFlags.
return any(b.hasFlags(Flags.FUEL) and b.p.percentBuMaxPinLocation for b in a)
return any(
b.hasFlags(Flags.FUEL)
and (
any(c.hasFlags(Flags.FUEL) and np.any(c.p.pinPercentBu) for c in b)
or b.p.percentBuMaxPinLocation
)
for b in a
)


def maxBurnupFuelPinLocation(b: "Block") -> IndexLocation:
Expand All @@ -82,7 +97,18 @@ def maxBurnupFuelPinLocation(b: "Block") -> IndexLocation:
IndexLocation
The spatial location in the block corresponding to the pin with the
highest burnup.
See Also
--------
* :func:`getMaxBurnupLocationFromChildren` looks just at the children of this
block, e.g., looking at pins. This function also looks at the block parameter
``Block.p.percentBuMaxPinLocation`` in case the max burnup location cannot be
determined from the child pins.
"""
# If we can't find any burnup from the children, that's okay. We have
# another way to find the max burnup location.
with contextlib.suppress(ValueError):
return getMaxBurnupLocationFromChildren(b)
# Should be an integer, that's what the description says. But a couple places
# set it to a float like 1.0 so it's still int-like but not something we can slice
buMaxPinNumber = int(b.p.percentBuMaxPinLocation)
Expand All @@ -93,3 +119,54 @@ def maxBurnupFuelPinLocation(b: "Block") -> IndexLocation:
# and can be found at ``maxBuBlock.getPinLocations()[pinNumber - 1]``
maxBuPinLocation = pinLocations[buMaxPinNumber - 1]
return maxBuPinLocation


def getMaxBurnupLocationFromChildren(
children: typing.Iterable["Component"],
) -> IndexLocation:
"""Find the location of the pin with highest burnup by looking at components.
Parameters
----------
children : iterable[Component]
Iterator over children with a spatial locator and ``pinPercentBu`` parameter
Returns
-------
IndexLocation
Location of the pin with the highest burnup.
Raises
------
ValueError
If no children have burnup, or the burnup and locators differ.
See Also
--------
* :func:`maxBurnupFuelPinLocation` uses this. You should use that method more generally,
unless you **know** you will always have ``Component.p.pinPercentBu`` defined.
"""
maxBu = 0
maxLocation = None
withBurnupAndLocs = filter(
lambda c: c.spatialLocator is not None and c.p.pinPercentBu is not None,
children,
)
for child in withBurnupAndLocs:
pinBu = child.p.pinPercentBu
if isinstance(child.spatialLocator, MultiIndexLocation):
locations = child.spatialLocator
else:
locations = [child.spatialLocator]
if len(locations) != pinBu.size:
raise ValueError(
f"Pin burnup and pin locations on {child} differ: {locations=} :: {pinBu=}"
)
myMaxIX = pinBu.argmax()
myMaxBu = pinBu[myMaxIX]
if myMaxBu > maxBu:
maxBu = myMaxBu
maxLocation = locations[myMaxIX]
if maxLocation is not None:
return maxLocation
raise ValueError("No burnups found!")

0 comments on commit f069641

Please sign in to comment.