diff --git a/armi/reactor/blueprints/blockBlueprint.py b/armi/reactor/blueprints/blockBlueprint.py index 6ee555cf38..0a41a9e00c 100644 --- a/armi/reactor/blueprints/blockBlueprint.py +++ b/armi/reactor/blueprints/blockBlueprint.py @@ -119,16 +119,6 @@ def construct( materialInput, componentDesign ) c = componentDesign.construct(blueprint, filteredMaterialInput) - if cs["inputHeightsConsideredHot"]: - if "group" in c.name: - for component in c: - component.applyHotHeightDensityReduction() - componentBlueprint.insertDepletableNuclideKeys( - component, blueprint - ) - else: - c.applyHotHeightDensityReduction() - componentBlueprint.insertDepletableNuclideKeys(c, blueprint) components[c.name] = c if spatialGrid: componentLocators = gridDesign.getMultiLocator( diff --git a/armi/reactor/blueprints/tests/test_blockBlueprints.py b/armi/reactor/blueprints/tests/test_blockBlueprints.py index ab3f7eabfa..98f0387b95 100644 --- a/armi/reactor/blueprints/tests/test_blockBlueprints.py +++ b/armi/reactor/blueprints/tests/test_blockBlueprints.py @@ -339,20 +339,9 @@ def test_densityConsistentWithComponentConstructor(self): delta=biggerDelta, ) - # This should be equal, but block construction calls applyHotHeightDensityReduction - # while programmatic construction allows components to exist in a state where - # their density is not consistent with material density. - self.assertNotAlmostEqual( + self.assertAlmostEqual( clad.getMassDensity(), programaticClad.getMassDensity(), - delta=biggerDelta, - ) - # its off by a factor of thermal expansion - self.assertNotAlmostEqual( - clad.getMassDensity(), - programaticClad.getMassDensity() - * programaticClad.getThermalExpansionFactor(), - delta=biggerDelta, ) diff --git a/armi/reactor/components/component.py b/armi/reactor/components/component.py index 9e5ee72d89..8763712636 100644 --- a/armi/reactor/components/component.py +++ b/armi/reactor/components/component.py @@ -348,10 +348,12 @@ def applyMaterialMassFracsToNumberDensities(self): self.p.numberDensities = densityTools.getNDensFromMasses( density, self.material.p.massFrac ) + self.applyHotHeightDensityReduction() def applyHotHeightDensityReduction(self): """ - Adjust number densities to account for prescribed hot block heights (axial expansion). + Adjust number densities to account for hot block heights (axial expansion) + (crucial for preserving 3D density). Notes ----- @@ -363,6 +365,9 @@ def applyHotHeightDensityReduction(self): -------- self.applyMaterialMassFracsToNumberDensities """ + # this is the same as getThermalExpansionFactor but doesn't fail + # on non-fluid materials that have 0 or undefined thermal expansion + # (we don't want materials to fail on __init__ which calls this) axialExpansionFactor = 1.0 + self.material.linearExpansionFactor( self.temperatureInC, self.inputTemperatureInC ) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index 7f3edd9abf..3355a5ec65 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -125,6 +125,23 @@ def setAssembly(self, a, setFuel=True): self.expansionData = ExpansionData(a, setFuel) self._isTopDummyBlockPresent() + def applyColdHeightMassIncrease(self): + """ + Increase component mass because they are declared at cold dims + + Notes + ----- + A cold 1 cm tall component will have more mass that a component with the + same mass/length as a component with a hot height of 1 cm. This should be + called when the setting `inputHeightsConsideredHot` is used. This basically + undoes component.applyHotHeightDensityReduction + """ + for c in self.linked.a.getComponents(): + axialExpansionFactor = 1.0 + c.material.linearExpansionFactor( + c.temperatureInC, c.inputTemperatureInC + ) + c.changeNDensByFactor(axialExpansionFactor) + def _isTopDummyBlockPresent(self): """determines if top most block of assembly is a dummy block @@ -175,9 +192,10 @@ def axiallyExpandAssembly(self, thermal: bool = False): # if ib == 0, leave block bottom = 0.0 if ib > 0: b.p.zbottom = self.linked.linkedBlocks[b][0].p.ztop - # if not in the dummy block, get expansion factor, do alignment, and modify block - if ib < (numOfBlocks - 1): + isDummyBlock = ib == (numOfBlocks - 1) + if not isDummyBlock: for c in b: + growFrac = self.expansionData.getExpansionFactor(c) runLog.debug( msg=" Component {0}, growFrac = {1:.4e}".format( @@ -222,8 +240,9 @@ def axiallyExpandAssembly(self, thermal: bool = False): # see also b.setHeight() # - the above not chosen due to call to calculateZCoords oldComponentVolumes = [c.getVolume() for c in b] - oldHeight = b.getHeight() + oldHeight = b.p.height b.p.height = b.p.ztop - b.p.zbottom + _checkBlockHeight(b) _conserveComponentMass(b, oldHeight, oldComponentVolumes) # set block mid point and redo mesh diff --git a/armi/reactor/converters/tests/test_axialExpansionChanger.py b/armi/reactor/converters/tests/test_axialExpansionChanger.py index 058f1596c0..8c9d34037c 100644 --- a/armi/reactor/converters/tests/test_axialExpansionChanger.py +++ b/armi/reactor/converters/tests/test_axialExpansionChanger.py @@ -758,20 +758,21 @@ class TestInputHeightsConsideredHot(unittest.TestCase): """verify thermal expansion for process loading of core""" def setUp(self): - """provide the base case""" + """This test uses a different armiRun.yaml than the default""" + _o, r = loadTestReactor( os.path.join(TEST_ROOT, "detailedAxialExpansion"), - {"inputHeightsConsideredHot": True}, + customSettings={"inputHeightsConsideredHot": True}, ) self.stdAssems = [a for a in r.core.getAssemblies()] _oCold, rCold = loadTestReactor( os.path.join(TEST_ROOT, "detailedAxialExpansion"), - {"inputHeightsConsideredHot": False}, + customSettings={"inputHeightsConsideredHot": False}, ) self.testAssems = [a for a in rCold.core.getAssemblies()] - def test_coldAssemblyHeight(self): + def test_coldAssemblyExpansion(self): """block heights are cold and should be expanded Notes @@ -801,6 +802,14 @@ def test_coldAssemblyHeight(self): checkColdBlockHeight(bStd, bExp, self.assertEqual, "the same") else: checkColdBlockHeight(bStd, bExp, self.assertNotEqual, "different") + if bStd.hasFlags(Flags.FUEL): + # fuel mass should grow because heights are considered cold heights + # and a cold 1 cm column has more mass than a hot 1 cm column + if not isinstance( + bStd.getComponent(Flags.FUEL).material, custom.Custom + ): + # custom materials don't expand + self.assertGreater(bExp.getMass("U235"), bStd.getMass("U235")) def checkColdBlockHeight(bStd, bExp, assertType, strForAssertion): diff --git a/armi/reactor/converters/tests/test_blockConverter.py b/armi/reactor/converters/tests/test_blockConverter.py index 5aced75744..a640ff52e4 100644 --- a/armi/reactor/converters/tests/test_blockConverter.py +++ b/armi/reactor/converters/tests/test_blockConverter.py @@ -55,7 +55,6 @@ def _perturbTemps(self, block, cName, tCold, tHot): "Give the component different ref and hot temperatures than in test_Blocks." c = block.getComponent(Flags.fromString(cName)) c.refTemp, c.refHot = tCold, tHot - c.applyHotHeightDensityReduction() c.setTemperature(tHot) return block diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index c86adda76a..865241a5b7 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -215,7 +215,6 @@ def __init__(self, name): self._circularRingPitch = 1.0 self._automaticVariableMesh = False self._minMeshSizeRatio = 0.15 - self._inputHeightsConsideredHot = True self._detailedAxialExpansion = False def setOptionsFromCs(self, cs): @@ -227,7 +226,6 @@ def setOptionsFromCs(self, cs): self._circularRingPitch = cs["circularRingPitch"] self._automaticVariableMesh = cs["automaticVariableMesh"] self._minMeshSizeRatio = cs["minMeshSizeRatio"] - self._inputHeightsConsideredHot = cs["inputHeightsConsideredHot"] self._detailedAxialExpansion = cs["detailedAxialExpansion"] def __getstate__(self): @@ -2338,6 +2336,12 @@ def _applyThermalExpansion( for a in assems: if not a.hasFlags(Flags.CONTROL): axialExpChngr.setAssembly(a) + # this doesn't get applied to control assems, so CR will be interpreted + # as hot. This should be conservative because the control rods will + # be modeled as slightly shorter with the correct hot density. Density + # is more important than height, so we are forcing density to be correct + # since we can't do axial expansion (yet) + axialExpChngr.applyColdHeightMassIncrease() axialExpChngr.expansionData.computeThermalExpansionFactors() axialExpChngr.axiallyExpandAssembly(thermal=True) # resolve axially disjoint mesh (if needed) diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py index cacad79e3e..b655802987 100644 --- a/armi/reactor/tests/test_blocks.py +++ b/armi/reactor/tests/test_blocks.py @@ -2282,7 +2282,6 @@ def test_coldMass(self): and hot height. """ fuel = self.b.getComponent(Flags.FUEL) - fuel.applyHotHeightDensityReduction() # set ref (input/cold) temperature. Thot = fuel.temperatureInC Tcold = fuel.inputTemperatureInC diff --git a/armi/reactor/tests/test_components.py b/armi/reactor/tests/test_components.py index edf7e3dae8..d5194357db 100644 --- a/armi/reactor/tests/test_components.py +++ b/armi/reactor/tests/test_components.py @@ -218,7 +218,6 @@ def test_getComponentArea(self): ) # show that area expansion is consistent with the density change in the material - self.component.applyHotHeightDensityReduction() hotDensity = self.component.density() hotArea = self.component.getArea() thermalExpansionFactor = self.component.getThermalExpansionFactor( @@ -234,7 +233,6 @@ def test_getComponentArea(self): area=math.pi, ) ) - coldComponent.applyHotHeightDensityReduction() coldDensity = coldComponent.density() coldArea = coldComponent.getArea() @@ -499,148 +497,152 @@ def test_changeNumberDensities(self): def test_demonstrateWaysToExpand(self): """Demonstrate that material is conserved at during expansion""" + # when comparing to 3D density, the comparison is not quite correct. + # We need a bigger delta, this will be investigated/fixed in another PR + biggerDelta = 0.01 # g/cc + ########### - # # 1 2D Expansion + # # 1 height taken as hot height (how components defined by default) ########### - # expansion only happens in 2D so only area is necessary + hotHeight = 1.0 + tWarm = 50 + coldOuterDiameter = 1.0 + circle1 = Circle("circle", "HT9", 20, tWarm, coldOuterDiameter) + tHot = 500 + circle2 = Circle("circle", "HT9", 20, tHot, coldOuterDiameter) + + # mass density is proportional to Fe number density and derived from + # all the number densities and atomic masses + self.assertAlmostEqual( + circle1.p.numberDensities["FE"] / circle2.p.numberDensities["FE"], + circle1.getMassDensity() / circle2.getMassDensity(), + ) - # when comparing to 3D density, the comparison is not quite correct. - # We need a bigger delta, this will be investigated/fixed in another PR - biggerDelta = 0.001 # g/cc - tCold = 50 - circle1 = Circle("circle", "HT9", 20, tCold, 1.0) - tHotC = 500 - circle2 = Circle("circle", "HT9", 20, tHotC, 1.0) + # the colder one has more because it is the same cold outer diameter + # but it would be taller at the same temperature + mass1 = circle1.getMassDensity() * circle1.getArea() * hotHeight + mass2 = circle2.getMassDensity() * circle2.getArea() * hotHeight + self.assertGreater(mass1, mass2) + + # they are off by factor of thermal exp self.assertAlmostEqual( - circle1.p.numberDensities["FE"] * circle1.getArea(), - circle2.p.numberDensities["FE"] * circle2.getArea(), + mass1 * circle1.getThermalExpansionFactor(), + mass2 * circle2.getThermalExpansionFactor(), ) + # material.density is the 2D density of a material # material.density3 is true density and not equal in this case # density must be density by calling applyHotHeightDensityReduction # or other methods (see rest of test). for circle in [circle1, circle2]: - self.assertAlmostEqual( + # 2D density is not equal after application of applyHotHeightDensityReduction + # which happens during construction + self.assertNotAlmostEqual( circle.getMassDensity(), circle.material.density(Tc=circle.temperatureInC), ) - # True density not equal because we expand in 2D - self.assertNotAlmostEqual( - circle.getMassDensity(), - circle.material.density3(Tc=circle.temperatureInC), - delta=biggerDelta, + # 2D density is off by the thermal exp factor + self.assertAlmostEqual( + circle.getMassDensity() * circle.getThermalExpansionFactor(), + circle.material.density(Tc=circle.temperatureInC), ) - # True density off by factor of thermal expansion - expFac = circle.getThermalExpansionFactor() self.assertAlmostEqual( - circle.getMassDensity() / expFac, + circle.getMassDensity(), circle.material.density3(Tc=circle.temperatureInC), delta=biggerDelta, ) # Change temp forward and backward and show equal oldArea = circle1.getArea() - circle1.setTemperature(tHotC) - self.assertAlmostEqual( - circle1.p.numberDensities["FE"], - circle2.p.numberDensities["FE"], + initialDens = circle1.getMassDensity() + # this math is done in applyHotHeightDensityReduction + applyHotHeightDensityReductionFactor = ( + 1.0 + + circle1.material.linearExpansionFactor( + circle1.temperatureInC, circle1.inputTemperatureInC + ) ) + factorToUndoHotHeight = circle1.getThermalExpansionFactor() self.assertAlmostEqual( - circle1.getMassDensity(), - circle1.material.density(Tc=circle2.temperatureInC), + applyHotHeightDensityReductionFactor, + factorToUndoHotHeight, ) - circle1.setTemperature(tCold) - self.assertAlmostEqual(oldArea, circle1.getArea()) + # when block.setHeight is called (which effectively changes component height) + # component.setNumberDensity is called (for solid isotopes) to adjust the number + # density so that now the 2D expansion will be approximated/expanded around + # the hot temp which is akin to these adjustments + + # undo the old applyHotHeightDensityReduction + circle1.changeNDensByFactor(factorToUndoHotHeight) + circle1.setTemperature(tHot) + circle1.applyHotHeightDensityReduction() # apply at new temp + + # now its density is same as hot component self.assertAlmostEqual( - circle1.p.numberDensities["FE"] * circle1.getArea(), - circle2.p.numberDensities["FE"] * circle2.getArea(), + circle1.getMassDensity(), + circle2.getMassDensity(), ) - ########### - # # 2 3D with applyHotHeightDensityReduction and equal hot height - ########### - hotHeight = 1.0 - circle1.applyHotHeightDensityReduction() - circle2.applyHotHeightDensityReduction() - # circle 1 has bigger mass because it has taller cold height and same - # cold radius - coldHeight1 = hotHeight / circle1.getThermalExpansionFactor() - coldHeight2 = hotHeight / circle2.getThermalExpansionFactor() - self.assertGreater(coldHeight1, coldHeight2) - self.assertGreater( - circle1.p.numberDensities["FE"] * circle1.getArea() * hotHeight, - circle2.p.numberDensities["FE"] * circle2.getArea() * hotHeight, + # show that mass is conserved after expansion + circle1NewHotHeight = ( + hotHeight * circle1.getThermalExpansionFactor() / factorToUndoHotHeight ) - # they are off in mass by a factor of thermal expansion self.assertAlmostEqual( - circle1.p.numberDensities["FE"] - * circle1.getArea() - * hotHeight - * circle1.getThermalExpansionFactor(), - circle2.p.numberDensities["FE"] - * circle2.getArea() - * hotHeight - * circle2.getThermalExpansionFactor(), + mass1, circle1.getMassDensity() * circle1.getArea() * circle1NewHotHeight ) - # Because of applyHotHeightDensityReduction the mass density is now - # Consistent with density3 but different from density (2D) by a factor of - # thermal expansion + # you can calculate the height exp factor directly this way self.assertAlmostEqual( - circle2.getMassDensity(), - circle2.material.density(Tc=circle2.temperatureInC) - / circle2.getThermalExpansionFactor(), + circle1.getThermalExpansionFactor() / factorToUndoHotHeight, + circle1.getThermalExpansionFactor(Tc=circle1.temperatureInC, T0=tWarm), ) + self.assertAlmostEqual( - circle2.getMassDensity(), - circle2.material.density3(Tc=circle2.temperatureInC), + circle1.getMassDensity(), + circle1.material.density3(Tc=circle2.temperatureInC), delta=biggerDelta, ) - coldMass = ( - circle2.material.density(Tc=circle2.inputTemperatureInC) - * circle2.getArea(cold=True) - * coldHeight2 - ) - hotMass = circle2.getArea() * hotHeight * circle2.getMassDensity() - self.assertAlmostEqual(coldMass, hotMass) - - # now change the temperature of Circle 2 - newHot = tHotC / 2.0 - # undo original applyHotHeightDensityReduction - axialExpansionFactor = 1.0 + circle2.material.linearExpansionFactor( - circle2.temperatureInC, circle2.inputTemperatureInC - ) - circle2.changeNDensByFactor(axialExpansionFactor) - # change temp - circle2.setTemperature(newHot) - densityAdjustment = circle2.getThermalExpansionFactor(Tc=newHot, T0=tHotC) - circle2.applyHotHeightDensityReduction() + # change back to old temp + circle1.changeNDensByFactor(circle1.getThermalExpansionFactor()) + circle1.setTemperature(tWarm) + circle1.applyHotHeightDensityReduction() + + # check for consistency + self.assertAlmostEqual(initialDens, circle1.getMassDensity()) + self.assertAlmostEqual(oldArea, circle1.getArea()) self.assertAlmostEqual( - circle2.getMassDensity(), - circle2.material.density3(Tc=circle2.temperatureInC), - delta=biggerDelta, + mass1, circle1.getMassDensity() * circle1.getArea() * hotHeight ) ########### - # # 3 "True" 3D start with cold or hot and show how quantity is - # conserved with inputHeightsConsideredHot + # # 2 height taken as cold height and show how quantity is + # conserved with inputHeightsConsideredHot = False ########### coldHeight = 1.0 - circle1 = Circle("circle", "HT9", 20, tCold, 1.0) - circle2 = Circle("circle", "HT9", 20, tHotC, 1.0) - circle1.setTemperature(500) # the should be the same now + circle1 = Circle("circle", "HT9", 20, tWarm, 1.0) + circle2 = Circle("circle", "HT9", 20, tHot, 1.0) + # same as 1 but we will make like 2 + circle1AdjustTo2 = Circle("circle", "HT9", 20, tWarm, 1.0) + + # make it hot like 2 + circle1AdjustTo2.changeNDensByFactor( + circle1AdjustTo2.getThermalExpansionFactor() + ) + circle1AdjustTo2.setTemperature(tHot) + circle1AdjustTo2.applyHotHeightDensityReduction() + # check that its like 2 + self.assertAlmostEqual( + circle2.getMassDensity(), circle1AdjustTo2.getMassDensity() + ) + self.assertAlmostEqual(circle2.getArea(), circle1AdjustTo2.getArea()) - for circle in [circle1, circle2]: + for circle in [circle1, circle2, circle1AdjustTo2]: - # when block.setHeight is called (which effectively changes component height) - # component.setNumberDensity is called (for solid isotopes) to adjust the number - # density so that now the 2D expansion will be approximated around the hot temp - circle.changeNDensByFactor(1 / circle.getThermalExpansionFactor()) self.assertAlmostEqual( circle.getMassDensity(), circle.material.density3(Tc=circle.temperatureInC), delta=biggerDelta, ) - # total mass consistent - # New height will be taller + # total mass consistent between hot and cold + # Hot height will be taller hotHeight = coldHeight * circle.getThermalExpansionFactor() self.assertAlmostEqual( coldHeight