From 8174fbe309733a52e8b9286d3c8365f49a137171 Mon Sep 17 00:00:00 2001 From: aalberti Date: Wed, 26 Jul 2023 16:02:17 -0700 Subject: [PATCH 01/26] WIP: have the pin groupings working. Now need to do something with them.... --- .../converters/axialExpansionChanger.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index 8e7f365d2..940d8caa7 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -13,6 +13,7 @@ # limitations under the License. """Enable component-wise axial expansion for assemblies and/or a reactor.""" +import collections from statistics import mean from typing import List @@ -20,6 +21,7 @@ from armi.materials import material from armi.reactor.components import UnshapedComponent from armi.reactor.flags import Flags +from armi.reactor.grids import MultiIndexLocation, HexGrid from numpy import array TARGET_FLAGS_IN_PREFERRED_ORDER = [ @@ -406,12 +408,48 @@ class AssemblyAxialLinkage: def __init__(self, StdAssem): self.a = StdAssem + self.pinnedBlocks = [] self.linkedBlocks = {} self.linkedComponents = {} self._determineAxialLinkage() def _determineAxialLinkage(self): """Gets the block and component based linkage.""" + # get the pinned blocks + self.pinnedBlocks = [b for b in self.a if b.spatialGrid] + # determine the index locations and max number of pin groupings in each pinned block + self.indexLocations = collections.defaultdict(list) + numOfPinGroupingsPerBlock = [] + for b in self.pinnedBlocks: + for c in getSolidComponents(b): + if isinstance(c.spatialLocator, MultiIndexLocation): + ringPosConfirm = [] + for index in c.spatialLocator.indices: + try: + ringPosConfirm.append( + c.spatialLocator.grid.indicesToRingPos( + index[0], index[1] + ) + ) + except AttributeError: + # autogrids have None type for spatialLocator.grid + ringPosConfirm.append( + HexGrid.indicesToRingPos(index[0], index[1]) + ) + ringPosConfirmSorted = sorted( + ringPosConfirm, key=lambda x: (x[0], x[1]) + ) + # need to determine number of pin groupings + if ringPosConfirmSorted not in self.indexLocations[b]: + self.indexLocations[b].append(ringPosConfirmSorted) + numOfPinGroupingsPerBlock.append(len(self.indexLocations[b])) + # throw an error is the len of indexLocations isn't all the same + # you need to have the same number of pin groupings throughout an assembly for the + # grid linking to work + if len(set(numOfPinGroupingsPerBlock)) != 1: + raise RuntimeError( + "There needs to be the same number of pin groupings throughout an assembly." + ) for b in self.a: self._getLinkedBlocks(b) for c in getSolidComponents(b): @@ -542,6 +580,9 @@ def _determineLinked(componentA, componentB): linked : bool status is componentA and componentB are axially linked to one another """ + # if isinstance(componentA.spatialLocator, MultiIndexLocation) and isinstance(componentB.spatialLocator, MultiIndexLocation): + # # do stuff! + # print("") if ( (componentA.containsSolidMaterial() and componentB.containsSolidMaterial()) and isinstance(componentA, type(componentB)) From 0a5b205210d8d841351f62a155d30262a4646acb Mon Sep 17 00:00:00 2001 From: aalberti Date: Thu, 27 Jul 2023 09:46:42 -0700 Subject: [PATCH 02/26] adjusting blueprints to have for block grids - axial exp changer starting to account for block grids. - works when lattices have different numbers of pins. - need to change for case when pins are the same number in the grid (this should generate linking problems) --- .../converters/axialExpansionChanger.py | 63 ++++---- .../refSmallCoreGrid.yaml | 23 +++ .../refSmallReactorBase.yaml | 153 ++++++++++++++++-- 3 files changed, 197 insertions(+), 42 deletions(-) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index 940d8caa7..66b92d5fd 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -13,7 +13,6 @@ # limitations under the License. """Enable component-wise axial expansion for assemblies and/or a reactor.""" -import collections from statistics import mean from typing import List @@ -418,42 +417,50 @@ def _determineAxialLinkage(self): # get the pinned blocks self.pinnedBlocks = [b for b in self.a if b.spatialGrid] # determine the index locations and max number of pin groupings in each pinned block - self.indexLocations = collections.defaultdict(list) + self.indexLocations = {} numOfPinGroupingsPerBlock = [] for b in self.pinnedBlocks: + numPinGroups = self._getPinGroupings(b) + # store the length of pin groupings + numOfPinGroupingsPerBlock.append(numPinGroups) + self._checkProperPinGroupings(numOfPinGroupingsPerBlock) + for b in self.a: + self._getLinkedBlocks(b) for c in getSolidComponents(b): - if isinstance(c.spatialLocator, MultiIndexLocation): - ringPosConfirm = [] - for index in c.spatialLocator.indices: - try: - ringPosConfirm.append( - c.spatialLocator.grid.indicesToRingPos( - index[0], index[1] - ) - ) - except AttributeError: - # autogrids have None type for spatialLocator.grid - ringPosConfirm.append( - HexGrid.indicesToRingPos(index[0], index[1]) - ) - ringPosConfirmSorted = sorted( - ringPosConfirm, key=lambda x: (x[0], x[1]) - ) - # need to determine number of pin groupings - if ringPosConfirmSorted not in self.indexLocations[b]: - self.indexLocations[b].append(ringPosConfirmSorted) - numOfPinGroupingsPerBlock.append(len(self.indexLocations[b])) + self._getLinkedComponents(b, c) + + def _getPinGroupings(self, b) -> float: + pinGroups = set() + for c in getSolidComponents(b): + if isinstance(c.spatialLocator, MultiIndexLocation): + ringPosConfirm = [] + for index in c.spatialLocator.indices: + try: + ringPosConfirm.append( + c.spatialLocator.grid.indicesToRingPos(index[0], index[1]) + ) + except AttributeError: + # autogrids have None type for spatialLocator.grid + ringPosConfirm.append( + HexGrid.indicesToRingPos(index[0], index[1]) + ) + ringPosConfirmSorted = tuple( + sorted(ringPosConfirm, key=lambda x: (x[0], x[1])) + ) + # store pin groupings + self.indexLocations[c] = ringPosConfirmSorted + pinGroups.add(ringPosConfirmSorted) + return len(pinGroups) + + def _checkProperPinGroupings(self, pinGroups: List): # throw an error is the len of indexLocations isn't all the same # you need to have the same number of pin groupings throughout an assembly for the # grid linking to work - if len(set(numOfPinGroupingsPerBlock)) != 1: + if len(set(pinGroups)) != 1: raise RuntimeError( "There needs to be the same number of pin groupings throughout an assembly." + f"{self.a}, {self.pinnedBlocks}, {pinGroups}" ) - for b in self.a: - self._getLinkedBlocks(b) - for c in getSolidComponents(b): - self._getLinkedComponents(b, c) def _getLinkedBlocks(self, b): """Retrieve the axial linkage for block b. diff --git a/armi/tests/detailedAxialExpansion/refSmallCoreGrid.yaml b/armi/tests/detailedAxialExpansion/refSmallCoreGrid.yaml index 4726487cd..223d4b64b 100644 --- a/armi/tests/detailedAxialExpansion/refSmallCoreGrid.yaml +++ b/armi/tests/detailedAxialExpansion/refSmallCoreGrid.yaml @@ -19,3 +19,26 @@ core: IC IC MC OC AF IC IC PC SH symmetry: third periodic + +twoPin: + # geom: hex + geom: hex_corners_up + symmetry: full + lattice map: | + - - - - - - - - 1 1 1 1 1 1 1 1 1 + - - - - - - - 1 1 1 1 1 1 1 1 1 1 + - - - - - - 1 1 1 1 1 1 1 1 1 1 1 + - - - - - 1 1 1 2 2 2 2 2 2 1 1 1 + - - - - 1 1 1 2 1 1 1 1 1 2 1 1 1 + - - - 1 1 1 2 1 1 1 1 1 1 2 1 1 1 + - - 1 1 1 2 1 1 2 2 2 1 1 2 1 1 1 + - 1 1 1 2 1 1 2 1 1 2 1 1 2 1 1 1 + 1 1 1 2 1 1 2 1 1 1 2 1 1 2 1 1 1 + 1 1 1 2 1 1 2 1 1 2 1 1 2 1 1 1 + 1 1 1 2 1 1 2 2 2 1 1 2 1 1 1 + 1 1 1 2 1 1 1 1 1 1 2 1 1 1 + 1 1 1 2 1 1 1 1 1 2 1 1 1 + 1 1 1 2 2 2 2 2 2 1 1 1 + 1 1 1 1 1 1 1 1 1 1 1 + 1 1 1 1 1 1 1 1 1 1 + 1 1 1 1 1 1 1 1 1 diff --git a/armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml b/armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml index 623b8b8b9..655d00870 100644 --- a/armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml +++ b/armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml @@ -40,7 +40,7 @@ blocks: Thot: 450.0 ip: grid.op mult: 1.0 - op: 16.75 + op: 19.0 duct: &block_duct coolant: *component_coolant @@ -49,9 +49,9 @@ blocks: material: HT9 Tinput: 25.0 Thot: 450.0 - ip: 16.0 + ip: 18.0 mult: 1.0 - op: 16.6 + op: 18.5 intercoolant: &component_intercoolant shape: Hexagon material: Sodium @@ -59,7 +59,7 @@ blocks: Thot: 450.0 ip: duct.op mult: 1.0 - op: 16.75 + op: 19.0 SodiumBlock : &block_dummy flags: dummy @@ -70,7 +70,7 @@ blocks: Thot: 450.0 ip: 0.0 mult: 1.0 - op: 16.75 + op: 19.0 ## ------------------------------------------------------------------------------------ ## fuel blocks @@ -113,31 +113,85 @@ blocks: duct: *component_duct intercoolant: *component_intercoolant - fuel: &block_fuel + axial shield 2pin: &block_fuel_axial_shield_twopin + grid name: twoPin + shield1: &component_shield_shield + shape: Circle + material: HT9 + Tinput: 25.0 + Thot: 600.0 + id: 0.0 + od: 0.86602 + latticeIDs: [1] + bond1: &component_shield_bond + shape: Circle + material: Sodium + Tinput: 25.0 + Thot: 470.0 + id: shield1.od + od: clad1.id + latticeIDs: [1] + clad1: &component_shield_clad + shape: Circle + material: HT9 + Tinput: 25.0 + Thot: 470.0 + id: 1.0 + od: 1.09 + latticeIDs: [1] + wire1: &component_shield_wire + shape: Helix + material: HT9 + Tinput: 25.0 + Thot: 470.0 + axialPitch: 30.15 + helixDiameter: 1.19056 + id: 0.0 + od: 0.10056 + latticeIDs: [1] + shield2: + <<: *component_shield_shield + latticeIDs: [2] + bond2: + <<: *component_shield_bond + latticeIDs: [2] + clad2: + <<: *component_shield_clad + latticeIDs: [2] + wire2: + <<: *component_shield_wire + latticeIDs: [2] + coolant: *component_coolant + duct: *component_duct + intercoolant: *component_intercoolant + axial expansion target component: shield1 + + fuel: &block_fuel + grid name: twoPin fuel: &component_fuel_fuel shape: Circle material: UZr Tinput: 25.0 Thot: 600.0 id: 0.0 - mult: 169.0 od: 0.86602 + latticeIDs: [1] bond: &component_fuel_bond shape: Circle material: Sodium Tinput: 25.0 Thot: 470.0 id: fuel.od - mult: fuel.mult od: clad.id + latticeIDs: [1] clad: &component_fuel_clad shape: Circle material: HT9 Tinput: 25.0 Thot: 470.0 id: 1.0 - mult: fuel.mult od: 1.09 + latticeIDs: [1] wire: &component_fuel_wire shape: Helix material: HT9 @@ -146,12 +200,26 @@ blocks: axialPitch: 30.15 helixDiameter: 1.19056 id: 0.0 - mult: fuel.mult od: 0.10056 + latticeIDs: [1] + fuel2: + <<: *component_fuel_fuel + latticeIDs: [2] + bond2: + <<: *component_fuel_bond + latticeIDs: [2] + clad2: + <<: *component_fuel_clad + latticeIDs: [2] + wire2: + <<: *component_fuel_wire + latticeIDs: [2] + coolant: *component_coolant duct: *component_duct intercoolant: *component_intercoolant - + axial expansion target component: fuel + plenum: &block_plenum gap: &component_plenum_gap shape: Circle @@ -182,14 +250,71 @@ blocks: coolant: *component_coolant duct: *component_duct intercoolant: *component_intercoolant + axial expansion target component: clad - aclp plenum : &block_aclp + aclp plenum: &block_aclp gap: *component_plenum_gap clad: *component_plenum_clad wire: *component_plenum_wire coolant: *component_coolant duct: *component_duct intercoolant: *component_intercoolant + axial expansion target component: clad + + plenum 2pin: &block_plenum_twopin + grid name: twoPin + gap: &component_plenum_gap1 + shape: Circle + material: Void + Tinput: 25.0 + Thot: 600.0 + id: 0.0 + od: clad.id + latticeIDs: [1] + clad: &component_plenum_clad1 + shape: Circle + material: HT9 + Tinput: 25.0 + Thot: 470.0 + id: 1.0 + od: 1.09 + latticeIDs: [1] + wire: &component_plenum_wire1 + shape: Helix + material: HT9 + Tinput: 25.0 + Thot: 470.0 + axialPitch: 30.15 + helixDiameter: 1.19056 + id: 0.0 + od: 0.10056 + latticeIDs: [1] + gap2: &component_plenum_gap2 + <<: *component_plenum_gap1 + latticeIDs: [2] + clad2: &component_plenum_clad2 + <<: *component_plenum_clad1 + latticeIDs: [2] + wire2: &component_plenum_wire2 + <<: *component_plenum_wire1 + latticeIDs: [2] + coolant: *component_coolant + duct: *component_duct + intercoolant: *component_intercoolant + axial expansion target component: clad + + aclp plenum 2pin: &block_aclp_twopin + grid name: twoPin + gap: *component_plenum_gap1 + clad: *component_plenum_clad1 + wire: *component_plenum_wire1 + gap2: *component_plenum_gap2 + clad2: *component_plenum_clad2 + wire2: *component_plenum_wire2 + coolant: *component_coolant + duct: *component_duct + intercoolant: *component_intercoolant + axial expansion target component: clad fuel2: &block_fuel2 fuel: @@ -353,7 +478,7 @@ blocks: Thot: 450.0 ip: duct.op mult: 1.0 - op: 16.75 + op: 19.0 moveable control: &block_control control: @@ -474,7 +599,7 @@ assemblies: axial mesh points: &standard_axial_mesh_points [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] igniter fuel: specifier: IC - blocks: &igniter_fuel_blocks [*block_grid_plate, *block_fuel_axial_shield, *block_fuel, *block_fuel, *block_fuel, *block_plenum, *block_aclp, *block_plenum, *block_duct, *block_dummy] + blocks: &igniter_fuel_blocks [*block_grid_plate, *block_fuel_axial_shield_twopin, *block_fuel, *block_fuel, *block_fuel, *block_plenum_twopin, *block_aclp_twopin, *block_plenum_twopin, *block_duct, *block_dummy] height: *highOffset_height axial mesh points: *standard_axial_mesh_points material modifications: From 02c568548d0140c0f550da7e62aa61d43369b187 Mon Sep 17 00:00:00 2001 From: aalberti Date: Thu, 27 Jul 2023 13:14:07 -0700 Subject: [PATCH 03/26] resolve multiple axial linkage w/ spatialLocator - still requires all pinned blocks to have the same number of pin groupings - but can now use the spatial locators of components to resolve multiple axial linkage --- .../converters/axialExpansionChanger.py | 42 ++++++--- .../refSmallCoreGrid.yaml | 6 +- .../refSmallReactorBase.yaml | 89 +++++++++++++++++-- 3 files changed, 114 insertions(+), 23 deletions(-) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index 66b92d5fd..636fd3bb2 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -539,14 +539,10 @@ def _getLinkedComponents(self, b, c): for otherC in getSolidComponents(linkdBlk.getChildren()): if _determineLinked(c, otherC): if lstLinkedC[ib] is not None: - errMsg = ( - "Multiple component axial linkages have been found for " - f"Component {c}; Block {b}; Assembly {b.parent}." - " This is indicative of an error in the blueprints! Linked components found are" - f"{lstLinkedC[ib]} and {otherC}" + lstLinkedC[ib] = _resolveMultipleLinkage( + c, otherC, lstLinkedC[ib] ) - runLog.error(msg=errMsg) - raise RuntimeError(errMsg) + continue lstLinkedC[ib] = otherC self.linkedComponents[c] = lstLinkedC @@ -563,6 +559,32 @@ def _getLinkedComponents(self, b, c): ) +def _resolveMultipleLinkage(primary, candidate1, candidate2): + """Use c.spatialLocator.indices to determine the proper linkage with primary.""" + priIndices: List[int] = primary.spatialLocator.indices[0] + cand1Indices: List[int] = candidate1.spatialLocator.indices[0] + cand2Indices: List[int] = candidate2.spatialLocator.indices[0] + chooseC1: bool = False + chooseC2: bool = False + if (priIndices == cand1Indices).all(): + chooseC1 = True + if (priIndices == cand2Indices).all(): + chooseC2 = True + if (chooseC1 and chooseC2) or (chooseC1 is False and chooseC2 is False): + # if both True, candidate1 and candidate2 are in the same grid location (unphysical) + # if both false, candidate1 and candidate2 are not in the correct grid location (linking is impossible) + errMsg = ( + "Multiple component axial linkages have been found for\n" + f"Component {primary}\nBlock {primary.parent}\nAssembly {primary.parent.parent}.\n" + "This is indicative of an error in the blueprints and the correct use of block " + "grids should resolve this issue. Candidate components for linking:\n" + f"Primary: {primary}\nCandidates: {candidate1}, {candidate2}" + ) + runLog.error(msg=errMsg) + raise RuntimeError(errMsg) + return candidate1 if chooseC1 else candidate2 + + def _determineLinked(componentA, componentB): """Determine axial component linkage for two components. @@ -590,10 +612,8 @@ def _determineLinked(componentA, componentB): # if isinstance(componentA.spatialLocator, MultiIndexLocation) and isinstance(componentB.spatialLocator, MultiIndexLocation): # # do stuff! # print("") - if ( - (componentA.containsSolidMaterial() and componentB.containsSolidMaterial()) - and isinstance(componentA, type(componentB)) - and (componentA.getDimension("mult") == componentB.getDimension("mult")) + if isinstance(componentA, type(componentB)) and ( + componentA.getDimension("mult") == componentB.getDimension("mult") ): if isinstance(componentA, UnshapedComponent): runLog.warning( diff --git a/armi/tests/detailedAxialExpansion/refSmallCoreGrid.yaml b/armi/tests/detailedAxialExpansion/refSmallCoreGrid.yaml index 223d4b64b..59ff48b44 100644 --- a/armi/tests/detailedAxialExpansion/refSmallCoreGrid.yaml +++ b/armi/tests/detailedAxialExpansion/refSmallCoreGrid.yaml @@ -20,14 +20,14 @@ core: AF IC IC PC SH symmetry: third periodic -twoPin: +fourPin: # geom: hex geom: hex_corners_up symmetry: full lattice map: | - - - - - - - - 1 1 1 1 1 1 1 1 1 - - - - - - - 1 1 1 1 1 1 1 1 1 1 - - - - - - - 1 1 1 1 1 1 1 1 1 1 1 + - - - - - - 1 3 1 1 1 1 1 1 1 1 1 - - - - - 1 1 1 2 2 2 2 2 2 1 1 1 - - - - 1 1 1 2 1 1 1 1 1 2 1 1 1 - - - 1 1 1 2 1 1 1 1 1 1 2 1 1 1 @@ -40,5 +40,5 @@ twoPin: 1 1 1 2 1 1 1 1 1 2 1 1 1 1 1 1 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 - 1 1 1 1 1 1 1 1 1 1 + 1 1 1 1 1 1 1 4 1 1 1 1 1 1 1 1 1 1 1 diff --git a/armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml b/armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml index 655d00870..42f52c488 100644 --- a/armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml +++ b/armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml @@ -113,8 +113,8 @@ blocks: duct: *component_duct intercoolant: *component_intercoolant - axial shield 2pin: &block_fuel_axial_shield_twopin - grid name: twoPin + axial shield 4pin: &block_fuel_axial_shield_fourPin + grid name: fourPin shield1: &component_shield_shield shape: Circle material: HT9 @@ -161,13 +161,37 @@ blocks: wire2: <<: *component_shield_wire latticeIDs: [2] + shield3: + <<: *component_shield_shield + latticeIDs: [3] + bond3: + <<: *component_shield_bond + latticeIDs: [3] + clad3: + <<: *component_shield_clad + latticeIDs: [3] + wire3: + <<: *component_shield_wire + latticeIDs: [3] + shield4: + <<: *component_shield_shield + latticeIDs: [4] + bond4: + <<: *component_shield_bond + latticeIDs: [4] + clad4: + <<: *component_shield_clad + latticeIDs: [4] + wire4: + <<: *component_shield_wire + latticeIDs: [4] coolant: *component_coolant duct: *component_duct intercoolant: *component_intercoolant axial expansion target component: shield1 fuel: &block_fuel - grid name: twoPin + grid name: fourPin fuel: &component_fuel_fuel shape: Circle material: UZr @@ -214,7 +238,30 @@ blocks: wire2: <<: *component_fuel_wire latticeIDs: [2] - + fuel3: + <<: *component_fuel_fuel + latticeIDs: [3] + bond3: + <<: *component_fuel_bond + latticeIDs: [3] + clad3: + <<: *component_fuel_clad + latticeIDs: [3] + wire3: + <<: *component_fuel_wire + latticeIDs: [3] + fuel4: + <<: *component_fuel_fuel + latticeIDs: [4] + bond4: + <<: *component_fuel_bond + latticeIDs: [4] + clad4: + <<: *component_fuel_clad + latticeIDs: [4] + wire4: + <<: *component_fuel_wire + latticeIDs: [4] coolant: *component_coolant duct: *component_duct intercoolant: *component_intercoolant @@ -261,8 +308,8 @@ blocks: intercoolant: *component_intercoolant axial expansion target component: clad - plenum 2pin: &block_plenum_twopin - grid name: twoPin + plenum 4pin: &block_plenum_fourPin + grid name: fourPin gap: &component_plenum_gap1 shape: Circle material: Void @@ -298,19 +345,43 @@ blocks: wire2: &component_plenum_wire2 <<: *component_plenum_wire1 latticeIDs: [2] + gap3: &component_plenum_gap3 + <<: *component_plenum_gap1 + latticeIDs: [3] + clad3: &component_plenum_clad3 + <<: *component_plenum_clad1 + latticeIDs: [3] + wire3: &component_plenum_wire3 + <<: *component_plenum_wire1 + latticeIDs: [3] + gap4: &component_plenum_gap4 + <<: *component_plenum_gap1 + latticeIDs: [4] + clad4: &component_plenum_clad4 + <<: *component_plenum_clad1 + latticeIDs: [4] + wire4: &component_plenum_wire4 + <<: *component_plenum_wire1 + latticeIDs: [4] coolant: *component_coolant duct: *component_duct intercoolant: *component_intercoolant axial expansion target component: clad - aclp plenum 2pin: &block_aclp_twopin - grid name: twoPin + aclp plenum 4pin: &block_aclp_fourPin + grid name: fourPin gap: *component_plenum_gap1 clad: *component_plenum_clad1 wire: *component_plenum_wire1 gap2: *component_plenum_gap2 clad2: *component_plenum_clad2 wire2: *component_plenum_wire2 + gap3: *component_plenum_gap3 + clad3: *component_plenum_clad3 + wire3: *component_plenum_wire3 + gap4: *component_plenum_gap4 + clad4: *component_plenum_clad4 + wire4: *component_plenum_wire4 coolant: *component_coolant duct: *component_duct intercoolant: *component_intercoolant @@ -599,7 +670,7 @@ assemblies: axial mesh points: &standard_axial_mesh_points [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] igniter fuel: specifier: IC - blocks: &igniter_fuel_blocks [*block_grid_plate, *block_fuel_axial_shield_twopin, *block_fuel, *block_fuel, *block_fuel, *block_plenum_twopin, *block_aclp_twopin, *block_plenum_twopin, *block_duct, *block_dummy] + blocks: &igniter_fuel_blocks [*block_grid_plate, *block_fuel_axial_shield_fourPin, *block_fuel, *block_fuel, *block_fuel, *block_plenum_fourPin, *block_aclp_fourPin, *block_plenum_fourPin, *block_duct, *block_dummy] height: *highOffset_height axial mesh points: *standard_axial_mesh_points material modifications: From e789aa57bc64b3c81b484fc24d9c1e2847f42358 Mon Sep 17 00:00:00 2001 From: aalberti Date: Thu, 27 Jul 2023 14:23:30 -0700 Subject: [PATCH 04/26] add check to skip non-pinned assemblies --- armi/reactor/converters/axialExpansionChanger.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index 636fd3bb2..148318afa 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -452,14 +452,14 @@ def _getPinGroupings(self, b) -> float: pinGroups.add(ringPosConfirmSorted) return len(pinGroups) - def _checkProperPinGroupings(self, pinGroups: List): + def _checkProperPinGroupings(self, numOfPinGroups: List): # throw an error is the len of indexLocations isn't all the same # you need to have the same number of pin groupings throughout an assembly for the # grid linking to work - if len(set(pinGroups)) != 1: + if numOfPinGroups and len(set(numOfPinGroups)) != 1: raise RuntimeError( "There needs to be the same number of pin groupings throughout an assembly." - f"{self.a}, {self.pinnedBlocks}, {pinGroups}" + f"{self.a}, {self.pinnedBlocks}, {numOfPinGroups}" ) def _getLinkedBlocks(self, b): From cd3a1c72cdccc586f6bab3d3e0fd1e97ae9aa1c7 Mon Sep 17 00:00:00 2001 From: aalberti Date: Fri, 28 Jul 2023 16:01:06 -0700 Subject: [PATCH 05/26] remove requirement for consistent pin groupings - uses a smarter grid-based approach. Score! - needs cleanup + unit testing --- .../converters/axialExpansionChanger.py | 83 +++++---- .../tests/test_axialExpansionChanger.py | 6 +- .../refSmallReactorBase.yaml | 169 +++++++----------- 3 files changed, 117 insertions(+), 141 deletions(-) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index 148318afa..5d7239b86 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -414,16 +414,6 @@ def __init__(self, StdAssem): def _determineAxialLinkage(self): """Gets the block and component based linkage.""" - # get the pinned blocks - self.pinnedBlocks = [b for b in self.a if b.spatialGrid] - # determine the index locations and max number of pin groupings in each pinned block - self.indexLocations = {} - numOfPinGroupingsPerBlock = [] - for b in self.pinnedBlocks: - numPinGroups = self._getPinGroupings(b) - # store the length of pin groupings - numOfPinGroupingsPerBlock.append(numPinGroups) - self._checkProperPinGroupings(numOfPinGroupingsPerBlock) for b in self.a: self._getLinkedBlocks(b) for c in getSolidComponents(b): @@ -585,8 +575,8 @@ def _resolveMultipleLinkage(primary, candidate1, candidate2): return candidate1 if chooseC1 else candidate2 -def _determineLinked(componentA, componentB): - """Determine axial component linkage for two components. +def _determineLinked(componentA, componentB) -> bool: + """Determine axial component linkage for two solid components. Parameters ---------- @@ -609,11 +599,10 @@ def _determineLinked(componentA, componentB): linked : bool status is componentA and componentB are axially linked to one another """ - # if isinstance(componentA.spatialLocator, MultiIndexLocation) and isinstance(componentB.spatialLocator, MultiIndexLocation): - # # do stuff! - # print("") - if isinstance(componentA, type(componentB)) and ( - componentA.getDimension("mult") == componentB.getDimension("mult") + if ( + componentA.containsSolidMaterial() + and componentB.containsSolidMaterial() + and isinstance(componentA, type(componentB)) ): if isinstance(componentA, UnshapedComponent): runLog.warning( @@ -624,23 +613,31 @@ def _determineLinked(componentA, componentB): single=True, ) linked = False - else: - idA, odA = ( - componentA.getCircleInnerDiameter(cold=True), - componentA.getBoundingCircleOuterDiameter(cold=True), - ) - idB, odB = ( - componentB.getCircleInnerDiameter(cold=True), - componentB.getBoundingCircleOuterDiameter(cold=True), - ) - - biggerID = max(idA, idB) - smallerOD = min(odA, odB) - if biggerID >= smallerOD: - # one object fits inside the other - linked = False + elif isinstance(componentA.spatialLocator, MultiIndexLocation) and isinstance( + componentB.spatialLocator, MultiIndexLocation + ): + componentAIndices = [ + list(index) for index in componentA.spatialLocator.indices + ] + componentBIndices = [ + list(index) for index in componentB.spatialLocator.indices + ] + # check for common indices between components. If either component has indices within its counterpart, + # then they are candidates to be linked and overlap should be checked. + if len(componentAIndices) < len(componentBIndices): + if all(index in componentBIndices for index in componentAIndices): + linked = _checkOverlap(componentA, componentB) + else: + linked = False else: - linked = True + if all(index in componentAIndices for index in componentBIndices): + linked = _checkOverlap(componentA, componentB) + else: + linked = False + elif componentA.getDimension("mult") == componentB.getDimension("mult"): + linked = _checkOverlap(componentA, componentB) + else: + linked = False else: linked = False @@ -648,6 +645,26 @@ def _determineLinked(componentA, componentB): return linked +def _checkOverlap(componentA, componentB) -> bool: + idA, odA = ( + componentA.getCircleInnerDiameter(cold=True), + componentA.getBoundingCircleOuterDiameter(cold=True), + ) + idB, odB = ( + componentB.getCircleInnerDiameter(cold=True), + componentB.getBoundingCircleOuterDiameter(cold=True), + ) + biggerID = max(idA, idB) + smallerOD = min(odA, odB) + if biggerID >= smallerOD: + # one object fits inside the other + linked = False + else: + linked = True + + return linked + + class ExpansionData: """Object containing data needed for axial expansion.""" diff --git a/armi/reactor/converters/tests/test_axialExpansionChanger.py b/armi/reactor/converters/tests/test_axialExpansionChanger.py index 4ba6af5e8..a1e7cd493 100644 --- a/armi/reactor/converters/tests/test_axialExpansionChanger.py +++ b/armi/reactor/converters/tests/test_axialExpansionChanger.py @@ -893,8 +893,10 @@ def checkColdHeightBlockMass( are thermally expanded. """ # custom materials don't expand - if not isinstance(bStd.getComponent(flagType).material, custom.Custom): - self.assertGreater(bExp.getMass(nuclide), bStd.getMass(nuclide)) + compsOfInterest = bStd.getChildrenWithFlags(typeSpec=flagType) + for c in compsOfInterest: + if not isinstance(c.material, custom.Custom): + self.assertGreater(bExp.getMass(nuclide), bStd.getMass(nuclide)) def checkColdBlockHeight(bStd, bExp, assertType, strForAssertion): diff --git a/armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml b/armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml index 42f52c488..1711a56ee 100644 --- a/armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml +++ b/armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml @@ -115,31 +115,31 @@ blocks: axial shield 4pin: &block_fuel_axial_shield_fourPin grid name: fourPin - shield1: &component_shield_shield + shield: shape: Circle material: HT9 Tinput: 25.0 Thot: 600.0 id: 0.0 od: 0.86602 - latticeIDs: [1] - bond1: &component_shield_bond + latticeIDs: [1,2,3,4] + bond: shape: Circle material: Sodium Tinput: 25.0 Thot: 470.0 - id: shield1.od - od: clad1.id - latticeIDs: [1] - clad1: &component_shield_clad + id: shield.od + od: clad.id + latticeIDs: [1,2,3,4] + clad: shape: Circle material: HT9 Tinput: 25.0 Thot: 470.0 id: 1.0 od: 1.09 - latticeIDs: [1] - wire1: &component_shield_wire + latticeIDs: [1,2,3,4] + wire: shape: Helix material: HT9 Tinput: 25.0 @@ -148,51 +148,14 @@ blocks: helixDiameter: 1.19056 id: 0.0 od: 0.10056 - latticeIDs: [1] - shield2: - <<: *component_shield_shield - latticeIDs: [2] - bond2: - <<: *component_shield_bond - latticeIDs: [2] - clad2: - <<: *component_shield_clad - latticeIDs: [2] - wire2: - <<: *component_shield_wire - latticeIDs: [2] - shield3: - <<: *component_shield_shield - latticeIDs: [3] - bond3: - <<: *component_shield_bond - latticeIDs: [3] - clad3: - <<: *component_shield_clad - latticeIDs: [3] - wire3: - <<: *component_shield_wire - latticeIDs: [3] - shield4: - <<: *component_shield_shield - latticeIDs: [4] - bond4: - <<: *component_shield_bond - latticeIDs: [4] - clad4: - <<: *component_shield_clad - latticeIDs: [4] - wire4: - <<: *component_shield_wire - latticeIDs: [4] + latticeIDs: [1,2,3,4] coolant: *component_coolant duct: *component_duct intercoolant: *component_intercoolant - axial expansion target component: shield1 fuel: &block_fuel grid name: fourPin - fuel: &component_fuel_fuel + fuel: &component_fuel4pin_fuel shape: Circle material: UZr Tinput: 25.0 @@ -200,7 +163,7 @@ blocks: id: 0.0 od: 0.86602 latticeIDs: [1] - bond: &component_fuel_bond + bond: &component_fuel4pin_bond shape: Circle material: Sodium Tinput: 25.0 @@ -208,7 +171,7 @@ blocks: id: fuel.od od: clad.id latticeIDs: [1] - clad: &component_fuel_clad + clad: &component_fuel4pin_clad shape: Circle material: HT9 Tinput: 25.0 @@ -216,7 +179,7 @@ blocks: id: 1.0 od: 1.09 latticeIDs: [1] - wire: &component_fuel_wire + wire: &component_fuel4pin_wire shape: Helix material: HT9 Tinput: 25.0 @@ -227,40 +190,40 @@ blocks: od: 0.10056 latticeIDs: [1] fuel2: - <<: *component_fuel_fuel + <<: *component_fuel4pin_fuel latticeIDs: [2] bond2: - <<: *component_fuel_bond + <<: *component_fuel4pin_bond latticeIDs: [2] clad2: - <<: *component_fuel_clad + <<: *component_fuel4pin_clad latticeIDs: [2] wire2: - <<: *component_fuel_wire + <<: *component_fuel4pin_wire latticeIDs: [2] fuel3: - <<: *component_fuel_fuel + <<: *component_fuel4pin_fuel latticeIDs: [3] bond3: - <<: *component_fuel_bond + <<: *component_fuel4pin_bond latticeIDs: [3] clad3: - <<: *component_fuel_clad + <<: *component_fuel4pin_clad latticeIDs: [3] wire3: - <<: *component_fuel_wire + <<: *component_fuel4pin_wire latticeIDs: [3] fuel4: - <<: *component_fuel_fuel + <<: *component_fuel4pin_fuel latticeIDs: [4] bond4: - <<: *component_fuel_bond + <<: *component_fuel4pin_bond latticeIDs: [4] clad4: - <<: *component_fuel_clad + <<: *component_fuel4pin_clad latticeIDs: [4] wire4: - <<: *component_fuel_wire + <<: *component_fuel4pin_wire latticeIDs: [4] coolant: *component_coolant duct: *component_duct @@ -317,7 +280,7 @@ blocks: Thot: 600.0 id: 0.0 od: clad.id - latticeIDs: [1] + latticeIDs: [1,2,3,4] clad: &component_plenum_clad1 shape: Circle material: HT9 @@ -325,7 +288,7 @@ blocks: Thot: 470.0 id: 1.0 od: 1.09 - latticeIDs: [1] + latticeIDs: [1,2,3,4] wire: &component_plenum_wire1 shape: Helix material: HT9 @@ -335,34 +298,7 @@ blocks: helixDiameter: 1.19056 id: 0.0 od: 0.10056 - latticeIDs: [1] - gap2: &component_plenum_gap2 - <<: *component_plenum_gap1 - latticeIDs: [2] - clad2: &component_plenum_clad2 - <<: *component_plenum_clad1 - latticeIDs: [2] - wire2: &component_plenum_wire2 - <<: *component_plenum_wire1 - latticeIDs: [2] - gap3: &component_plenum_gap3 - <<: *component_plenum_gap1 - latticeIDs: [3] - clad3: &component_plenum_clad3 - <<: *component_plenum_clad1 - latticeIDs: [3] - wire3: &component_plenum_wire3 - <<: *component_plenum_wire1 - latticeIDs: [3] - gap4: &component_plenum_gap4 - <<: *component_plenum_gap1 - latticeIDs: [4] - clad4: &component_plenum_clad4 - <<: *component_plenum_clad1 - latticeIDs: [4] - wire4: &component_plenum_wire4 - <<: *component_plenum_wire1 - latticeIDs: [4] + latticeIDs: [1,2,3,4] coolant: *component_coolant duct: *component_duct intercoolant: *component_intercoolant @@ -373,15 +309,6 @@ blocks: gap: *component_plenum_gap1 clad: *component_plenum_clad1 wire: *component_plenum_wire1 - gap2: *component_plenum_gap2 - clad2: *component_plenum_clad2 - wire2: *component_plenum_wire2 - gap3: *component_plenum_gap3 - clad3: *component_plenum_clad3 - wire3: *component_plenum_wire3 - gap4: *component_plenum_gap4 - clad4: *component_plenum_clad4 - wire4: *component_plenum_wire4 coolant: *component_coolant duct: *component_duct intercoolant: *component_intercoolant @@ -423,15 +350,45 @@ blocks: mergeWith: clad mult: 169.0 od: 1.0 - clad: *component_fuel_clad - wire: *component_fuel_wire + clad: &component_fuel_clad + shape: Circle + material: HT9 + Tinput: 25.0 + Thot: 470.0 + id: 1.0 + od: 1.09 + mult: fuel.mult + wire: &component_fuel_wire + shape: Helix + material: HT9 + Tinput: 25.0 + Thot: 470.0 + axialPitch: 30.15 + helixDiameter: 1.19056 + id: 0.0 + od: 0.10056 + mult: fuel.mult coolant: *component_coolant duct: *component_duct intercoolant: *component_intercoolant lta fuel a: &block_lta1_fuel - fuel: *component_fuel_fuel - bond: *component_fuel_bond + fuel: + shape: Circle + material: UZr + Tinput: 25.0 + Thot: 600.0 + id: 0.0 + od: 0.86602 + mult: 169.0 + bond: &component_fuel_bond + shape: Circle + material: Sodium + Tinput: 25.0 + Thot: 470.0 + id: fuel.od + od: clad.id + mult: fuel.mult liner2: *component_fuel2_liner2 liner1: *component_fuel2_liner1 clad: *component_fuel_clad From db8bbd0facbf7e8bec9d7332466181eb176b9b32 Mon Sep 17 00:00:00 2001 From: aalberti Date: Mon, 31 Jul 2023 08:02:02 -0700 Subject: [PATCH 06/26] rm unused methods --- .../converters/axialExpansionChanger.py | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index 5d7239b86..0804c1fa7 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -419,39 +419,6 @@ def _determineAxialLinkage(self): for c in getSolidComponents(b): self._getLinkedComponents(b, c) - def _getPinGroupings(self, b) -> float: - pinGroups = set() - for c in getSolidComponents(b): - if isinstance(c.spatialLocator, MultiIndexLocation): - ringPosConfirm = [] - for index in c.spatialLocator.indices: - try: - ringPosConfirm.append( - c.spatialLocator.grid.indicesToRingPos(index[0], index[1]) - ) - except AttributeError: - # autogrids have None type for spatialLocator.grid - ringPosConfirm.append( - HexGrid.indicesToRingPos(index[0], index[1]) - ) - ringPosConfirmSorted = tuple( - sorted(ringPosConfirm, key=lambda x: (x[0], x[1])) - ) - # store pin groupings - self.indexLocations[c] = ringPosConfirmSorted - pinGroups.add(ringPosConfirmSorted) - return len(pinGroups) - - def _checkProperPinGroupings(self, numOfPinGroups: List): - # throw an error is the len of indexLocations isn't all the same - # you need to have the same number of pin groupings throughout an assembly for the - # grid linking to work - if numOfPinGroups and len(set(numOfPinGroups)) != 1: - raise RuntimeError( - "There needs to be the same number of pin groupings throughout an assembly." - f"{self.a}, {self.pinnedBlocks}, {numOfPinGroups}" - ) - def _getLinkedBlocks(self, b): """Retrieve the axial linkage for block b. From 8098453ac83b263e84587fdc7ef1db1b602f817f Mon Sep 17 00:00:00 2001 From: aalberti Date: Mon, 31 Jul 2023 08:03:30 -0700 Subject: [PATCH 07/26] rm unused instance variable --- armi/reactor/converters/axialExpansionChanger.py | 1 - 1 file changed, 1 deletion(-) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index 0804c1fa7..e6d729b5e 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -407,7 +407,6 @@ class AssemblyAxialLinkage: def __init__(self, StdAssem): self.a = StdAssem - self.pinnedBlocks = [] self.linkedBlocks = {} self.linkedComponents = {} self._determineAxialLinkage() From 1a1b34758f4957d71a90ceae6bb95552c8a0b4c5 Mon Sep 17 00:00:00 2001 From: aalberti Date: Mon, 31 Jul 2023 08:13:20 -0700 Subject: [PATCH 08/26] clean up a terrible docstring --- armi/reactor/converters/axialExpansionChanger.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index e6d729b5e..f40b0c9bb 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -390,19 +390,11 @@ class AssemblyAxialLinkage: a : :py:class:`Assembly ` reference to original assembly; is directly modified/changed during expansion. - linkedBlocks : dict - keys --> :py:class:`Block ` + linkedBlocks : dict[Block, List[Blocks]] + Keys: blocks. Values: list of axially linked blocks; index 0 = lower linked block; index 1: upper linked block. - values --> list of axially linked blocks; index 0 = lower linked block; index 1: upper linked block. - - see also: self._getLinkedBlocks() - - linkedComponents : dict - keys --> :py:class:`Component ` - - values --> list of axially linked components; index 0 = lower linked component; index 1: upper linked component. - - see also: self._getLinkedComponents + linkedComponents : dict[Component, List[Component]] + Keys: components. Values: list of axially linked components; index 0 = lower linked component; index 1: upper linked component. """ def __init__(self, StdAssem): From 115c3a9f1462b10c6e17bb20d3701bdb1dc6dd2b Mon Sep 17 00:00:00 2001 From: aalberti Date: Mon, 31 Jul 2023 08:46:16 -0700 Subject: [PATCH 09/26] retain original behavior for multiple linkage - also make it a class method to include self.a (instead of parent.parent....) --- .../converters/axialExpansionChanger.py | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index f40b0c9bb..2f74bee16 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -20,7 +20,7 @@ from armi.materials import material from armi.reactor.components import UnshapedComponent from armi.reactor.flags import Flags -from armi.reactor.grids import MultiIndexLocation, HexGrid +from armi.reactor.grids import MultiIndexLocation, CoordinateLocation from numpy import array TARGET_FLAGS_IN_PREFERRED_ORDER = [ @@ -487,7 +487,7 @@ def _getLinkedComponents(self, b, c): for otherC in getSolidComponents(linkdBlk.getChildren()): if _determineLinked(c, otherC): if lstLinkedC[ib] is not None: - lstLinkedC[ib] = _resolveMultipleLinkage( + lstLinkedC[ib] = self._resolveMultipleLinkage( c, otherC, lstLinkedC[ib] ) continue @@ -506,31 +506,38 @@ def _getLinkedComponents(self, b, c): single=True, ) - -def _resolveMultipleLinkage(primary, candidate1, candidate2): - """Use c.spatialLocator.indices to determine the proper linkage with primary.""" - priIndices: List[int] = primary.spatialLocator.indices[0] - cand1Indices: List[int] = candidate1.spatialLocator.indices[0] - cand2Indices: List[int] = candidate2.spatialLocator.indices[0] - chooseC1: bool = False - chooseC2: bool = False - if (priIndices == cand1Indices).all(): - chooseC1 = True - if (priIndices == cand2Indices).all(): - chooseC2 = True - if (chooseC1 and chooseC2) or (chooseC1 is False and chooseC2 is False): - # if both True, candidate1 and candidate2 are in the same grid location (unphysical) - # if both false, candidate1 and candidate2 are not in the correct grid location (linking is impossible) + def _resolveMultipleLinkage(self, primary, candidate1, candidate2): + """Use c.spatialLocator.indices to determine the proper linkage with primary.""" errMsg = ( "Multiple component axial linkages have been found for\n" - f"Component {primary}\nBlock {primary.parent}\nAssembly {primary.parent.parent}.\n" + f"Component {primary}\nBlock {primary.parent}\nAssembly {self.a}.\n" "This is indicative of an error in the blueprints and the correct use of block " "grids should resolve this issue. Candidate components for linking:\n" f"Primary: {primary}\nCandidates: {candidate1}, {candidate2}" ) - runLog.error(msg=errMsg) - raise RuntimeError(errMsg) - return candidate1 if chooseC1 else candidate2 + if ( + isinstance(primary.spatialLocator, CoordinateLocation) + and isinstance(candidate1.spatialLocator, CoordinateLocation) + and isinstance(candidate2.spatialLocator, CoordinateLocation) + ): + runLog.error(msg=errMsg) + raise RuntimeError(errMsg) + + priIndices: List[int] = primary.spatialLocator.indices[0] + cand1Indices: List[int] = candidate1.spatialLocator.indices[0] + cand2Indices: List[int] = candidate2.spatialLocator.indices[0] + chooseC1: bool = False + chooseC2: bool = False + if (priIndices == cand1Indices).all(): + chooseC1 = True + if (priIndices == cand2Indices).all(): + chooseC2 = True + if (chooseC1 and chooseC2) or (chooseC1 is False and chooseC2 is False): + # if both True, candidate1 and candidate2 are in the same grid location (unphysical) + # if both false, candidate1 and candidate2 are not in the correct grid location (linking is impossible) + runLog.error(msg=errMsg) + raise RuntimeError(errMsg) + return candidate1 if chooseC1 else candidate2 def _determineLinked(componentA, componentB) -> bool: From 7f4ca22ec9259f12e50e623b382287cfe49b18e2 Mon Sep 17 00:00:00 2001 From: aalberti Date: Mon, 31 Jul 2023 08:46:31 -0700 Subject: [PATCH 10/26] improve docstrings --- .../converters/axialExpansionChanger.py | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index 2f74bee16..4e0e14967 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -552,12 +552,16 @@ def _determineLinked(componentA, componentB) -> bool: Notes ----- - - Requires that shapes have the getCircleInnerDiameter and getBoundingCircleOuterDiameter defined - - For axial linkage to be True, components MUST be solids, the same Component Class, multiplicity, and meet inner - and outer diameter requirements. - - When component dimensions are retrieved, cold=True to ensure that dimensions are evaluated - at cold/input temperatures. At temperature, solid-solid interfaces in ARMI may produce - slight overlaps due to thermal expansion. Handling these potential overlaps are out of scope. + If componentA and componentB are both solids and the same type, geometric overlap can be checked via + getCircleInnerDiameter and getBoundingCircleOuterDiameter. Five different cases are accounted for. + If they do not meet these initial criteria, linkage is assumed to be False. + Case #1: Unshaped Components. There is no way to determine overlap so they're assumed to be not linked. + Case #2: Blocks with specified grids. If componentA and componentB share common grid indices (cannot be a partial + case, ALL of the indices must be contained by one or the other), then overlap can be checked. + Case #3: If Component position is not specified via a grid, the multiplicity is checked. If consistent, they are + assumed to be in the same positions and their overlap is checked. + Case #4: Cases 1-3 are not True so we assume there is no linkage. + Case #5: Components are either not both solids or are not the same type. These cannot be linked. Returns ------- @@ -570,6 +574,7 @@ def _determineLinked(componentA, componentB) -> bool: and isinstance(componentA, type(componentB)) ): if isinstance(componentA, UnshapedComponent): + ## Case 1 -- see docstring runLog.warning( f"Components {componentA} and {componentB} are UnshapedComponents " "and do not have 'getCircleInnerDiameter' or getBoundingCircleOuterDiameter methods; " @@ -581,6 +586,7 @@ def _determineLinked(componentA, componentB) -> bool: elif isinstance(componentA.spatialLocator, MultiIndexLocation) and isinstance( componentB.spatialLocator, MultiIndexLocation ): + ## Case 2 -- see docstring componentAIndices = [ list(index) for index in componentA.spatialLocator.indices ] @@ -600,17 +606,28 @@ def _determineLinked(componentA, componentB) -> bool: else: linked = False elif componentA.getDimension("mult") == componentB.getDimension("mult"): + ## Case 3 -- see docstring linked = _checkOverlap(componentA, componentB) else: + ## Case 4 -- see docstring linked = False else: + ## Case 5 -- see docstring linked = False return linked def _checkOverlap(componentA, componentB) -> bool: + """Check two components for geometric overlap. + + Notes + ----- + When component dimensions are retrieved, cold=True to ensure that dimensions are evaluated + at cold/input temperatures. At temperature, solid-solid interfaces in ARMI may produce + slight overlaps due to thermal expansion. Handling these potential overlaps are out of scope. + """ idA, odA = ( componentA.getCircleInnerDiameter(cold=True), componentA.getBoundingCircleOuterDiameter(cold=True), From cf62f02e0db64e83f5763a62198971fc5aad1761 Mon Sep 17 00:00:00 2001 From: aalberti Date: Mon, 31 Jul 2023 09:03:12 -0700 Subject: [PATCH 11/26] make determineLinked and checkOverlap static methods --- .../converters/axialExpansionChanger.py | 196 +++++++++--------- .../tests/test_axialExpansionChanger.py | 14 +- 2 files changed, 107 insertions(+), 103 deletions(-) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index 4e0e14967..c6730641b 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -485,7 +485,7 @@ def _getLinkedComponents(self, b, c): for ib, linkdBlk in enumerate(self.linkedBlocks[b]): if linkdBlk is not None: for otherC in getSolidComponents(linkdBlk.getChildren()): - if _determineLinked(c, otherC): + if AssemblyAxialLinkage._determineLinked(c, otherC): if lstLinkedC[ib] is not None: lstLinkedC[ib] = self._resolveMultipleLinkage( c, otherC, lstLinkedC[ib] @@ -539,112 +539,116 @@ def _resolveMultipleLinkage(self, primary, candidate1, candidate2): raise RuntimeError(errMsg) return candidate1 if chooseC1 else candidate2 + @staticmethod + def _determineLinked(componentA, componentB) -> bool: + """Determine axial component linkage for two solid components. -def _determineLinked(componentA, componentB) -> bool: - """Determine axial component linkage for two solid components. - - Parameters - ---------- - componentA : :py:class:`Component ` - component of interest - componentB : :py:class:`Component ` - component to compare and see if is linked to componentA + Parameters + ---------- + componentA : :py:class:`Component ` + component of interest + componentB : :py:class:`Component ` + component to compare and see if is linked to componentA - Notes - ----- - If componentA and componentB are both solids and the same type, geometric overlap can be checked via - getCircleInnerDiameter and getBoundingCircleOuterDiameter. Five different cases are accounted for. - If they do not meet these initial criteria, linkage is assumed to be False. - Case #1: Unshaped Components. There is no way to determine overlap so they're assumed to be not linked. - Case #2: Blocks with specified grids. If componentA and componentB share common grid indices (cannot be a partial - case, ALL of the indices must be contained by one or the other), then overlap can be checked. - Case #3: If Component position is not specified via a grid, the multiplicity is checked. If consistent, they are - assumed to be in the same positions and their overlap is checked. - Case #4: Cases 1-3 are not True so we assume there is no linkage. - Case #5: Components are either not both solids or are not the same type. These cannot be linked. - - Returns - ------- - linked : bool - status is componentA and componentB are axially linked to one another - """ - if ( - componentA.containsSolidMaterial() - and componentB.containsSolidMaterial() - and isinstance(componentA, type(componentB)) - ): - if isinstance(componentA, UnshapedComponent): - ## Case 1 -- see docstring - runLog.warning( - f"Components {componentA} and {componentB} are UnshapedComponents " - "and do not have 'getCircleInnerDiameter' or getBoundingCircleOuterDiameter methods; " - "nor is it physical to do so. Instead of crashing and raising an error, " - "they are going to be assumed to not be linked.", - single=True, - ) - linked = False - elif isinstance(componentA.spatialLocator, MultiIndexLocation) and isinstance( - componentB.spatialLocator, MultiIndexLocation + Notes + ----- + If componentA and componentB are both solids and the same type, geometric overlap can be checked via + getCircleInnerDiameter and getBoundingCircleOuterDiameter. Five different cases are accounted for. + If they do not meet these initial criteria, linkage is assumed to be False. + Case #1: Unshaped Components. There is no way to determine overlap so they're assumed to be not linked. + Case #2: Blocks with specified grids. If componentA and componentB share common grid indices (cannot be a partial + case, ALL of the indices must be contained by one or the other), then overlap can be checked. + Case #3: If Component position is not specified via a grid, the multiplicity is checked. If consistent, they are + assumed to be in the same positions and their overlap is checked. + Case #4: Cases 1-3 are not True so we assume there is no linkage. + Case #5: Components are either not both solids or are not the same type. These cannot be linked. + + Returns + ------- + linked : bool + status is componentA and componentB are axially linked to one another + """ + if ( + componentA.containsSolidMaterial() + and componentB.containsSolidMaterial() + and isinstance(componentA, type(componentB)) ): - ## Case 2 -- see docstring - componentAIndices = [ - list(index) for index in componentA.spatialLocator.indices - ] - componentBIndices = [ - list(index) for index in componentB.spatialLocator.indices - ] - # check for common indices between components. If either component has indices within its counterpart, - # then they are candidates to be linked and overlap should be checked. - if len(componentAIndices) < len(componentBIndices): - if all(index in componentBIndices for index in componentAIndices): - linked = _checkOverlap(componentA, componentB) + if isinstance(componentA, UnshapedComponent): + ## Case 1 -- see docstring + runLog.warning( + f"Components {componentA} and {componentB} are UnshapedComponents " + "and do not have 'getCircleInnerDiameter' or getBoundingCircleOuterDiameter methods; " + "nor is it physical to do so. Instead of crashing and raising an error, " + "they are going to be assumed to not be linked.", + single=True, + ) + linked = False + elif isinstance( + componentA.spatialLocator, MultiIndexLocation + ) and isinstance(componentB.spatialLocator, MultiIndexLocation): + ## Case 2 -- see docstring + componentAIndices = [ + list(index) for index in componentA.spatialLocator.indices + ] + componentBIndices = [ + list(index) for index in componentB.spatialLocator.indices + ] + # check for common indices between components. If either component has indices within its counterpart, + # then they are candidates to be linked and overlap should be checked. + if len(componentAIndices) < len(componentBIndices): + if all(index in componentBIndices for index in componentAIndices): + linked = AssemblyAxialLinkage._checkOverlap( + componentA, componentB + ) + else: + linked = False else: - linked = False + if all(index in componentAIndices for index in componentBIndices): + linked = AssemblyAxialLinkage._checkOverlap( + componentA, componentB + ) + else: + linked = False + elif componentA.getDimension("mult") == componentB.getDimension("mult"): + ## Case 3 -- see docstring + linked = AssemblyAxialLinkage._checkOverlap(componentA, componentB) else: - if all(index in componentAIndices for index in componentBIndices): - linked = _checkOverlap(componentA, componentB) - else: - linked = False - elif componentA.getDimension("mult") == componentB.getDimension("mult"): - ## Case 3 -- see docstring - linked = _checkOverlap(componentA, componentB) + ## Case 4 -- see docstring + linked = False + else: - ## Case 4 -- see docstring + ## Case 5 -- see docstring linked = False - else: - ## Case 5 -- see docstring - linked = False - - return linked + return linked + @staticmethod + def _checkOverlap(componentA, componentB) -> bool: + """Check two components for geometric overlap. -def _checkOverlap(componentA, componentB) -> bool: - """Check two components for geometric overlap. + Notes + ----- + When component dimensions are retrieved, cold=True to ensure that dimensions are evaluated + at cold/input temperatures. At temperature, solid-solid interfaces in ARMI may produce + slight overlaps due to thermal expansion. Handling these potential overlaps are out of scope. + """ + idA, odA = ( + componentA.getCircleInnerDiameter(cold=True), + componentA.getBoundingCircleOuterDiameter(cold=True), + ) + idB, odB = ( + componentB.getCircleInnerDiameter(cold=True), + componentB.getBoundingCircleOuterDiameter(cold=True), + ) + biggerID = max(idA, idB) + smallerOD = min(odA, odB) + if biggerID >= smallerOD: + # one object fits inside the other + linked = False + else: + linked = True - Notes - ----- - When component dimensions are retrieved, cold=True to ensure that dimensions are evaluated - at cold/input temperatures. At temperature, solid-solid interfaces in ARMI may produce - slight overlaps due to thermal expansion. Handling these potential overlaps are out of scope. - """ - idA, odA = ( - componentA.getCircleInnerDiameter(cold=True), - componentA.getBoundingCircleOuterDiameter(cold=True), - ) - idB, odB = ( - componentB.getCircleInnerDiameter(cold=True), - componentB.getBoundingCircleOuterDiameter(cold=True), - ) - biggerID = max(idA, idB) - smallerOD = min(odA, odB) - if biggerID >= smallerOD: - # one object fits inside the other - linked = False - else: - linked = True - - return linked + return linked class ExpansionData: diff --git a/armi/reactor/converters/tests/test_axialExpansionChanger.py b/armi/reactor/converters/tests/test_axialExpansionChanger.py index a1e7cd493..7cb146f3f 100644 --- a/armi/reactor/converters/tests/test_axialExpansionChanger.py +++ b/armi/reactor/converters/tests/test_axialExpansionChanger.py @@ -28,7 +28,7 @@ from armi.reactor.converters.axialExpansionChanger import ( AxialExpansionChanger, ExpansionData, - _determineLinked, + AssemblyAxialLinkage, getSolidComponents, ) from armi.reactor.flags import Flags @@ -651,7 +651,7 @@ def test_determineLinked(self): compDims = {"Tinput": 25.0, "Thot": 25.0} compA = UnshapedComponent("unshaped_1", "FakeMat", **compDims) compB = UnshapedComponent("unshaped_2", "FakeMat", **compDims) - self.assertFalse(_determineLinked(compA, compB)) + self.assertFalse(AssemblyAxialLinkage._determineLinked(compA, compB)) def test_getLinkedComponents(self): """Test for multiple component axial linkage.""" @@ -963,26 +963,26 @@ def runTest( typeB = method(*common, **dims[1]) if assertionBool: self.assertTrue( - _determineLinked(typeA, typeB), + AssemblyAxialLinkage._determineLinked(typeA, typeB), msg="Test {0:s} failed for component type {1:s}!".format( name, str(method) ), ) self.assertTrue( - _determineLinked(typeB, typeA), + AssemblyAxialLinkage._determineLinked(typeB, typeA), msg="Test {0:s} failed for component type {1:s}!".format( name, str(method) ), ) else: self.assertFalse( - _determineLinked(typeA, typeB), + AssemblyAxialLinkage._determineLinked(typeA, typeB), msg="Test {0:s} failed for component type {1:s}!".format( name, str(method) ), ) self.assertFalse( - _determineLinked(typeB, typeA), + AssemblyAxialLinkage._determineLinked(typeB, typeA), msg="Test {0:s} failed for component type {1:s}!".format( name, str(method) ), @@ -1081,7 +1081,7 @@ def test_liquids(self): def test_unshapedComponentAndCircle(self): comp1 = Circle(*self.common, od=1.0, id=0.0) comp2 = UnshapedComponent(*self.common, area=1.0) - self.assertFalse(_determineLinked(comp1, comp2)) + self.assertFalse(AssemblyAxialLinkage._determineLinked(comp1, comp2)) def buildTestAssemblyWithFakeMaterial(name: str, hot: bool = False): From 4eeef7ab4e8e892a0d1a790c83629386d7052e98 Mon Sep 17 00:00:00 2001 From: aalberti Date: Thu, 3 Aug 2023 15:54:05 -0700 Subject: [PATCH 12/26] organizing and adding new tests --- .../converters/axialExpansionChanger.py | 6 +- .../tests/test_axialExpansionChanger.py | 107 +++++++++++------- 2 files changed, 64 insertions(+), 49 deletions(-) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index c6730641b..349513210 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -568,11 +568,7 @@ def _determineLinked(componentA, componentB) -> bool: linked : bool status is componentA and componentB are axially linked to one another """ - if ( - componentA.containsSolidMaterial() - and componentB.containsSolidMaterial() - and isinstance(componentA, type(componentB)) - ): + if isinstance(componentA, type(componentB)): if isinstance(componentA, UnshapedComponent): ## Case 1 -- see docstring runLog.warning( diff --git a/armi/reactor/converters/tests/test_axialExpansionChanger.py b/armi/reactor/converters/tests/test_axialExpansionChanger.py index 7cb146f3f..3077101de 100644 --- a/armi/reactor/converters/tests/test_axialExpansionChanger.py +++ b/armi/reactor/converters/tests/test_axialExpansionChanger.py @@ -33,7 +33,7 @@ ) from armi.reactor.flags import Flags from armi.reactor.tests.test_reactors import loadTestReactor, reduceTestReactorRings -from armi.tests import TEST_ROOT +from armi.tests import TEST_ROOT, mockRunLogs from armi.utils import units from numpy import array, linspace, zeros @@ -914,23 +914,24 @@ def checkColdBlockHeight(bStd, bExp, assertType, strForAssertion): ) -class TestLinkage(AxialExpansionTestBase, unittest.TestCase): - """Test axial linkage between components.""" +class TestCheckOverlap(AxialExpansionTestBase, unittest.TestCase): + """Test AssemblyAxialLinkage._checkOverlap for axial linkage between various component combinations.""" - def setUp(self): + @classmethod + def setUpClass(cls): """Contains common dimensions for all component class types.""" - AxialExpansionTestBase.setUp(self) - self.common = ("test", "FakeMat", 25.0, 25.0) # name, material, Tinput, Thot + AxialExpansionTestBase.setUp(cls) + cls.common = ("test", "FakeMat", 25.0, 25.0) # name, material, Tinput, Thot - def tearDown(self): - AxialExpansionTestBase.tearDown(self) + @classmethod + def tearDownClass(cls): + AxialExpansionTestBase.tearDown(cls) def runTest( self, componentsToTest: dict, assertionBool: bool, name: str, - commonArgs: tuple = None, ): """Runs various linkage tests. @@ -942,8 +943,6 @@ def runTest( expected truth value for test name : str the name of the test - commonArgs : tuple, optional - arguments common to all Component class types Notes ----- @@ -954,35 +953,31 @@ def runTest( Add dictionary entry with following: {Component Class Type: [{}, {}] """ - if commonArgs is None: - common = self.common - else: - common = commonArgs for method, dims in componentsToTest.items(): - typeA = method(*common, **dims[0]) - typeB = method(*common, **dims[1]) + typeA = method(*self.common, **dims[0]) + typeB = method(*self.common, **dims[1]) if assertionBool: self.assertTrue( - AssemblyAxialLinkage._determineLinked(typeA, typeB), + AssemblyAxialLinkage._checkOverlap(typeA, typeB), msg="Test {0:s} failed for component type {1:s}!".format( name, str(method) ), ) self.assertTrue( - AssemblyAxialLinkage._determineLinked(typeB, typeA), + AssemblyAxialLinkage._checkOverlap(typeB, typeA), msg="Test {0:s} failed for component type {1:s}!".format( name, str(method) ), ) else: self.assertFalse( - AssemblyAxialLinkage._determineLinked(typeA, typeB), + AssemblyAxialLinkage._checkOverlap(typeA, typeB), msg="Test {0:s} failed for component type {1:s}!".format( name, str(method) ), ) self.assertFalse( - AssemblyAxialLinkage._determineLinked(typeB, typeA), + AssemblyAxialLinkage._checkOverlap(typeB, typeA), msg="Test {0:s} failed for component type {1:s}!".format( name, str(method) ), @@ -1013,21 +1008,6 @@ def test_overlappingSolidPins(self): } self.runTest(componentTypesToTest, True, "test_overlappingSolidPins") - def test_differentMultNotOverlapping(self): - componentTypesToTest = { - Circle: [{"od": 0.5, "mult": 10}, {"od": 0.5, "mult": 20}], - Hexagon: [{"op": 0.5, "mult": 10}, {"op": 1.0, "mult": 20}], - Rectangle: [ - {"lengthOuter": 1.0, "widthOuter": 1.0, "mult": 10}, - {"lengthOuter": 1.0, "widthOuter": 1.0, "mult": 20}, - ], - Helix: [ - {"od": 0.5, "axialPitch": 1.0, "helixDiameter": 1.0, "mult": 10}, - {"od": 1.0, "axialPitch": 1.0, "helixDiameter": 1.0, "mult": 20}, - ], - } - self.runTest(componentTypesToTest, False, "test_differentMultNotOverlapping") - def test_solidPinNotOverlappingAnnulus(self): componentTypesToTest = { Circle: [{"od": 0.5, "id": 0.0}, {"od": 1.0, "id": 0.6}], @@ -1070,19 +1050,58 @@ def test_AnnularHexOverlappingThickAnnularHex(self): componentTypesToTest, True, "test_AnnularHexOverlappingThickAnnularHex" ) - def test_liquids(self): - componentTypesToTest = { - Circle: [{"od": 1.0, "id": 0.0}, {"od": 1.0, "id": 0.0}], - Hexagon: [{"op": 1.0, "ip": 0.0}, {"op": 1.0, "ip": 0.0}], - } - liquid = ("test", "Sodium", 425.0, 425.0) # name, material, Tinput, Thot - self.runTest(componentTypesToTest, False, "test_liquids", commonArgs=liquid) - def test_unshapedComponentAndCircle(self): +class TestDetermineLinked(AxialExpansionTestBase, unittest.TestCase): + """Test AssemblyAxialLinkage._determineLinked for the different linkage cases. + + Notes + ----- + Each test represents a linkage "Case". See the docstring for + AssemblyAxialLinkage::_determineLinked for a description of each case. + """ + + @classmethod + def setUpClass(cls): + AxialExpansionTestBase.setUp(cls) + cls.common = ("test", "FakeMat", 25.0, 25.0) + + @classmethod + def tearDownClass(cls): + AxialExpansionTestBase.tearDown(cls) + + def test_Case5(self): comp1 = Circle(*self.common, od=1.0, id=0.0) comp2 = UnshapedComponent(*self.common, area=1.0) self.assertFalse(AssemblyAxialLinkage._determineLinked(comp1, comp2)) + def test_Case1(self): + comp1 = UnshapedComponent(*self.common, area=2.0) + comp2 = UnshapedComponent(*self.common, area=1.0) + with mockRunLogs.BufferLog() as mock: + self.assertFalse(AssemblyAxialLinkage._determineLinked(comp1, comp2)) + self.assertIn( + "nor is it physical to do so. Instead of crashing and raising an error, ", + mock.getStdout(), + ) + + def test_Case4(self): + componentTypesToTest = { + Circle: [{"od": 0.5, "mult": 10}, {"od": 0.5, "mult": 20}], + Hexagon: [{"op": 0.5, "mult": 10}, {"op": 1.0, "mult": 20}], + Rectangle: [ + {"lengthOuter": 1.0, "widthOuter": 1.0, "mult": 10}, + {"lengthOuter": 1.0, "widthOuter": 1.0, "mult": 20}, + ], + Helix: [ + {"od": 0.5, "axialPitch": 1.0, "helixDiameter": 1.0, "mult": 10}, + {"od": 1.0, "axialPitch": 1.0, "helixDiameter": 1.0, "mult": 20}, + ], + } + for method, dims in componentTypesToTest.items(): + compA = method(*self.common, **dims[0]) + compB = method(*self.common, **dims[1]) + self.assertFalse(AssemblyAxialLinkage._determineLinked(compA, compB)) + def buildTestAssemblyWithFakeMaterial(name: str, hot: bool = False): """Create test assembly consisting of list of fake material. From ee9e2522b225c9e20ad72f9f947e3f397d83ac98 Mon Sep 17 00:00:00 2001 From: aalberti Date: Fri, 4 Aug 2023 08:45:57 -0700 Subject: [PATCH 13/26] adding test coverage for grid-based linking --- .../blueprints/tests/test_blockBlueprints.py | 22 +++++++++- .../tests/test_axialExpansionChanger.py | 42 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/armi/reactor/blueprints/tests/test_blockBlueprints.py b/armi/reactor/blueprints/tests/test_blockBlueprints.py index 36882a74d..70358764d 100644 --- a/armi/reactor/blueprints/tests/test_blockBlueprints.py +++ b/armi/reactor/blueprints/tests/test_blockBlueprints.py @@ -32,6 +32,15 @@ id: 0.0 od: 0.7 latticeIDs: [1] + fuel2: + flags: SLUG + shape: Circle + material: UZr + Tinput: 25.0 + Thot: 600.0 + id: 0.0 + od: 0.7 + latticeIDs: [3] clad: # same args as test_blocks (except mult) shape: Circle material: HT9 @@ -39,7 +48,7 @@ Thot: 450.0 id: .77 od: .80 - latticeIDs: [1,2] + latticeIDs: [1,2,3] coolant: shape: DerivedShape material: Sodium @@ -72,6 +81,15 @@ id: 0.0 od: 0.67 latticeIDs: [1] + fuel2: + flags: SLUG + shape: Circle + material: UZr + Tinput: 25.0 + Thot: 600.0 + id: 0.0 + od: 0.67 + latticeIDs: [3] clad: shape: Circle material: HT9 @@ -79,7 +97,7 @@ Thot: 450.0 id: .77 od: .80 - latticeIDs: [1,2] + latticeIDs: [1,2,3] coolant: shape: DerivedShape material: Sodium diff --git a/armi/reactor/converters/tests/test_axialExpansionChanger.py b/armi/reactor/converters/tests/test_axialExpansionChanger.py index 3077101de..5a0122685 100644 --- a/armi/reactor/converters/tests/test_axialExpansionChanger.py +++ b/armi/reactor/converters/tests/test_axialExpansionChanger.py @@ -15,10 +15,12 @@ """Test axialExpansionChanger.""" import collections import os +import io import unittest from statistics import mean from armi import materials +from armi.settings import Settings from armi.materials import _MATERIAL_NAMESPACE_ORDER, custom from armi.reactor.assemblies import HexAssembly, grids from armi.reactor.blocks import HexBlock @@ -33,6 +35,8 @@ ) from armi.reactor.flags import Flags from armi.reactor.tests.test_reactors import loadTestReactor, reduceTestReactorRings +from armi.reactor.blueprints.tests.test_blockBlueprints import FULL_BP +from armi.reactor import blueprints from armi.tests import TEST_ROOT, mockRunLogs from armi.utils import units from numpy import array, linspace, zeros @@ -1102,6 +1106,44 @@ def test_Case4(self): compB = method(*self.common, **dims[1]) self.assertFalse(AssemblyAxialLinkage._determineLinked(compA, compB)) + def test_Case2(self): + cs = Settings() + with io.StringIO(FULL_BP) as stream: + bps = blueprints.Blueprints.load(stream) + bps._prepConstruction(cs) + fuelBlockLower = bps.assemblies["fuel"][0] + fuelBlockUpper = bps.assemblies["fuel"][1] + fuelCompL = fuelBlockLower.getComponent(Flags.FUEL) + slugCompL = fuelBlockLower.getComponent(Flags.SLUG) + cladCompL = fuelBlockLower.getComponent(Flags.CLAD) + fuelCompU = fuelBlockUpper.getComponent(Flags.FUEL) + slugCompU = fuelBlockUpper.getComponent(Flags.SLUG) + cladCompU = fuelBlockUpper.getComponent(Flags.CLAD) + # test fuel component linking + self.assertFalse( + AssemblyAxialLinkage._determineLinked(fuelCompL, cladCompU) + ) + self.assertFalse( + AssemblyAxialLinkage._determineLinked(fuelCompL, slugCompU) + ) + self.assertTrue(AssemblyAxialLinkage._determineLinked(fuelCompL, fuelCompU)) + # test slug component linking + self.assertFalse( + AssemblyAxialLinkage._determineLinked(slugCompL, cladCompU) + ) + self.assertTrue(AssemblyAxialLinkage._determineLinked(slugCompL, slugCompU)) + self.assertFalse( + AssemblyAxialLinkage._determineLinked(slugCompL, fuelCompU) + ) + # test clad component linking + self.assertFalse( + AssemblyAxialLinkage._determineLinked(cladCompL, fuelCompU) + ) + self.assertFalse( + AssemblyAxialLinkage._determineLinked(cladCompL, slugCompL) + ) + self.assertTrue(AssemblyAxialLinkage._determineLinked(cladCompL, cladCompU)) + def buildTestAssemblyWithFakeMaterial(name: str, hot: bool = False): """Create test assembly consisting of list of fake material. From 169edc438b0437c80b2d9dccb67b691a38173725 Mon Sep 17 00:00:00 2001 From: aalberti Date: Fri, 4 Aug 2023 10:49:30 -0700 Subject: [PATCH 14/26] rm duplicate test --- armi/reactor/converters/tests/test_axialExpansionChanger.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/armi/reactor/converters/tests/test_axialExpansionChanger.py b/armi/reactor/converters/tests/test_axialExpansionChanger.py index 5a0122685..69dff79e1 100644 --- a/armi/reactor/converters/tests/test_axialExpansionChanger.py +++ b/armi/reactor/converters/tests/test_axialExpansionChanger.py @@ -651,12 +651,6 @@ def test_isFuelLocked(self): the_exception = cm.exception self.assertEqual(the_exception.error_code, 3) - def test_determineLinked(self): - compDims = {"Tinput": 25.0, "Thot": 25.0} - compA = UnshapedComponent("unshaped_1", "FakeMat", **compDims) - compB = UnshapedComponent("unshaped_2", "FakeMat", **compDims) - self.assertFalse(AssemblyAxialLinkage._determineLinked(compA, compB)) - def test_getLinkedComponents(self): """Test for multiple component axial linkage.""" shieldBlock = self.obj.linked.a[0] From 0d1a5b92ad07449f2c6bca500a4b8a723cc66e03 Mon Sep 17 00:00:00 2001 From: aalberti Date: Fri, 4 Aug 2023 16:07:26 -0700 Subject: [PATCH 15/26] enable multiple axial linkage during set up - during axial expansion choose the component which will be used for expansion. - all tests pass. Need to add aditional testing for coverage, improve docstrings, and see if logic optimization is available --- .../converters/axialExpansionChanger.py | 83 +++++++++---------- .../tests/test_axialExpansionChanger.py | 9 -- 2 files changed, 37 insertions(+), 55 deletions(-) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index 349513210..57f8bf3ed 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -13,6 +13,7 @@ # limitations under the License. """Enable component-wise axial expansion for assemblies and/or a reactor.""" +import collections from statistics import mean from typing import List @@ -283,9 +284,13 @@ def axiallyExpandAssembly(self): if ib == 0: c.zbottom = 0.0 else: - if self.linked.linkedComponents[c][0] is not None: + if self.linked.linkedComponents[c][0]: # use linked components below - c.zbottom = self.linked.linkedComponents[c][0].ztop + linkedComponent = self.determineLinkedComponent( + self.linked.a[ib - 1], + self.linked.linkedComponents[c][0], + ) + c.zbottom = linkedComponent.ztop else: # otherwise there aren't any linked components # so just set the bottom of the component to @@ -319,6 +324,32 @@ def axiallyExpandAssembly(self): bounds[2] = array(mesh) self.linked.a.spatialGrid._bounds = tuple(bounds) + def determineLinkedComponent(self, bBelow, linkedComponents): + """Determine the linked component.""" + linked = None + if len(linkedComponents) == 1: + # easy case, just pull the linked component + linked = linkedComponents[0] + else: + # get target component from block below + for cBelow in getSolidComponents(bBelow): + if self.expansionData.isTargetComponent(cBelow): + targetIndices = [ + list(index) for index in cBelow.spatialLocator.indices + ] + break + # loop over the linked components and see which shares spatial locator indices with targetIndices + for c in linkedComponents: + componentIndices = [list(index) for index in c.spatialLocator.indices] + if all(index in targetIndices for index in componentIndices): + linked = c + break + + if linked is None: + raise RuntimeError("Can't match with the target component!") + + return linked + def manageCoreMesh(self, r): """Manage core mesh post assembly-level expansion. @@ -481,64 +512,24 @@ def _getLinkedComponents(self, b, c): RuntimeError multiple candidate components are found to be axially linked to a component """ - lstLinkedC = [None, None] + self.linkedComponents[c] = collections.defaultdict(list) for ib, linkdBlk in enumerate(self.linkedBlocks[b]): if linkdBlk is not None: for otherC in getSolidComponents(linkdBlk.getChildren()): if AssemblyAxialLinkage._determineLinked(c, otherC): - if lstLinkedC[ib] is not None: - lstLinkedC[ib] = self._resolveMultipleLinkage( - c, otherC, lstLinkedC[ib] - ) - continue - lstLinkedC[ib] = otherC + self.linkedComponents[c][ib].append(otherC) - self.linkedComponents[c] = lstLinkedC - - if lstLinkedC[0] is None: + if not self.linkedComponents[c][0]: runLog.debug( f"Assembly {self.a}, Block {b}, Component {c} has nothing linked below it!", single=True, ) - if lstLinkedC[1] is None: + if not self.linkedComponents[c][1]: runLog.debug( f"Assembly {self.a}, Block {b}, Component {c} has nothing linked above it!", single=True, ) - def _resolveMultipleLinkage(self, primary, candidate1, candidate2): - """Use c.spatialLocator.indices to determine the proper linkage with primary.""" - errMsg = ( - "Multiple component axial linkages have been found for\n" - f"Component {primary}\nBlock {primary.parent}\nAssembly {self.a}.\n" - "This is indicative of an error in the blueprints and the correct use of block " - "grids should resolve this issue. Candidate components for linking:\n" - f"Primary: {primary}\nCandidates: {candidate1}, {candidate2}" - ) - if ( - isinstance(primary.spatialLocator, CoordinateLocation) - and isinstance(candidate1.spatialLocator, CoordinateLocation) - and isinstance(candidate2.spatialLocator, CoordinateLocation) - ): - runLog.error(msg=errMsg) - raise RuntimeError(errMsg) - - priIndices: List[int] = primary.spatialLocator.indices[0] - cand1Indices: List[int] = candidate1.spatialLocator.indices[0] - cand2Indices: List[int] = candidate2.spatialLocator.indices[0] - chooseC1: bool = False - chooseC2: bool = False - if (priIndices == cand1Indices).all(): - chooseC1 = True - if (priIndices == cand2Indices).all(): - chooseC2 = True - if (chooseC1 and chooseC2) or (chooseC1 is False and chooseC2 is False): - # if both True, candidate1 and candidate2 are in the same grid location (unphysical) - # if both false, candidate1 and candidate2 are not in the correct grid location (linking is impossible) - runLog.error(msg=errMsg) - raise RuntimeError(errMsg) - return candidate1 if chooseC1 else candidate2 - @staticmethod def _determineLinked(componentA, componentB) -> bool: """Determine axial component linkage for two solid components. diff --git a/armi/reactor/converters/tests/test_axialExpansionChanger.py b/armi/reactor/converters/tests/test_axialExpansionChanger.py index 69dff79e1..8a2d9cba1 100644 --- a/armi/reactor/converters/tests/test_axialExpansionChanger.py +++ b/armi/reactor/converters/tests/test_axialExpansionChanger.py @@ -651,15 +651,6 @@ def test_isFuelLocked(self): the_exception = cm.exception self.assertEqual(the_exception.error_code, 3) - def test_getLinkedComponents(self): - """Test for multiple component axial linkage.""" - shieldBlock = self.obj.linked.a[0] - shieldComp = shieldBlock[0] - shieldComp.setDimension("od", 0.785, cold=True) - with self.assertRaises(RuntimeError) as cm: - self.obj.linked._getLinkedComponents(shieldBlock, shieldComp) - self.assertEqual(cm.exception, 3) - class TestDetermineTargetComponent(AxialExpansionTestBase, unittest.TestCase): """Verify determineTargetComponent method is properly updating _componentDeterminesBlockHeight.""" From ab7cdb531f329d4d4e8edadf256dc8c140140132 Mon Sep 17 00:00:00 2001 From: aalberti Date: Mon, 7 Aug 2023 08:50:34 -0700 Subject: [PATCH 16/26] add case3 to TestDetermineLinked --- armi/reactor/converters/tests/test_axialExpansionChanger.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/armi/reactor/converters/tests/test_axialExpansionChanger.py b/armi/reactor/converters/tests/test_axialExpansionChanger.py index 8a2d9cba1..056b7a71b 100644 --- a/armi/reactor/converters/tests/test_axialExpansionChanger.py +++ b/armi/reactor/converters/tests/test_axialExpansionChanger.py @@ -1073,6 +1073,11 @@ def test_Case1(self): mock.getStdout(), ) + def test_Case3(self): + comp1 = Circle(*self.common, od=1.0, id=0.0, mult=7) + comp2 = Circle(*self.common, od=1.5, id=0.0, mult=7) + self.assertTrue(AssemblyAxialLinkage._determineLinked(comp1, comp2)) + def test_Case4(self): componentTypesToTest = { Circle: [{"od": 0.5, "mult": 10}, {"od": 0.5, "mult": 20}], From 1259c1f9bbe5ecbb7dc9b48bde0a4fd9f235c4ce Mon Sep 17 00:00:00 2001 From: aalberti Date: Mon, 7 Aug 2023 15:14:21 -0700 Subject: [PATCH 17/26] add additional complexities to test blueprints - add testing to cover complexities --- .../blueprints/tests/test_blockBlueprints.py | 46 +++++--- .../converters/axialExpansionChanger.py | 52 ++++----- .../tests/test_axialExpansionChanger.py | 107 +++++++++++++----- 3 files changed, 136 insertions(+), 69 deletions(-) diff --git a/armi/reactor/blueprints/tests/test_blockBlueprints.py b/armi/reactor/blueprints/tests/test_blockBlueprints.py index 70358764d..0cab613c9 100644 --- a/armi/reactor/blueprints/tests/test_blockBlueprints.py +++ b/armi/reactor/blueprints/tests/test_blockBlueprints.py @@ -32,8 +32,15 @@ id: 0.0 od: 0.7 latticeIDs: [1] - fuel2: - flags: SLUG + feed: + shape: Circle + material: UZr + Tinput: 25.0 + Thot: 600.0 + id: 0.0 + od: 0.7 + latticeIDs: [2] + slug: shape: Circle material: UZr Tinput: 25.0 @@ -41,14 +48,23 @@ id: 0.0 od: 0.7 latticeIDs: [3] - clad: # same args as test_blocks (except mult) + clad: &component_clad + # same args as test_blocks (except mult) shape: Circle material: HT9 Tinput: 25.0 Thot: 450.0 id: .77 od: .80 - latticeIDs: [1,2,3] + latticeIDs: [1] + clad_feed: + <<: *component_clad + flags: CLAD FEED + latticeIDs: [2] + clad_slug: + <<: *component_clad + flags: CLAD SLUG + latticeIDs: [3] coolant: shape: DerivedShape material: Sodium @@ -81,23 +97,23 @@ id: 0.0 od: 0.67 latticeIDs: [1] - fuel2: - flags: SLUG + test: shape: Circle material: UZr Tinput: 25.0 Thot: 600.0 id: 0.0 od: 0.67 - latticeIDs: [3] - clad: - shape: Circle - material: HT9 - Tinput: 25.0 - Thot: 450.0 - id: .77 - od: .80 - latticeIDs: [1,2,3] + latticeIDs: [2,3] + clad: *component_clad + clad_feed: + # should be clad_test with CLAD TEST + # flags, but for testing in + # test_axialExpansionChanger.TestRetrieveAxialLinkage + # we make it clad_feed. + <<: *component_clad + flags: CLAD FEED + latticeIDs: [2,3] coolant: shape: DerivedShape material: Sodium diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index 57f8bf3ed..f9abb79f7 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -21,7 +21,7 @@ from armi.materials import material from armi.reactor.components import UnshapedComponent from armi.reactor.flags import Flags -from armi.reactor.grids import MultiIndexLocation, CoordinateLocation +from armi.reactor.grids import MultiIndexLocation from numpy import array TARGET_FLAGS_IN_PREFERRED_ORDER = [ @@ -286,10 +286,7 @@ def axiallyExpandAssembly(self): else: if self.linked.linkedComponents[c][0]: # use linked components below - linkedComponent = self.determineLinkedComponent( - self.linked.a[ib - 1], - self.linked.linkedComponents[c][0], - ) + linkedComponent = self.retrieveLinkedComponent(c) c.zbottom = linkedComponent.ztop else: # otherwise there aren't any linked components @@ -324,30 +321,29 @@ def axiallyExpandAssembly(self): bounds[2] = array(mesh) self.linked.a.spatialGrid._bounds = tuple(bounds) - def determineLinkedComponent(self, bBelow, linkedComponents): - """Determine the linked component.""" - linked = None - if len(linkedComponents) == 1: - # easy case, just pull the linked component - linked = linkedComponents[0] - else: - # get target component from block below - for cBelow in getSolidComponents(bBelow): - if self.expansionData.isTargetComponent(cBelow): - targetIndices = [ - list(index) for index in cBelow.spatialLocator.indices - ] - break - # loop over the linked components and see which shares spatial locator indices with targetIndices - for c in linkedComponents: - componentIndices = [list(index) for index in c.spatialLocator.indices] - if all(index in targetIndices for index in componentIndices): - linked = c - break - - if linked is None: - raise RuntimeError("Can't match with the target component!") + def retrieveLinkedComponent(self, c): + """Retrieve the linked component. + Notes + ----- + 3 cases are considered, see test_axialExpansionChanger.py::TestRetriveAxialLinkage for details. + """ + linkedComponents = self.linked.linkedComponents[c][0] + # Case 1 + if len(linkedComponents) == 1: + return linkedComponents[0] + + ## Case 2 + for otherC in linkedComponents: + if otherC.hasFlags(c.p.flags): + return otherC + + # Case 3 + maxCompZtop = 0.0 + for otherC in linkedComponents: + if otherC.ztop > maxCompZtop: + linked = otherC + maxCompZtop = otherC.ztop return linked def manageCoreMesh(self, r): diff --git a/armi/reactor/converters/tests/test_axialExpansionChanger.py b/armi/reactor/converters/tests/test_axialExpansionChanger.py index 056b7a71b..f32da3180 100644 --- a/armi/reactor/converters/tests/test_axialExpansionChanger.py +++ b/armi/reactor/converters/tests/test_axialExpansionChanger.py @@ -1097,42 +1097,97 @@ def test_Case4(self): self.assertFalse(AssemblyAxialLinkage._determineLinked(compA, compB)) def test_Case2(self): + """Spot check the linkage for the Flag.TEST component.""" cs = Settings() with io.StringIO(FULL_BP) as stream: bps = blueprints.Blueprints.load(stream) bps._prepConstruction(cs) fuelBlockLower = bps.assemblies["fuel"][0] fuelBlockUpper = bps.assemblies["fuel"][1] - fuelCompL = fuelBlockLower.getComponent(Flags.FUEL) - slugCompL = fuelBlockLower.getComponent(Flags.SLUG) - cladCompL = fuelBlockLower.getComponent(Flags.CLAD) - fuelCompU = fuelBlockUpper.getComponent(Flags.FUEL) - slugCompU = fuelBlockUpper.getComponent(Flags.SLUG) - cladCompU = fuelBlockUpper.getComponent(Flags.CLAD) - # test fuel component linking - self.assertFalse( - AssemblyAxialLinkage._determineLinked(fuelCompL, cladCompU) - ) - self.assertFalse( - AssemblyAxialLinkage._determineLinked(fuelCompL, slugCompU) - ) - self.assertTrue(AssemblyAxialLinkage._determineLinked(fuelCompL, fuelCompU)) - # test slug component linking - self.assertFalse( - AssemblyAxialLinkage._determineLinked(slugCompL, cladCompU) - ) - self.assertTrue(AssemblyAxialLinkage._determineLinked(slugCompL, slugCompU)) - self.assertFalse( - AssemblyAxialLinkage._determineLinked(slugCompL, fuelCompU) + self.assertTrue( + AssemblyAxialLinkage._determineLinked( + fuelBlockUpper.getComponent(Flags.TEST), + fuelBlockLower.getComponent( + Flags.FEED | Flags.DEPLETABLE, exact=True + ), + ) ) - # test clad component linking - self.assertFalse( - AssemblyAxialLinkage._determineLinked(cladCompL, fuelCompU) + self.assertTrue( + AssemblyAxialLinkage._determineLinked( + fuelBlockUpper.getComponent(Flags.TEST), + fuelBlockLower.getComponent(Flags.SLUG | Flags.DEPLETABLE), + ) ) self.assertFalse( - AssemblyAxialLinkage._determineLinked(cladCompL, slugCompL) + AssemblyAxialLinkage._determineLinked( + fuelBlockUpper.getComponent(Flags.TEST), + fuelBlockLower.getComponent(Flags.FUEL | Flags.DEPLETABLE), + ) ) - self.assertTrue(AssemblyAxialLinkage._determineLinked(cladCompL, cladCompU)) + + +class TestRetrieveAxialLinkage(unittest.TestCase): + """Ensure that axial linkage for components can be retrieved appropriately. + + Notes + ----- + Three cases here. + Case 1: easy case, just pull the linked component as there is an explicit 1-1 linking. + Case 2: c.p.FEED in block 1 has candidate links to [c.p.FEED and c.p.SLUG] in block 0. + Use the matching flags to determine linkage. + Case 3: c.p.TEST in block 1 has candidate links to [c.p.FEED, c.p.SLUG]. + Determine which component in block 0 has the highest ztop. This is the + linked component. + + Warning + ------- + If c.p.CLAD in block 1 has candidate links to [c.p.CLAD, c.p.CLAD] in block 0, + the first component with the clad flags in block 0 will be the linked component. + If possible, use additional flags in the blueprints to be as explicit as possible. + """ + + def setUp(self): + self.axialExpChnger = AxialExpansionChanger() + self.axialExpChnger.reset() + + def test_Case1(self): + a = buildTestAssemblyWithFakeMaterial("HT9") + self.axialExpChnger.setAssembly(a) + refLinkage = { + 1: [Flags.SHIELD, Flags.CLAD, Flags.DUCT], + 2: [Flags.FUEL, Flags.CLAD, Flags.DUCT], + 3: [Flags.FUEL, Flags.CLAD, Flags.DUCT], + } + for ib, b in enumerate(self.axialExpChnger.linked.a[1:], start=1): + for ic, c in enumerate(getSolidComponents(b)): + linkedC = self.axialExpChnger.retrieveLinkedComponent(c) + self.assertTrue(linkedC.hasFlags(refLinkage[ib][ic])) + + def test_Cases2And3(self): + cs = Settings() + with io.StringIO(FULL_BP) as stream: + bps = blueprints.Blueprints.load(stream) + bps._prepConstruction(cs) + a = bps.assemblies["fuel"] + self.axialExpChnger.setAssembly(a) + refLinkage = { + 1: [ + Flags.FUEL, + Flags.FEED, + Flags.CLAD, + [Flags.CLAD, Flags.FEED], + Flags.DUCT, + ], + } + for ib, b in enumerate(self.axialExpChnger.linked.a): + for ic, c in enumerate(getSolidComponents(b)): + if ib == 0: + # c.ztop gets set during axial expansion, but since we aren't doing + # actual expansion, we set it manually + c.ztop = b.p.ztop + continue + linkedC = self.axialExpChnger.retrieveLinkedComponent(c) + self.assertTrue(linkedC.hasFlags(refLinkage[ib][ic])) def buildTestAssemblyWithFakeMaterial(name: str, hot: bool = False): From 04a4e24e8f46450383e694caea38e65f8b4a2d0d Mon Sep 17 00:00:00 2001 From: aalberti Date: Mon, 7 Aug 2023 15:21:47 -0700 Subject: [PATCH 18/26] fix broken unit test --- armi/reactor/blueprints/tests/test_blockBlueprints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armi/reactor/blueprints/tests/test_blockBlueprints.py b/armi/reactor/blueprints/tests/test_blockBlueprints.py index 0cab613c9..12930436e 100644 --- a/armi/reactor/blueprints/tests/test_blockBlueprints.py +++ b/armi/reactor/blueprints/tests/test_blockBlueprints.py @@ -358,7 +358,7 @@ def test_densityConsistentWithComponentConstructor(self): self.cs, self.blueprints ) fuelBlock = a1[0] - clad = fuelBlock.getComponent(Flags.CLAD) + clad = fuelBlock.getComponent(Flags.CLAD, exact=True) # now construct clad programmatically like in test_Blocks programmaticBlock = test_blocks.buildSimpleFuelBlock() From bd9bfa66516a15d09bace32e261d1d9e523a7f50 Mon Sep 17 00:00:00 2001 From: Chris Keckler Date: Mon, 9 Oct 2023 08:39:37 -0500 Subject: [PATCH 19/26] Update ztop for blocks that don't have any solid components --- .../converters/axialExpansionChanger.py | 62 +++++++++++-------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index 829bbcbbb..f5c0a4047 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -276,35 +276,43 @@ def axiallyExpandAssembly(self): b.p.zbottom = self.linked.linkedBlocks[b][0].p.ztop isDummyBlock = ib == (numOfBlocks - 1) if not isDummyBlock: - for c in getSolidComponents(b): - growFrac = self.expansionData.getExpansionFactor(c) - runLog.debug(msg=f" Component {c}, growFrac = {growFrac:.4e}") - c.height = growFrac * blockHeight - # align linked components - if ib == 0: - c.zbottom = 0.0 - else: - if self.linked.linkedComponents[c][0]: - # use linked components below - linkedComponent = self.retrieveLinkedComponent(c) - c.zbottom = linkedComponent.ztop + if (solidComponents := getSolidComponents(b)) : + for c in solidComponents: + growFrac = self.expansionData.getExpansionFactor(c) + runLog.debug( + msg=f" Component {c}, growFrac = {growFrac:.4e}" + ) + c.height = growFrac * blockHeight + # align linked components + if ib == 0: + c.zbottom = 0.0 else: - # otherwise there aren't any linked components - # so just set the bottom of the component to - # the top of the block below it - c.zbottom = self.linked.linkedBlocks[b][0].p.ztop - c.ztop = c.zbottom + c.height - # update component number densities - newNumberDensities = { - nuc: c.getNumberDensity(nuc) / growFrac - for nuc in c.getNuclides() - } - c.setNumberDensities(newNumberDensities) - # redistribute block boundaries if on the target component - if self.expansionData.isTargetComponent(c): - b.p.ztop = c.ztop - b.p.height = b.p.ztop - b.p.zbottom + if self.linked.linkedComponents[c][0]: + # use linked components below + linkedComponent = self.retrieveLinkedComponent(c) + c.zbottom = linkedComponent.ztop + else: + # otherwise there aren't any linked components + # so just set the bottom of the component to + # the top of the block below it + c.zbottom = self.linked.linkedBlocks[b][0].p.ztop + c.ztop = c.zbottom + c.height + # update component number densities + newNumberDensities = { + nuc: c.getNumberDensity(nuc) / growFrac + for nuc in c.getNuclides() + } + c.setNumberDensities(newNumberDensities) + # redistribute block boundaries if on the target component + if self.expansionData.isTargetComponent(c): + b.p.ztop = c.ztop + b.p.height = b.p.ztop - b.p.zbottom + else: + # the block has only fluids but is not the dummy block, + # just shift it upwards + b.p.ztop = b.p.zbottom + b.p.height else: + # this block is the dummy block b.p.height = b.p.ztop - b.p.zbottom b.p.z = b.p.zbottom + b.getHeight() / 2.0 From 4f1dd9c63b60c8e51f7cb77c8567e284ddb4acd8 Mon Sep 17 00:00:00 2001 From: Chris Keckler Date: Mon, 9 Oct 2023 10:29:47 -0500 Subject: [PATCH 20/26] Add test on assembly that has a purely fluid block --- .../tests/test_axialExpansionChanger.py | 26 +++++++++++++++++++ .../refSmallReactorBase.yaml | 23 +++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/armi/reactor/converters/tests/test_axialExpansionChanger.py b/armi/reactor/converters/tests/test_axialExpansionChanger.py index f32da3180..814a2a289 100644 --- a/armi/reactor/converters/tests/test_axialExpansionChanger.py +++ b/armi/reactor/converters/tests/test_axialExpansionChanger.py @@ -355,6 +355,32 @@ def _getMass(a): newMass = a.getMass("B10") return newMass + def test_ExpandAssemWithFluidBlock(self): + _oCold, rCold = loadTestReactor( + os.path.join(TEST_ROOT, "detailedAxialExpansion"), + customSettings={"inputHeightsConsideredHot": False}, + ) + a = rCold.blueprints.assemblies["fuel with fluid block"] + origMesh = a.getAxialMesh() + changer = AxialExpansionChanger(detailedAxialExpansion=True) + changer.setAssembly(a) + for b in a: + for c in b: + changer.expansionData.updateComponentTemp(c, c.temperatureInC + 50) + changer.expansionData.computeThermalExpansionFactors() + changer.axiallyExpandAssembly() + + newMesh = a.getAxialMesh() + + # check that height of assembly overall hasn't changed + self.assertEqual(origMesh[-1], newMesh[-1]) + + # check that top block (dummy block) _has_ changed height + self.assertGreater(newMesh[-2], origMesh[-2]) + + # check that height of "SodiumBlock" hasn't changed + self.assertEqual(newMesh[-2] - newMesh[-3], origMesh[-2] - origMesh[-3]) + def test_PrescribedExpansionContractionConservation(self): """Expand all components and then contract back to original state. diff --git a/armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml b/armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml index 1711a56ee..bf15411ee 100644 --- a/armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml +++ b/armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml @@ -61,7 +61,7 @@ blocks: mult: 1.0 op: 19.0 - SodiumBlock : &block_dummy + DummyBlock: &block_dummy flags: dummy coolant: shape: Hexagon @@ -72,6 +72,17 @@ blocks: mult: 1.0 op: 19.0 + SodiumBlock: &block_sodium + axial expansion target component: coolant + coolant: + shape: Hexagon + material: Sodium + Tinput: 25.0 + Thot: 450.0 + ip: 0.0 + mult: 1.0 + op: 19.0 + ## ------------------------------------------------------------------------------------ ## fuel blocks axial shield: &block_fuel_axial_shield @@ -691,3 +702,13 @@ assemblies: height: [25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 17.5] axial mesh points: *standard_axial_mesh_points xs types: *igniter_fuel_xs_types + fuel with fluid block: + specifier: FB + # blocks: [*block_grid_plate, *block_fuel_axial_shield_fourPin, *block_fuel, *block_fuel, *block_fuel, *block_plenum_fourPin, *block_aclp_fourPin, *block_plenum_fourPin, *block_duct, *block_sodium, *block_dummy] + # height: [25.0, 27.5, 27.5, 27.5, 27.5, 15.0, 25.0, 25.0, 24.0, 1.0, 17.5] + # axial mesh points: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + # xs types: [A, A, B, C, C, D, A, A, A, A, A] + blocks: [*block_grid_plate, *block_radial_shield, *block_radial_shield, *block_radial_shield, *block_radial_shield, *block_shield_plenum, *block_shield_aclp, *block_shield_plenum, *block_duct, *block_sodium, *block_dummy] + height: [25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 24.0, 1.0, 17.5] + axial mesh points: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + xs types: [A, A, B, C, C, D, A, A, A, A, A] From 73e7795370a9bfab3a5e85a3e10387d1c6b7e861 Mon Sep 17 00:00:00 2001 From: Chris Keckler Date: Mon, 9 Oct 2023 10:31:31 -0500 Subject: [PATCH 21/26] Remove use of walrus operator --- armi/reactor/converters/axialExpansionChanger.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index f5c0a4047..ab7578fbd 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -276,7 +276,8 @@ def axiallyExpandAssembly(self): b.p.zbottom = self.linked.linkedBlocks[b][0].p.ztop isDummyBlock = ib == (numOfBlocks - 1) if not isDummyBlock: - if (solidComponents := getSolidComponents(b)) : + solidComponents = getSolidComponents(b) + if solidComponents: for c in solidComponents: growFrac = self.expansionData.getExpansionFactor(c) runLog.debug( From 63e37fded186d6cb594aa0e5e8c6aece5668a2ab Mon Sep 17 00:00:00 2001 From: Chris Keckler Date: Tue, 10 Oct 2023 16:35:43 +0100 Subject: [PATCH 22/26] Revert "Remove use of walrus operator" This reverts commit 73e7795370a9bfab3a5e85a3e10387d1c6b7e861. --- armi/reactor/converters/axialExpansionChanger.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index ab7578fbd..f5c0a4047 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -276,8 +276,7 @@ def axiallyExpandAssembly(self): b.p.zbottom = self.linked.linkedBlocks[b][0].p.ztop isDummyBlock = ib == (numOfBlocks - 1) if not isDummyBlock: - solidComponents = getSolidComponents(b) - if solidComponents: + if (solidComponents := getSolidComponents(b)) : for c in solidComponents: growFrac = self.expansionData.getExpansionFactor(c) runLog.debug( From 25f8e9fc581a7fa5f7d628c827af6847e12eb0c7 Mon Sep 17 00:00:00 2001 From: Chris Keckler Date: Tue, 10 Oct 2023 16:35:55 +0100 Subject: [PATCH 23/26] Revert "Add test on assembly that has a purely fluid block" This reverts commit 4f1dd9c63b60c8e51f7cb77c8567e284ddb4acd8. --- .../tests/test_axialExpansionChanger.py | 26 ------------------- .../refSmallReactorBase.yaml | 23 +--------------- 2 files changed, 1 insertion(+), 48 deletions(-) diff --git a/armi/reactor/converters/tests/test_axialExpansionChanger.py b/armi/reactor/converters/tests/test_axialExpansionChanger.py index 814a2a289..f32da3180 100644 --- a/armi/reactor/converters/tests/test_axialExpansionChanger.py +++ b/armi/reactor/converters/tests/test_axialExpansionChanger.py @@ -355,32 +355,6 @@ def _getMass(a): newMass = a.getMass("B10") return newMass - def test_ExpandAssemWithFluidBlock(self): - _oCold, rCold = loadTestReactor( - os.path.join(TEST_ROOT, "detailedAxialExpansion"), - customSettings={"inputHeightsConsideredHot": False}, - ) - a = rCold.blueprints.assemblies["fuel with fluid block"] - origMesh = a.getAxialMesh() - changer = AxialExpansionChanger(detailedAxialExpansion=True) - changer.setAssembly(a) - for b in a: - for c in b: - changer.expansionData.updateComponentTemp(c, c.temperatureInC + 50) - changer.expansionData.computeThermalExpansionFactors() - changer.axiallyExpandAssembly() - - newMesh = a.getAxialMesh() - - # check that height of assembly overall hasn't changed - self.assertEqual(origMesh[-1], newMesh[-1]) - - # check that top block (dummy block) _has_ changed height - self.assertGreater(newMesh[-2], origMesh[-2]) - - # check that height of "SodiumBlock" hasn't changed - self.assertEqual(newMesh[-2] - newMesh[-3], origMesh[-2] - origMesh[-3]) - def test_PrescribedExpansionContractionConservation(self): """Expand all components and then contract back to original state. diff --git a/armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml b/armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml index bf15411ee..1711a56ee 100644 --- a/armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml +++ b/armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml @@ -61,7 +61,7 @@ blocks: mult: 1.0 op: 19.0 - DummyBlock: &block_dummy + SodiumBlock : &block_dummy flags: dummy coolant: shape: Hexagon @@ -72,17 +72,6 @@ blocks: mult: 1.0 op: 19.0 - SodiumBlock: &block_sodium - axial expansion target component: coolant - coolant: - shape: Hexagon - material: Sodium - Tinput: 25.0 - Thot: 450.0 - ip: 0.0 - mult: 1.0 - op: 19.0 - ## ------------------------------------------------------------------------------------ ## fuel blocks axial shield: &block_fuel_axial_shield @@ -702,13 +691,3 @@ assemblies: height: [25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 17.5] axial mesh points: *standard_axial_mesh_points xs types: *igniter_fuel_xs_types - fuel with fluid block: - specifier: FB - # blocks: [*block_grid_plate, *block_fuel_axial_shield_fourPin, *block_fuel, *block_fuel, *block_fuel, *block_plenum_fourPin, *block_aclp_fourPin, *block_plenum_fourPin, *block_duct, *block_sodium, *block_dummy] - # height: [25.0, 27.5, 27.5, 27.5, 27.5, 15.0, 25.0, 25.0, 24.0, 1.0, 17.5] - # axial mesh points: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - # xs types: [A, A, B, C, C, D, A, A, A, A, A] - blocks: [*block_grid_plate, *block_radial_shield, *block_radial_shield, *block_radial_shield, *block_radial_shield, *block_shield_plenum, *block_shield_aclp, *block_shield_plenum, *block_duct, *block_sodium, *block_dummy] - height: [25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 24.0, 1.0, 17.5] - axial mesh points: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - xs types: [A, A, B, C, C, D, A, A, A, A, A] From 989a43bf940563982bde7d2117acacdbf38fe433 Mon Sep 17 00:00:00 2001 From: Chris Keckler Date: Tue, 10 Oct 2023 16:36:02 +0100 Subject: [PATCH 24/26] Revert "Update ztop for blocks that don't have any solid components" This reverts commit bd9bfa66516a15d09bace32e261d1d9e523a7f50. --- .../converters/axialExpansionChanger.py | 62 ++++++++----------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index f5c0a4047..829bbcbbb 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -276,43 +276,35 @@ def axiallyExpandAssembly(self): b.p.zbottom = self.linked.linkedBlocks[b][0].p.ztop isDummyBlock = ib == (numOfBlocks - 1) if not isDummyBlock: - if (solidComponents := getSolidComponents(b)) : - for c in solidComponents: - growFrac = self.expansionData.getExpansionFactor(c) - runLog.debug( - msg=f" Component {c}, growFrac = {growFrac:.4e}" - ) - c.height = growFrac * blockHeight - # align linked components - if ib == 0: - c.zbottom = 0.0 + for c in getSolidComponents(b): + growFrac = self.expansionData.getExpansionFactor(c) + runLog.debug(msg=f" Component {c}, growFrac = {growFrac:.4e}") + c.height = growFrac * blockHeight + # align linked components + if ib == 0: + c.zbottom = 0.0 + else: + if self.linked.linkedComponents[c][0]: + # use linked components below + linkedComponent = self.retrieveLinkedComponent(c) + c.zbottom = linkedComponent.ztop else: - if self.linked.linkedComponents[c][0]: - # use linked components below - linkedComponent = self.retrieveLinkedComponent(c) - c.zbottom = linkedComponent.ztop - else: - # otherwise there aren't any linked components - # so just set the bottom of the component to - # the top of the block below it - c.zbottom = self.linked.linkedBlocks[b][0].p.ztop - c.ztop = c.zbottom + c.height - # update component number densities - newNumberDensities = { - nuc: c.getNumberDensity(nuc) / growFrac - for nuc in c.getNuclides() - } - c.setNumberDensities(newNumberDensities) - # redistribute block boundaries if on the target component - if self.expansionData.isTargetComponent(c): - b.p.ztop = c.ztop - b.p.height = b.p.ztop - b.p.zbottom - else: - # the block has only fluids but is not the dummy block, - # just shift it upwards - b.p.ztop = b.p.zbottom + b.p.height + # otherwise there aren't any linked components + # so just set the bottom of the component to + # the top of the block below it + c.zbottom = self.linked.linkedBlocks[b][0].p.ztop + c.ztop = c.zbottom + c.height + # update component number densities + newNumberDensities = { + nuc: c.getNumberDensity(nuc) / growFrac + for nuc in c.getNuclides() + } + c.setNumberDensities(newNumberDensities) + # redistribute block boundaries if on the target component + if self.expansionData.isTargetComponent(c): + b.p.ztop = c.ztop + b.p.height = b.p.ztop - b.p.zbottom else: - # this block is the dummy block b.p.height = b.p.ztop - b.p.zbottom b.p.z = b.p.zbottom + b.getHeight() / 2.0 From e3b47651561cf238e3994dbcc32ca0c3c1062158 Mon Sep 17 00:00:00 2001 From: Chris Keckler Date: Tue, 10 Oct 2023 20:41:31 +0100 Subject: [PATCH 25/26] Explicitly raise an error if assemblies include blocks without solid components --- .../converters/axialExpansionChanger.py | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index 829bbcbbb..a90277236 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -22,6 +22,8 @@ from armi.reactor.components import UnshapedComponent from armi.reactor.flags import Flags from armi.reactor.grids import MultiIndexLocation +from armi.utils.customExceptions import InputError + from numpy import array TARGET_FLAGS_IN_PREFERRED_ORDER = [ @@ -219,7 +221,7 @@ def setAssembly(self, a, setFuel=True, expandFromTinputToThot=False): self.expansionData = ExpansionData( a, setFuel=setFuel, expandFromTinputToThot=expandFromTinputToThot ) - self._isTopDummyBlockPresent() + self._checkAssemblyConstructionIsValid() def applyColdHeightMassIncrease(self): """ @@ -238,6 +240,10 @@ def applyColdHeightMassIncrease(self): ) c.changeNDensByFactor(axialExpansionFactor) + def _checkAssemblyConstructionIsValid(self): + self._isTopDummyBlockPresent() + self._checkForBlocksWithoutSolids() + def _isTopDummyBlockPresent(self): """Determines if top most block of assembly is a dummy block. @@ -259,6 +265,33 @@ def _isTopDummyBlockPresent(self): runLog.error(msg) raise RuntimeError(msg) + def _checkForBlocksWithoutSolids(self): + """ + Makes sure that there aren't any blocks (other than the top-most dummy block) + that are entirely fluid filled, unless all blocks in the assembly are only + fluids. The expansion changer doesn't know what to do with such mixed assemblies. + """ + solidCompsInAssem = [ + c + for c in self.linked.a.iterComponents() + if not isinstance(c.material, material.Fluid) + ] + if len(solidCompsInAssem) == 0: + return + + for b in self.linked.a[:-1]: + # the topmost block has already been confirmed as the dummy block + solidCompsInBlock = [ + c + for c in b.iterComponents() + if not isinstance(c.material, material.Fluid) + ] + if len(solidCompsInBlock) == 0: + raise InputError( + f"Assembly {self.linked.a} is constructed improperly for use with the axial expansion changer.\n" + "Consider using the assemFlagsToSkipAxialExpansion case setting." + ) + def axiallyExpandAssembly(self): """Utilizes assembly linkage to do axial expansion.""" mesh = [0.0] From 836be51b2b5481465ef38a59aefd14794b4fceea Mon Sep 17 00:00:00 2001 From: Chris Keckler Date: Tue, 10 Oct 2023 20:41:50 +0100 Subject: [PATCH 26/26] Add test on new check --- .../converters/tests/test_axialExpansionChanger.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/armi/reactor/converters/tests/test_axialExpansionChanger.py b/armi/reactor/converters/tests/test_axialExpansionChanger.py index f32da3180..151bf3592 100644 --- a/armi/reactor/converters/tests/test_axialExpansionChanger.py +++ b/armi/reactor/converters/tests/test_axialExpansionChanger.py @@ -21,7 +21,7 @@ from armi import materials from armi.settings import Settings -from armi.materials import _MATERIAL_NAMESPACE_ORDER, custom +from armi.materials import _MATERIAL_NAMESPACE_ORDER, custom, ht9 from armi.reactor.assemblies import HexAssembly, grids from armi.reactor.blocks import HexBlock from armi.reactor.components import DerivedShape, UnshapedComponent @@ -38,6 +38,7 @@ from armi.reactor.blueprints.tests.test_blockBlueprints import FULL_BP from armi.reactor import blueprints from armi.tests import TEST_ROOT, mockRunLogs +from armi.utils.customExceptions import InputError from armi.utils import units from numpy import array, linspace, zeros @@ -355,6 +356,17 @@ def _getMass(a): newMass = a.getMass("B10") return newMass + def test_checkForBlocksWithoutSolids(self): + a = buildTestAssemblyWithFakeMaterial( + name="Sodium" + ) # every component is sodium + changer = AxialExpansionChanger(detailedAxialExpansion=True) + changer.setAssembly(a) # no error because _everything_ is sodium + + a[0][1].material = ht9.HT9() + with self.assertRaises(InputError): + changer.setAssembly(a) + def test_PrescribedExpansionContractionConservation(self): """Expand all components and then contract back to original state.