Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move cold height expansion to bp #1047

Merged
merged 12 commits into from
Dec 29, 2022
12 changes: 7 additions & 5 deletions armi/bookkeeping/db/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,13 @@ def loadOperator(pathToDb, loadCycle, loadNode, allowMissing=False):
updateGlobalAssemblyNum(r)

o = thisCase.initializeOperator(r=r)
runLog.warning(
"The operator provided is not in the same state as the operator was.\n"
"When the reactor was at the prescribed cycle and node, it should have\n"
"access to the same interface stack, but the interfaces will also not be in the "
"same state.\n"
runLog.important(
"The operator will not be in the same state that it was at that cycle and "
"node, only the reactor.\n"
"The operator should have access to the same interface stack, but the "
"interfaces will not be in the same state (they will be fresh instances "
"of each interface as if __init__ was just called rather than the state "
"during the run at this time node.)\n"
"ARMI does not support loading operator states, as they are not stored."
)
return o
Expand Down
77 changes: 71 additions & 6 deletions armi/reactor/blueprints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@
from armi.reactor import assemblies
from armi.reactor import geometry
from armi.reactor import systemLayoutInput
from armi.reactor.flags import Flags
from armi.scripts import migration

from armi.utils import textProcessors

# NOTE: using non-ARMI-standard imports because these are all a part of this package,
Expand All @@ -102,6 +104,7 @@
from armi.reactor.blueprints.componentBlueprint import ComponentGroups
from armi.reactor.blueprints import isotopicOptions
from armi.reactor.blueprints.gridBlueprint import Grids, Triplet
from armi.reactor.converters.axialExpansionChanger import AxialExpansionChanger
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm

So, the primary blueprint code knows about the Axial Expansion?

But two-thirds of the projects that use ARMI, and half the ones at TerraPower, don't even have axial assemblies.

So, how does this code work for, say, a molten chloride reactor?

I ask for two reasons:

  1. ARMI has to work for all reactors, not just one.
  2. Axial Expansion is very slow, and for sure we don't want to instantiate all that unless we have to.

Copy link
Member Author

@onufer onufer Dec 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you look at the code it’s only called if input heights considered cold

Both before and after this change the same calls are made by the time o=armi.init is done running. o=armi.init is about the minimal thing you can do and the new code will be just as fast or faster for those circumstances (because its producing the correct assemblies before the deepcopy, rather than deepcopying and then fixing them all after....

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a fair point. Okay, so we aren't breaking non-axial-type reactors.

But we are importing something that is specific to a TYPE of reactor (albeit a common one) into a part of the code that (right now) reads YAML files and builds some math.

It just feels like something that should be in assemblyBlueprint.py. But, I assume, it was just hard to work into that module?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you need to know the meshes of each of the individual assemblies to go through the processes since they all have their indices mapped to the reference assembly (see a.makeAxialSnapList(referenceAssembly)). I dont see anywhere in that module where there are the full set of assemblies. The nice thing about doing it here is that the blueprints assemblies will exists for the shortest possible time ad the incorrect dimensions/masses since they are fixed right after construction (line 322)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you want, right after the area consistancy check i could move all the new code to
axialExpansionChanger

and have it be

if not cs["detailedAxialExpansion"]:
    axialExpansionChanger.makeAssembliesSnapToUniformMesh(self.assemblies.values(), cs["nonUniformAssemFlags"])
if not cs["inputHeightsConsideredHot"]:
    axialExpansionChanger.expandColdDimsToHot(self.assemblies.values(),  cs["detailedAxialExpansion"])

Fundamentally though, blueprints should be responsible for producing assemblies with correct masses and elevations. It can certainly call someone else to do the work though

Copy link
Member

@john-science john-science Dec 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I see the problem you're trying to solve.
  2. I love that you moved the axial expansion logic into that module.
  3. Taking a step back, I think blueprints/__init__.py already has many direct references to assemblies at the top level. So I think you're not causing a design flaw so much as stuck within the system we created.
  4. Let's explore your solution. The worst case is that each special type of reactor needs a bunch of these if cs["settingName"] statements, which point to some non-blueprint method. Unless that list gets long, that's probably going to look a little ugly, but not be heart-breakingly bad.
  5. Sure, after moving the logic to the axial expansion module, I'm convinced.

Sorry, I'm not trying to be a bother. I have just learned by hard experience that improving something at the start can save a TON of time spent on technical debt later. I keep meaning to give a talk on that.

Copy link
Member Author

@onufer onufer Dec 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great. thanks for clarifying @john-science. I genuinely don't think this list of things will grow considerably... I would recommend describing molten salt reactors and TRISO-based fuel as "non-pin-based" or "non-assembly-based" as opposed to "non-axial-type reactors".

Molten salt reactors still typically care about axial expansion of their vessel and the impact of that on reactivity (I spent a couple years on a molten salt project). Its just that the framework currently doesn't support it. Its not a trivial feature though and its a bit different from a fuel pin expanding. It took a couple hundred hours for us to get cold to hot dims working for pin-based-reactors. I am sure that if we had the capability to input cold vessel dims and have the framework expand the vessel to hot, molten salt reactors would want to use the capability. Its just we don't have it now, so those folks tend to continue to use hot axial heights.

If/when we have this capability, I would image we would change the call to expandColdDimsToHot to determine a different changer to use for non-pin based reactors rather than having another if statement here in blueprints.

Sure, after moving the logic to the axial expansion module, I'm convinced. what more do you need to approve the PR?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend describing molten salt reactors and TRISO-based fuel as "non-pin-based" or "non-assembly-based" as opposed to "non-axial-type reactors".

Thanks. And, always, I am interested in learning more about reactor design/analysis. Sorry, my graduate degrees are in physics, but not the right kind. I'm happy to learn.

what more do you need to approve the PR?

I added one little comment about a docstring, but I'm good otherwise. The logic looks sound, and I like the new layout better.

Thanks, man!


context.BLUEPRINTS_IMPORTED = True
context.BLUEPRINTS_IMPORT_CONTEXT = "".join(traceback.format_stack())
Expand Down Expand Up @@ -321,18 +324,80 @@ def _prepConstruction(self, cs):
self.assemblies[aDesign.name] = a
if currentCount != 0:
assemblies.setAssemNumCounter(currentCount)

self._checkAssemblyAreaConsistency(cs)

runLog.header("=========== Verifying Assembly Configurations ===========")
self._checkAssemblyAreaConsistency(cs)

# pylint: disable=no-member
getPluginManagerOrFail().hook.afterConstructionOfAssemblies(
assemblies=self.assemblies.values(), cs=cs
# if assemblies are defined in blueprints, handle meshing
# assume finest mesh is reference
assemsByNumBlocks = sorted(
john-science marked this conversation as resolved.
Show resolved Hide resolved
self.assemblies.values(),
key=lambda a: len(a),
reverse=True,
)
referenceAssembly = assemsByNumBlocks[0] if assemsByNumBlocks else None

if not cs["detailedAxialExpansion"]:
# make the snap lists so assems know how to expand
nonUniformAssems = [
Flags.fromStringIgnoreErrors(t) for t in cs["nonUniformAssemFlags"]
]
# prepare core for mesh snapping during axial expansion
for a in self.assemblies.values():
if any(a.hasFlags(f) for f in nonUniformAssems):
continue
a.makeAxialSnapList(referenceAssembly)
if not cs["inputHeightsConsideredHot"]:
# expand axial heights from cold to hot
self._coldDimsToHot(referenceAssembly, cs["detailedAxialExpansion"])
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@john-science happy to move this stuff else where and have a 1 line call to hit here.


# pylint: disable=no-member
getPluginManagerOrFail().hook.afterConstructionOfAssemblies(
assemblies=self.assemblies.values(), cs=cs
)

self._prepped = True

def _coldDimsToHot(
self,
referenceAssembly,
isDetailedAxialExpansion,
):
"""
Expand BOL assemblies, resolve disjoint axial mesh (if needed), and update block BOL heights

Parameters
----------
assems: list
list of :py:class:`Assembly <armi.reactor.assemblies.Assembly>` objects to be thermally expanded
"""
runLog.header(
"=========== Axially expanding all assemblies (except control) from Tinput to Thot ==========="
)
assems = list(self.assemblies.values())

axialExpChanger = AxialExpansionChanger(isDetailedAxialExpansion)
for a in assems:
if not a.hasFlags(Flags.CONTROL):
axialExpChanger.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)
axialExpChanger.applyColdHeightMassIncrease()
axialExpChanger.expansionData.computeThermalExpansionFactors()
axialExpChanger.axiallyExpandAssembly(thermal=True)
if not isDetailedAxialExpansion:
for a in assems:
if not a.hasFlags(Flags.CONTROL):
a.setBlockMesh(referenceAssembly.getAxialMesh())
# update block BOL heights to reflect hot heights
for a in assems:
if not a.hasFlags(Flags.CONTROL):
for b in a:
b.p.heightBOL = b.getHeight()
b.completeInitialLoading()

def _assignTypeNums(self):
if self.blockDesigns is None:
# this happens when directly defining assemblies.
Expand Down
84 changes: 4 additions & 80 deletions armi/reactor/reactors.py
Original file line number Diff line number Diff line change
Expand Up @@ -2239,52 +2239,17 @@ def processLoading(self, cs, dbLoad: bool = False):
"Please make sure that this is intended and not a input error."
)

nonUniformAssems = [
Flags.fromStringIgnoreErrors(t) for t in cs["nonUniformAssemFlags"]
]
if dbLoad:
# reactor.blueprints.assemblies need to be populated
# this normally happens during armi/reactor/blueprints/__init__.py::constructAssem
# but for DB load, this is not called so it must be here.
# pylint: disable=protected-access
self.parent.blueprints._prepConstruction(cs)
if not cs["detailedAxialExpansion"]:
# Apply mesh snapping for self.parent.blueprints.assemblies
# This is stored as a param for assemblies in-core, so only blueprints assemblies are
# considered here. To guarantee mesh snapping will function, makeAxialSnapList
# should be in reference to the assembly with the finest mesh as defined in the blueprints.
finestMeshAssembly = sorted(
self.parent.blueprints.assemblies.values(),
key=lambda a: len(a),
reverse=True,
)[0]
for a in self.parent.blueprints.assemblies.values():
if any(a.hasFlags(f) for f in nonUniformAssems):
continue
a.makeAxialSnapList(refAssem=finestMeshAssembly)
if not cs["inputHeightsConsideredHot"]:
runLog.header(
"=========== Axially expanding blueprints assemblies (except control) from Tinput to Thot ==========="
)
self._applyThermalExpansion(
self.parent.blueprints.assemblies.values(),
dbLoad,
finestMeshAssembly,
)

else:
if not cs["detailedAxialExpansion"]:
# prepare core for mesh snapping during axial expansion
for a in self.getAssemblies(includeAll=True):
if any(a.hasFlags(f) for f in nonUniformAssems):
continue
a.makeAxialSnapList(self.refAssem)
if not cs["inputHeightsConsideredHot"]:
runLog.header(
"=========== Axially expanding all assemblies (except control) from Tinput to Thot ==========="
)
self._applyThermalExpansion(self.getAssemblies(includeAll=True), dbLoad)

# set reactor level meshing params
nonUniformAssems = [
Flags.fromStringIgnoreErrors(t) for t in cs["nonUniformAssemFlags"]
]
# some assemblies, like control assemblies, have a non-conforming mesh
# and should not be included in self.p.referenceBlockAxialMesh and self.p.axialMesh
uniformAssems = [
Expand Down Expand Up @@ -2360,44 +2325,3 @@ def buildManualZones(self, cs):

if not len(self.zones):
runLog.debug("No manual zones defined in `zoneDefinitions` setting")

def _applyThermalExpansion(
self, assems: list, dbLoad: bool, referenceAssembly=None
):
"""expand assemblies, resolve disjoint axial mesh (if needed), and update block BOL heights

Parameters
----------
assems: list
list of :py:class:`Assembly <armi.reactor.assemblies.Assembly>` objects to be thermally expanded
dbLoad: bool
boolean to determine if Core::processLoading is loading a database or constructing a Core
referenceAssembly: optional, :py:class:`Assembly <armi.reactor.assemblies.Assembly>`
is the thermally expanded assembly whose axial mesh is used to snap the
blueprints assemblies axial mesh to
"""
axialExpChngr = AxialExpansionChanger(self._detailedAxialExpansion)
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)
if not dbLoad:
axialExpChngr.manageCoreMesh(self.parent)
elif not self._detailedAxialExpansion:
for a in assems:
if not a.hasFlags(Flags.CONTROL):
a.setBlockMesh(referenceAssembly.getAxialMesh())
# update block BOL heights to reflect hot heights
for a in assems:
if not a.hasFlags(Flags.CONTROL):
for b in a:
b.p.heightBOL = b.getHeight()
b.completeInitialLoading()
76 changes: 32 additions & 44 deletions armi/reactor/tests/test_reactors.py
Original file line number Diff line number Diff line change
Expand Up @@ -880,86 +880,74 @@ def test_nonUniformAssems(self):
self.assertEqual(originalHeights, heights)

def test_applyThermalExpansion_CoreConstruct(self):
"""test Core::_applyThermalExpansion for core construction
"""test that assemblies in core are correctly expanded.

Notes:
------
- all assertions skip the first block as it has no $\Delta T$ and does not expand
- to maintain code coverage, _applyThermalExpansion is called via processLoading
"""
self.o.cs["inputHeightsConsideredHot"] = False
assemsToChange = self.r.core.getAssemblies()
originalAssems = self.r.core.getAssemblies()
# stash original axial mesh info
oldRefBlockAxialMesh = self.r.core.p.referenceBlockAxialMesh
oldAxialMesh = self.r.core.p.axialMesh

nonEqualParameters = ["heightBOL", "molesHmBOL", "massHmBOL"]
equalParameters = ["smearDensity", "nHMAtBOL", "enrichmentBOL"]

oldBlockParameters = {}
for param in nonEqualParameters + equalParameters:
oldBlockParameters[param] = {}
for a in assemsToChange:
for b in a[1:]:
oldBlockParameters[param][b] = b.p[param]
_o, coldHeightR = loadTestReactor(
self.directoryChanger.destination,
customSettings={"inputHeightsConsideredHot": False},
)

self.r.core.processLoading(self.o.cs, dbLoad=False)
for i, val in enumerate(oldRefBlockAxialMesh[1:]):
self.assertNotEqual(val, self.r.core.p.referenceBlockAxialMesh[i])
self.assertNotEqual(val, coldHeightR.core.p.referenceBlockAxialMesh[i])
for i, val in enumerate(oldAxialMesh[1:]):
self.assertNotEqual(val, self.r.core.p.axialMesh[i])
self.assertNotEqual(val, coldHeightR.core.p.axialMesh[i])

for a in assemsToChange:
coldHeightAssems = coldHeightR.core.getAssemblies()
for a, coldHeightA in zip(originalAssems, coldHeightAssems):
if not a.hasFlags(Flags.CONTROL):
for b in a[1:]:
for b, coldHeightB in zip(a[1:], coldHeightA[1:]):
for param in nonEqualParameters:
if oldBlockParameters[param][b] and b.p[param]:
p, coldHeightP = b.p[param], coldHeightB.p[param]
if p and coldHeightP:
self.assertNotEqual(
oldBlockParameters[param][b], b.p[param]
p, coldHeightP, f"{param} {p} {coldHeightP}"
)
else:
self.assertAlmostEqual(
oldBlockParameters[param][b], b.p[param]
)
self.assertAlmostEqual(p, coldHeightP)
for param in equalParameters:
self.assertAlmostEqual(oldBlockParameters[param][b], b.p[param])
p, coldHeightP = b.p[param], coldHeightB.p[param]
self.assertAlmostEqual(p, coldHeightP)

def test_updateBlockBOLHeights_DBLoad(self):
"""test Core::_applyThermalExpansion for db load
"""Test that blueprints assemblies are expanded in DB load.

Notes:
------
- all assertions skip the first block as it has no $\Delta T$ and does not expand
- to maintain code coverage, _applyThermalExpansion is called via processLoading
"""
self.o.cs["inputHeightsConsideredHot"] = False
assemsToChange = [a for a in self.r.blueprints.assemblies.values()]
# stash original blueprint assemblies axial mesh info
originalAssems = sorted(a for a in self.r.blueprints.assemblies.values())
nonEqualParameters = ["heightBOL", "molesHmBOL", "massHmBOL"]
equalParameters = ["smearDensity", "nHMAtBOL", "enrichmentBOL"]
oldBlockParameters = {}
for param in nonEqualParameters + equalParameters:
oldBlockParameters[param] = {}
for a in assemsToChange:
for b in a[1:]:
oldBlockParameters[param][b] = b.p[param]

self.r.core.processLoading(self.o.cs, dbLoad=True)

for a in assemsToChange:
_o, coldHeightR = loadTestReactor(
self.directoryChanger.destination,
customSettings={"inputHeightsConsideredHot": False},
)
coldHeightAssems = sorted(a for a in coldHeightR.blueprints.assemblies.values())
for a, coldHeightA in zip(originalAssems, coldHeightAssems):
if not a.hasFlags(Flags.CONTROL):
for b in a[1:]:
for b, coldHeightB in zip(a[1:], coldHeightA[1:]):
for param in nonEqualParameters:
if oldBlockParameters[param][b] and b.p[param]:
self.assertNotEqual(
oldBlockParameters[param][b], b.p[param]
)
p, coldHeightP = b.p[param], coldHeightB.p[param]
if p and coldHeightP:
self.assertNotEqual(p, coldHeightP)
else:
self.assertAlmostEqual(
oldBlockParameters[param][b], b.p[param]
)
self.assertAlmostEqual(p, coldHeightP)
for param in equalParameters:
self.assertAlmostEqual(oldBlockParameters[param][b], b.p[param])
p, coldHeightP = b.p[param], coldHeightB.p[param]
self.assertAlmostEqual(p, coldHeightP)

def test_buildManualZones(self):
# define some manual zones in the settings
Expand Down