Skip to content

Commit 7ef5651

Browse files
Improving getOptimalAssemblyOrientation (#2019)
Improving getOptimalAssemblyOrientation The algorithm goes as follows: 1. Get all the pin powers and `IndexLocation`s from the block at the previous location 2. Obtain the `IndexLocation` of the pin with the highest burnup 3. For each possible rotation, - Find the new location with `HexGrid.rotateIndex` - Find the index where that location occurs in previous locations - Find the previous power at that location 4. Return the rotation with the lowest previous power This algorithm assumes a few things: 1. `len(pinLocations) == len(pinPowers)` in both cases. This may make sense, but we've found some cases where this assumption breaks. Not even edge cases, like the C5G7 LWR benchmark. 2. Your assembly has at least 60 degree symmetry of fuel pins and powers. This means if we find a fuel pin with high burnup and rotate it 60 degrees, there should be another fuel pin at that lattice site. This is mostly a safe assumption since many hexagonal reactors have at least 60 degree symmetry of fuel pin layout. This assumption holds if you have a full hexagonal lattice of fuel pins as well. --------- Co-authored-by: John Stilley <[email protected]>
1 parent 8b9a693 commit 7ef5651

File tree

8 files changed

+743
-138
lines changed

8 files changed

+743
-138
lines changed

armi/physics/fuelCycle/assemblyRotationAlgorithms.py

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,18 @@
2222
.. warning:: Nothing should go in this file, but rotation algorithms.
2323
"""
2424
import math
25+
from collections import defaultdict
2526

2627
from armi import runLog
2728
from armi.physics.fuelCycle.hexAssemblyFuelMgmtUtils import (
2829
getOptimalAssemblyOrientation,
2930
)
3031
from armi.physics.fuelCycle.settings import CONF_ASSEM_ROTATION_STATIONARY
32+
from armi.physics.fuelCycle.utils import (
33+
assemblyHasFuelPinBurnup,
34+
assemblyHasFuelPinPowers,
35+
)
36+
from armi.reactor.assemblies import Assembly
3137

3238

3339
def _rotationNumberToRadians(rot: int) -> float:
@@ -49,42 +55,47 @@ def buReducingAssemblyRotation(fh):
4955
simpleAssemblyRotation : an alternative rotation algorithm
5056
"""
5157
runLog.info("Algorithmically rotating assemblies to minimize burnup")
52-
numRotated = 0
53-
hist = fh.o.getInterface("history")
54-
for aPrev in fh.moved: # much more convenient to loop through aPrev first
58+
# Store how we should rotate each assembly but don't perform the rotation just yet
59+
# Consider assembly A is shuffled to a new location and rotated.
60+
# Now, assembly B is shuffled to where assembly A used to be. We need to consider the
61+
# power profile of A prior to it's rotation to understand the power profile B may see.
62+
rotations: dict[int, list[Assembly]] = defaultdict(list)
63+
for aPrev in fh.moved:
64+
# If the assembly was out of the core, it will not have pin powers.
65+
# No rotation information to be gained.
66+
if aPrev.lastLocationLabel in Assembly.NOT_IN_CORE:
67+
continue
5568
aNow = fh.r.core.getAssemblyWithStringLocation(aPrev.lastLocationLabel)
69+
# An assembly in the SFP could have burnup but if it's coming from the load
70+
# queue it's totally fresh. Skip a check over all pins in the model
71+
if aNow.lastLocationLabel == Assembly.LOAD_QUEUE:
72+
continue
5673
# no point in rotation if there's no pin detail
57-
if aNow in hist.getDetailAssemblies():
58-
_rotateByComparingLocations(aNow, aPrev)
59-
numRotated += 1
74+
if assemblyHasFuelPinPowers(aPrev) and assemblyHasFuelPinBurnup(aNow):
75+
rot = getOptimalAssemblyOrientation(aNow, aPrev)
76+
rotations[rot].append(aNow)
6077

6178
if fh.cs[CONF_ASSEM_ROTATION_STATIONARY]:
62-
for a in hist.getDetailAssemblies():
63-
if a not in fh.moved:
64-
_rotateByComparingLocations(a, a)
65-
numRotated += 1
66-
67-
runLog.info("Rotated {0} assemblies".format(numRotated))
68-
69-
70-
def _rotateByComparingLocations(aNow, aPrev):
71-
"""Rotate an assembly based on its previous location.
72-
73-
Parameters
74-
----------
75-
aNow : Assembly
76-
Assembly to be rotated
77-
aPrev : Assembly
78-
Assembly that previously occupied the location of this assembly.
79-
If ``aNow`` has not been moved, this should be ``aNow``
80-
"""
81-
rot = getOptimalAssemblyOrientation(aNow, aPrev)
82-
radians = _rotationNumberToRadians(rot)
83-
aNow.rotate(radians)
84-
(ring, pos) = aNow.spatialLocator.getRingPos()
85-
runLog.important(
86-
"Rotating Assembly ({0},{1}) to Orientation {2}".format(ring, pos, rot)
87-
)
79+
for a in filter(
80+
lambda asm: asm not in fh.moved
81+
and assemblyHasFuelPinPowers(asm)
82+
and assemblyHasFuelPinBurnup(asm),
83+
fh.r.core,
84+
):
85+
rot = getOptimalAssemblyOrientation(a, a)
86+
rotations[rot].append(a)
87+
88+
nRotations = 0
89+
for rot, assems in filter(lambda item: item[0], rotations.items()):
90+
# Radians used for the actual rotation. But a neater degrees print out is nice for logs
91+
radians = _rotationNumberToRadians(rot)
92+
degrees = round(math.degrees(radians), 3)
93+
for a in assems:
94+
runLog.important(f"Rotating assembly {a} {degrees} CCW.")
95+
a.rotate(radians)
96+
nRotations += 1
97+
98+
runLog.info(f"Rotated {nRotations} assemblies.")
8899

89100

90101
def simpleAssemblyRotation(fh):

armi/physics/fuelCycle/fuelHandlers.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
This module also handles repeat shuffles when doing a restart.
2626
"""
2727
# ruff: noqa: F401
28+
import inspect
2829
import os
2930
import re
30-
import warnings
3131

3232
import numpy as np
3333

@@ -114,10 +114,7 @@ def outage(self, factor=1.0):
114114
# The user can choose the algorithm method name directly in the settings
115115
if hasattr(rotAlgos, self.cs[CONF_ASSEMBLY_ROTATION_ALG]):
116116
rotationMethod = getattr(rotAlgos, self.cs[CONF_ASSEMBLY_ROTATION_ALG])
117-
try:
118-
rotationMethod()
119-
except TypeError:
120-
rotationMethod(self)
117+
rotationMethod(self)
121118
else:
122119
raise RuntimeError(
123120
"FuelHandler {0} does not have a rotation algorithm called {1}.\n"

armi/physics/fuelCycle/hexAssemblyFuelMgmtUtils.py

Lines changed: 85 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -19,108 +19,113 @@
1919
-----
2020
We are keeping these in ARMI even if they appear unused internally.
2121
"""
22+
import math
23+
import typing
24+
2225
import numpy as np
2326

2427
from armi import runLog
25-
from armi.reactor.flags import Flags
26-
from armi.utils.hexagon import getIndexOfRotatedCell
28+
from armi.physics.fuelCycle.utils import maxBurnupBlock, maxBurnupLocator
2729
from armi.utils.mathematics import findClosest
2830

31+
if typing.TYPE_CHECKING:
32+
from armi.reactor.assemblies import HexAssembly
33+
2934

30-
def getOptimalAssemblyOrientation(a, aPrev):
35+
def getOptimalAssemblyOrientation(a: "HexAssembly", aPrev: "HexAssembly") -> int:
3136
"""
32-
Get optimal assembly orientation/rotation to minimize peak burnup.
37+
Get optimal hex assembly orientation/rotation to minimize peak burnup.
3338
34-
Notes
35-
-----
36-
Works by placing the highest-BU pin in the location (of 6 possible locations) with lowest
37-
expected pin power. We evaluated "expected pin power" based on the power distribution in
38-
aPrev, which is the previous assembly located here. If aPrev has no pin detail, then we must use its
39-
corner fast fluxes to make an estimate.
39+
.. impl:: Provide an algorithm for rotating hexagonal assemblies to equalize burnup
40+
:id: I_ARMI_ROTATE_HEX_BURNUP
41+
:implements: R_ARMI_ROTATE_HEX_BURNUP
4042
4143
Parameters
4244
----------
4345
a : Assembly object
4446
The assembly that is being rotated.
45-
4647
aPrev : Assembly object
4748
The assembly that previously occupied this location (before the last shuffle).
48-
49-
If the assembly "a" was not shuffled, then "aPrev" = "a".
50-
51-
If "aPrev" has pin detail, then we will determine the orientation of "a" based on
52-
the pin powers of "aPrev" when it was located here.
53-
54-
If "aPrev" does NOT have pin detail, then we will determine the orientation of "a" based on
55-
the corner fast fluxes in "aPrev" when it was located here.
49+
If the assembly "a" was not shuffled, it's sufficient to pass ``a``.
5650
5751
Returns
5852
-------
59-
rot : int
60-
An integer from 0 to 5 representing the "orientation" of the assembly.
61-
This orientation is relative to the current assembly orientation.
62-
rot = 0 corresponds to no rotation.
63-
rot represents the number of pi/3 counterclockwise rotations for the default orientation.
53+
int
54+
An integer from 0 to 5 representing the number of pi/3 (60 degree) counterclockwise
55+
rotations from where ``a`` is currently oriented to the "optimal" orientation
6456
65-
Examples
66-
--------
67-
>>> getOptimalAssemblyOrientation(a, aPrev)
68-
4
57+
Raises
58+
------
59+
ValueError
60+
If there is insufficient information to determine the rotation of ``a``. This could
61+
be due to a lack of fuel blocks or parameters like ``linPowByPin``.
6962
70-
See Also
71-
--------
72-
rotateAssemblies : calls this to figure out how to rotate
63+
Notes
64+
-----
65+
Works by placing the highest-burnup pin in the location (of 6 possible locations) with lowest
66+
expected pin power. We evaluated "expected pin power" based on the power distribution in
67+
``aPrev``, the previous assembly located where ``a`` is going. The algorithm goes as follows.
68+
69+
1. Get all the pin powers and ``IndexLocation``s from the block at the previous location/timenode.
70+
2. Obtain the ``IndexLocation`` of the pin with the highest burnup in the current assembly.
71+
3. For each possible rotation,
72+
- Find the new location with ``HexGrid.rotateIndex``
73+
- Find the index where that location occurs in previous locations
74+
- Find the previous power at that location
75+
4. Return the rotation with the lowest previous power
76+
77+
This algorithm assumes a few things.
78+
79+
1. ``len(HexBlock.getPinCoordinates()) == len(HexBlock.p.linPowByPin)`` and,
80+
by extension, ``linPowByPin[i]`` is found at ``getPinCoordinates()[i]``.
81+
2. Your assembly has at least 60 degree symmetry of fuel pins and
82+
powers. This means if we find a fuel pin and rotate it 60 degrees, there should
83+
be another fuel pin at that lattice site. This is mostly a safe assumption
84+
since many hexagonal reactors have at least 60 degree symmetry of fuel pin layout.
85+
This assumption holds if you have a full hexagonal lattice of fuel pins as well.
86+
3. Fuel pins in ``a`` have similar locations in ``aPrev``. This is mostly a safe
87+
assumption in that most fuel assemblies have similar layouts so it's plausible
88+
that if ``a`` has a fuel pin at ``(1, 0, 0)``, so does ``aPrev``.
7389
"""
74-
# determine whether or not aPrev had pin details
75-
fuelPrev = aPrev.getFirstBlock(Flags.FUEL)
76-
if fuelPrev:
77-
aPrevDetailFlag = fuelPrev.p.pinLocation[4] is not None
90+
maxBuBlock = maxBurnupBlock(a)
91+
if maxBuBlock.spatialGrid is None:
92+
msg = f"Block {maxBuBlock} in {a} does not have a spatial grid. Cannot rotate."
93+
runLog.error(msg)
94+
raise ValueError(msg)
95+
maxBuPinLocation = maxBurnupLocator(maxBuBlock)
96+
# No need to rotate if max burnup pin is the center
97+
if maxBuPinLocation.i == 0 and maxBuPinLocation.j == 0:
98+
return 0
99+
100+
if aPrev is not a:
101+
blockAtPreviousLocation = aPrev[a.index(maxBuBlock)]
78102
else:
79-
aPrevDetailFlag = False
80-
81-
rot = 0 # default: no rotation
82-
# First get pin index of maximum BU in this assembly.
83-
_maxBuAssem, maxBuBlock = a.getMaxParam("percentBuMax", returnObj=True)
84-
if maxBuBlock is None:
85-
# no max block. They're all probably zero
86-
return rot
87-
88-
# start at 0 instead of 1
89-
maxBuPinIndexAssem = int(maxBuBlock.p.percentBuMaxPinLocation - 1)
90-
bIndexMaxBu = a.index(maxBuBlock)
91-
92-
if maxBuPinIndexAssem == 0:
93-
# Don't bother rotating if the highest-BU pin is the central pin. End this method.
94-
return rot
95-
else:
96-
# transfer percentBuMax rotated pin index to non-rotated pin index
97-
if aPrevDetailFlag:
98-
# aPrev has pin detail
99-
# Determine which of 6 possible rotated pin indices had the lowest power when aPrev was here.
100-
prevAssemPowHereMIN = float("inf")
101-
102-
for possibleRotation in range(6):
103-
index = getIndexOfRotatedCell(maxBuPinIndexAssem, possibleRotation)
104-
# get pin power at this index in the previously assembly located here
105-
# power previously at rotated index
106-
prevAssemPowHere = aPrev[bIndexMaxBu].p.linPowByPin[index - 1]
107-
108-
if prevAssemPowHere is not None:
109-
runLog.debug(
110-
"Previous power in rotation {0} where pinLoc={1} is {2:.4E} W/cm"
111-
"".format(possibleRotation, index, prevAssemPowHere)
112-
)
113-
if prevAssemPowHere < prevAssemPowHereMIN:
114-
prevAssemPowHereMIN = prevAssemPowHere
115-
rot = possibleRotation
116-
else:
117-
raise ValueError(
118-
"Cannot perform detailed rotation analysis without pin-level "
119-
"flux information."
120-
)
121-
122-
runLog.debug("Best relative rotation is {0}".format(rot))
123-
return rot
103+
blockAtPreviousLocation = maxBuBlock
104+
105+
previousLocations = blockAtPreviousLocation.getPinLocations()
106+
previousPowers = blockAtPreviousLocation.p.linPowByPin
107+
if len(previousLocations) != len(previousPowers):
108+
msg = (
109+
f"Inconsistent pin powers and number of pins in {blockAtPreviousLocation}. "
110+
f"Found {len(previousLocations)} locations but {len(previousPowers)} powers."
111+
)
112+
runLog.error(msg)
113+
raise ValueError(msg)
114+
115+
ringPowers = {
116+
(loc.i, loc.j): p for loc, p in zip(previousLocations, previousPowers)
117+
}
118+
119+
targetGrid = blockAtPreviousLocation.spatialGrid
120+
candidateRotation = 0
121+
candidatePower = ringPowers.get((maxBuPinLocation.i, maxBuPinLocation.j), math.inf)
122+
for rot in range(1, 6):
123+
candidateLocation = targetGrid.rotateIndex(maxBuPinLocation, rot)
124+
newPower = ringPowers.get((candidateLocation.i, candidateLocation.j), math.inf)
125+
if newPower < candidatePower:
126+
candidateRotation = rot
127+
candidatePower = newPower
128+
return candidateRotation
124129

125130

126131
def buildRingSchedule(

0 commit comments

Comments
 (0)