|
19 | 19 | -----
|
20 | 20 | We are keeping these in ARMI even if they appear unused internally.
|
21 | 21 | """
|
| 22 | +import math |
| 23 | +import typing |
| 24 | + |
22 | 25 | import numpy as np
|
23 | 26 |
|
24 | 27 | 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 |
27 | 29 | from armi.utils.mathematics import findClosest
|
28 | 30 |
|
| 31 | +if typing.TYPE_CHECKING: |
| 32 | + from armi.reactor.assemblies import HexAssembly |
| 33 | + |
29 | 34 |
|
30 |
| -def getOptimalAssemblyOrientation(a, aPrev): |
| 35 | +def getOptimalAssemblyOrientation(a: "HexAssembly", aPrev: "HexAssembly") -> int: |
31 | 36 | """
|
32 |
| - Get optimal assembly orientation/rotation to minimize peak burnup. |
| 37 | + Get optimal hex assembly orientation/rotation to minimize peak burnup. |
33 | 38 |
|
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 |
40 | 42 |
|
41 | 43 | Parameters
|
42 | 44 | ----------
|
43 | 45 | a : Assembly object
|
44 | 46 | The assembly that is being rotated.
|
45 |
| -
|
46 | 47 | aPrev : Assembly object
|
47 | 48 | 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``. |
56 | 50 |
|
57 | 51 | Returns
|
58 | 52 | -------
|
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 |
64 | 56 |
|
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``. |
69 | 62 |
|
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``. |
73 | 89 | """
|
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)] |
78 | 102 | 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 |
124 | 129 |
|
125 | 130 |
|
126 | 131 | def buildRingSchedule(
|
|
0 commit comments