Source code for armi.physics.fuelCycle.utils

# 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.
"""Geometric agnostic routines that are useful for fuel cycle analysis on pin-type reactors."""

import operator
import typing

import numpy as np

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

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


[docs]def assemblyHasFuelPinPowers(a: typing.Iterable["Block"]) -> bool: """Determine if an assembly has pin powers. These are necessary for determining rotation and may or may not be present on all assemblies. Parameters ---------- a : Assembly Assembly in question Returns ------- bool If at least one fuel block in the assembly has pin powers. """ # Avoid using Assembly.getChildrenWithFlags(Flags.FUEL) # because that creates an entire list where we may just need the first # fuel block fuelBlocks = filter(lambda b: b.hasFlags(Flags.FUEL), a) return any(b.hasFlags(Flags.FUEL) and np.any(b.p.linPowByPin) for b in fuelBlocks)
[docs]def assemblyHasFuelPinBurnup(a: typing.Iterable["Block"]) -> bool: """Determine if an assembly has pin burnups. These are necessary for determining rotation and may or may not be present on all assemblies. Parameters ---------- a : Assembly Assembly in question Returns ------- bool If a block with pin burnup was found. Notes ----- Checks if any `Component.p.pinPercentBu`` is set and contains non-zero data 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. hasFuelFlags = lambda o: o.hasFlags(Flags.FUEL) for b in filter(hasFuelFlags, a): for c in filter(hasFuelFlags, b): if np.any(c.p.pinPercentBu): return True return False
[docs]def maxBurnupLocator( 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. """ 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 (n={len(locations)}) and pin locations (n={pinBu.size}) " f"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!")
[docs]def maxBurnupBlock(a: typing.Iterable["Block"]) -> "Block": """Find the block that contains the pin with the highest burnup.""" buGetter = operator.attrgetter("p.percentBuPeak") # Discard any blocks with zero burnup blocksWithBurnup = filter(buGetter, a) try: return max(blocksWithBurnup, key=buGetter) except Exception as ee: msg = f"Error finding max burnup block from {a}" runLog.error(msg) raise ValueError(msg) from ee