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

Providing plugin hook getAxialExpansionChanger #1870

Merged
merged 19 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bca8a6a
Move expandColdDimsToHot to AxialExpansionChanger classmethod
drewj-tp Sep 9, 2024
c334aca
Add plugin hook getAxialExpansionChanger
drewj-tp Sep 9, 2024
344f5a5
Implement ReactorPlugin.getAxialExpansionChanger hook
drewj-tp Sep 9, 2024
054f6c0
Use getAxialExpanionChanger hook in blueprints construction
drewj-tp Sep 9, 2024
190f815
Remove self from staticmethod hookspec defineSystemBuilders
drewj-tp Sep 9, 2024
57a860b
Add release notes on getAxialExpansionChanger hook
drewj-tp Sep 12, 2024
0416cb7
Add test for axial expansion changer hook
drewj-tp Sep 12, 2024
9dbbca2
Document that one additional plugin per app should implement getAxial…
drewj-tp Sep 17, 2024
c5185af
Apply suggestions from code review
drewj-tp Sep 17, 2024
b1270c9
Add docs and comments for posterity in plugin tests
drewj-tp Sep 17, 2024
b2ff1ee
Merge remote-tracking branch 'refs/remotes/origin/drewj/ax-exp-hook/1…
drewj-tp Sep 17, 2024
bb01240
Remove confusing doc reference to ReactorPlugin in getAxialExpanderHook
drewj-tp Sep 17, 2024
6084c17
Update doc/release/0.4.rst
drewj-tp Sep 17, 2024
896e472
Use getPluginManagerOrFail in blueprints/__init__.py
drewj-tp Sep 17, 2024
86be86c
Allow getAxialExpansionChanger to be None in Blueprints
drewj-tp Sep 17, 2024
042df0e
Update armi/tests/test_plugins.py
john-science Sep 18, 2024
577a91f
Merge branch 'main' into drewj/ax-exp-hook/1843
drewj-tp Sep 18, 2024
4618602
Merge remote-tracking branch 'origin/main' into drewj/ax-exp-hook/1843
drewj-tp Sep 20, 2024
baf1055
Merge remote-tracking branch 'origin/main' into drewj/ax-exp-hook/1843
drewj-tp Sep 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions armi/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
# https://coverage.readthedocs.io/en/stable/excluding.html
if TYPE_CHECKING: # pragma: no cover
from armi.reactor.composites import Composite
from armi.reactor.converters.axialExpansionChanger import AxialExpansionChanger


HOOKSPEC = pluggy.HookspecMarker("armi")
Expand Down Expand Up @@ -665,6 +666,47 @@ def defineSystemBuilders() -> Dict[str, Callable[[str], "Composite"]]:
and a ``"sfp"`` lookup, triggered to run after all other hooks have been run.
"""

@staticmethod
@HOOKSPEC(firstresult=True)
def getAxialExpansionChanger() -> type["AxialExpansionChanger"]:
"""Produce the class responsible for performing axial expansion.

Plugins can provide this hook to override or negate axial expansion.
Will be used during initial construction of the core and assemblies, and
can be a class to perform custom axial expansion routines.

The first object returned that is not ``None`` will be used.
Plugins are encouraged to add the ``tryfirst=True`` arguments to their
``HOOKIMPL`` invocations to make sure their specific are earlier in the
hook call sequence. The default reactor plugin
:class:`armi.reactor.ReactorPlugin` is marked ``trylast=True`` to help
put other implementations earlier in the sequence.
albeanth marked this conversation as resolved.
Show resolved Hide resolved

Returns
-------
type of :class:`armi.reactor.converters.axialExpansionChanger.AxialExpansionChanger`

Notes
-----
This hook **should not** provide an instance of the class. The construction
of the changer will be handled by applications and plugins that need it.

This hook should only be provided by one additional plugin in your application. Otherwise
the `order of hook execution <https://pluggy.readthedocs.io/en/stable/index.html#call-time-order>`_
may not provide the behavior you expect.

Examples
--------
>>> class MyPlugin(ArmiPlugin):
... @staticmethod
... @HOOKIMPL(tryfirst=True)
... def getAxialExpansionChanger():
... from myproject.physics import BespokeAxialExpansion
...
... return BespokeAxialExpansion

"""


class UserPlugin(ArmiPlugin):
"""
Expand Down
13 changes: 10 additions & 3 deletions armi/reactor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,20 @@ def defineAssemblyTypes():

@staticmethod
@plugins.HOOKIMPL(trylast=True)
def defineSystemBuilders() -> Dict[
str, Callable[[str], Union["Core", "SpentFuelPool"]]
]:
def defineSystemBuilders() -> (
Dict[str, Callable[[str], Union["Core", "SpentFuelPool"]]]
):
from armi.reactor.reactors import Core
from armi.reactor.assemblyLists import SpentFuelPool

return {
"core": Core,
"sfp": SpentFuelPool,
}

@staticmethod
@plugins.HOOKIMPL(trylast=True)
def getAxialExpansionChanger():
from armi.reactor.converters.axialExpansionChanger import AxialExpansionChanger

return AxialExpansionChanger
5 changes: 3 additions & 2 deletions armi/reactor/blueprints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def __new__(mcs, name, bases, attrs):
else:
pluginSections = pm.hook.defineBlueprintsSections()
for plug in pluginSections:
for (attrName, section, resolver) in plug:
for attrName, section, resolver in plug:
assert isinstance(section, yamlize.Attribute)
if attrName in attrs:
raise plugins.PluginError(
Expand Down Expand Up @@ -326,7 +326,8 @@ def _prepConstruction(self, cs):
for a in list(self.assemblies.values())
if not any(a.hasFlags(f) for f in assemsToSkip)
)
axialExpansionChanger.axialExpansionChanger.expandColdDimsToHot(
chngrT = getPluginManager().hook.getAxialExpansionChanger()
albeanth marked this conversation as resolved.
Show resolved Hide resolved
drewj-tp marked this conversation as resolved.
Show resolved Hide resolved
chngrT.expandColdDimsToHot(
john-science marked this conversation as resolved.
Show resolved Hide resolved
assemsToExpand,
cs[CONF_DETAILED_AXIAL_EXPANSION],
)
Expand Down
130 changes: 65 additions & 65 deletions armi/reactor/converters/axialExpansionChanger/axialExpansionChanger.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,71 +52,6 @@ def makeAssemsAbleToSnapToUniformMesh(
a.makeAxialSnapList(referenceAssembly)


def expandColdDimsToHot(
assems: list,
isDetailedAxialExpansion: bool,
referenceAssembly=None,
):
"""
Expand BOL assemblies, resolve disjoint axial mesh (if needed), and update block BOL heights.

.. impl:: Perform expansion during core construction based on block heights at a specified temperature.
:id: I_ARMI_INP_COLD_HEIGHT
:implements: R_ARMI_INP_COLD_HEIGHT

This method is designed to be used during core construction to axially thermally expand the
assemblies to their "hot" temperatures (as determined by ``Thot`` values in blueprints).
First, The Assembly is prepared for axial expansion via ``setAssembly``. In
``applyColdHeightMassIncrease``, the number densities on each Component is adjusted to
reflect that Assembly inputs are at cold (i.e., ``Tinput``) temperatures. To expand to
the requested hot temperatures, thermal expansion factors are then computed in
``computeThermalExpansionFactors``. Finally, the Assembly is axially thermally expanded in
``axiallyExpandAssembly``.

If the setting ``detailedAxialExpansion`` is ``False``, then each Assembly gets its Block mesh
set to match that of the "reference" Assembly (see ``getDefaultReferenceAssem`` and ``setBlockMesh``).

Once the Assemblies are axially expanded, the Block BOL heights are updated. To account for the change in
Block volume from axial expansion, ``completeInitialLoading`` is called to update any volume-dependent
Block information.

Parameters
----------
assems: list[:py:class:`Assembly <armi.reactor.assemblies.Assembly>`]
list of assemblies to be thermally expanded
isDetailedAxialExpansion: bool
If False, assemblies will be forced to conform to the reference mesh after expansion
referenceAssembly: :py:class:`Assembly <armi.reactor.assemblies.Assembly>`, optional
Assembly whose mesh other meshes will conform to if isDetailedAxialExpansion is False.
If not provided, will assume the finest mesh assembly which is typically fuel.

Notes
-----
Calling this method will result in an increase in mass via applyColdHeightMassIncrease!

See Also
--------
:py:meth:`armi.reactor.converters.axialExpansionChanger.axialExpansionChanger.AxialExpansionChanger.applyColdHeightMassIncrease`
"""
assems = list(assems)
if not referenceAssembly:
referenceAssembly = getDefaultReferenceAssem(assems)
axialExpChanger = AxialExpansionChanger(isDetailedAxialExpansion)
for a in assems:
axialExpChanger.setAssembly(a, expandFromTinputToThot=True)
axialExpChanger.applyColdHeightMassIncrease()
axialExpChanger.expansionData.computeThermalExpansionFactors()
axialExpChanger.axiallyExpandAssembly()
if not isDetailedAxialExpansion:
for a in assems:
a.setBlockMesh(referenceAssembly.getAxialMesh())
# update block BOL heights to reflect hot heights
for a in assems:
for b in a:
b.p.heightBOL = b.getHeight()
b.completeInitialLoading()


class AxialExpansionChanger:
"""
Axially expand or contract assemblies or an entire core.
Expand Down Expand Up @@ -148,6 +83,71 @@ def __init__(self, detailedAxialExpansion: bool = False):
self.linked = None
self.expansionData = None

@classmethod
def expandColdDimsToHot(
cls,
assems: list,
isDetailedAxialExpansion: bool,
referenceAssembly=None,
):
"""Expand BOL assemblies, resolve disjoint axial mesh (if needed), and update block BOL heights.

.. impl:: Perform expansion during core construction based on block heights at a specified temperature.
:id: I_ARMI_INP_COLD_HEIGHT
:implements: R_ARMI_INP_COLD_HEIGHT

This method is designed to be used during core construction to axially thermally expand the
assemblies to their "hot" temperatures (as determined by ``Thot`` values in blueprints).
First, The Assembly is prepared for axial expansion via ``setAssembly``. In
``applyColdHeightMassIncrease``, the number densities on each Component is adjusted to
reflect that Assembly inputs are at cold (i.e., ``Tinput``) temperatures. To expand to
the requested hot temperatures, thermal expansion factors are then computed in
``computeThermalExpansionFactors``. Finally, the Assembly is axially thermally expanded in
``axiallyExpandAssembly``.

If the setting ``detailedAxialExpansion`` is ``False``, then each Assembly gets its Block mesh
set to match that of the "reference" Assembly (see ``getDefaultReferenceAssem`` and ``setBlockMesh``).

Once the Assemblies are axially expanded, the Block BOL heights are updated. To account for the change in
Block volume from axial expansion, ``completeInitialLoading`` is called to update any volume-dependent
Block information.

Parameters
----------
assems: list[:py:class:`Assembly <armi.reactor.assemblies.Assembly>`]
list of assemblies to be thermally expanded
isDetailedAxialExpansion: bool
If False, assemblies will be forced to conform to the reference mesh after expansion
referenceAssembly: :py:class:`Assembly <armi.reactor.assemblies.Assembly>`, optional
Assembly whose mesh other meshes will conform to if isDetailedAxialExpansion is False.
If not provided, will assume the finest mesh assembly which is typically fuel.

Notes
-----
Calling this method will result in an increase in mass via applyColdHeightMassIncrease!

See Also
--------
:py:meth:`armi.reactor.converters.axialExpansionChanger.axialExpansionChanger.AxialExpansionChanger.applyColdHeightMassIncrease`
"""
assems = list(assems)
if not referenceAssembly:
referenceAssembly = getDefaultReferenceAssem(assems)
axialExpChanger = cls(isDetailedAxialExpansion)
john-science marked this conversation as resolved.
Show resolved Hide resolved
for a in assems:
axialExpChanger.setAssembly(a, expandFromTinputToThot=True)
axialExpChanger.applyColdHeightMassIncrease()
axialExpChanger.expansionData.computeThermalExpansionFactors()
axialExpChanger.axiallyExpandAssembly()
if not isDetailedAxialExpansion:
for a in assems:
a.setBlockMesh(referenceAssembly.getAxialMesh())
# update block BOL heights to reflect hot heights
for a in assems:
for b in a:
b.p.heightBOL = b.getHeight()
b.completeInitialLoading()

def performPrescribedAxialExpansion(
self, a, componentLst: list, percents: list, setFuel=True
):
Expand Down
31 changes: 29 additions & 2 deletions armi/tests/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from armi import plugins
from armi import settings
from armi import utils
from armi.reactor.converters.axialExpansionChanger import AxialExpansionChanger
from armi.physics.neutronics import NeutronicsPlugin
from armi.reactor.blocks import Block
from armi.reactor.flags import Flags
Expand All @@ -42,13 +43,27 @@ def defineFlags():
return {"SUPER_FLAG": utils.flags.auto()}


class SillyAxialExpansionChanger(AxialExpansionChanger):
"""Fake, test-specific axial expansion changer that a plugin will register."""


class SillyAxialPlugin(plugins.ArmiPlugin):
"""Trivial plugin that implements the axial expansion hook."""

@staticmethod
@plugins.HOOKIMPL
def getAxialExpansionChanger() -> type[SillyAxialExpansionChanger]:
return SillyAxialExpansionChanger

albeanth marked this conversation as resolved.
Show resolved Hide resolved

class TestPluginRegistration(unittest.TestCase):
def setUp(self):
"""
Manipulate the standard App. We can't just configure our own, since the
pytest environment bleeds between tests.
"""
self._backupApp = deepcopy(getApp())
self.app = getApp()
self._backupApp = deepcopy(self.app)
john-science marked this conversation as resolved.
Show resolved Hide resolved

def tearDown(self):
"""Restore the App to its original state."""
Expand All @@ -68,7 +83,7 @@ def test_defineFlags(self):
:id: T_ARMI_PLUGIN_REGISTER
:tests: R_ARMI_PLUGIN
"""
app = getApp()
app = self.app
john-science marked this conversation as resolved.
Show resolved Hide resolved

# show the new plugin isn't loaded yet
pluginNames = [p[0] for p in app.pluginManager.list_name_plugin()]
Expand All @@ -92,6 +107,18 @@ def test_defineFlags(self):
# show the flag exists now
self.assertEqual(type(Flags.SUPER_FLAG._value), int)

def test_axialExpansionHook(self):
"""Test that plugins can override the axial expansion of assemblies via a hook."""
pm = self.app.pluginManager
first = pm.hook.getAxialExpansionChanger()
# By default, make sure we get the armi-shipped expansion class
self.assertIs(first, AxialExpansionChanger)
pm.register(SillyAxialPlugin)
albeanth marked this conversation as resolved.
Show resolved Hide resolved
second = pm.hook.getAxialExpansionChanger()
# Registering a plugin that implements the hook means we get
# that plugin's axial expander
self.assertIs(second, SillyAxialExpansionChanger)


class TestPluginBasics(unittest.TestCase):
def test_defineParameters(self):
Expand Down
2 changes: 2 additions & 0 deletions doc/release/0.4.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ New Features
#. Removing the ``tabulate`` dependency by ingesting it to ``armi.utils.tabulate``. (`PR#1811 <https://github.com/terrapower/armi/pull/1811>`_)
#. Adding ``--skip-inspection`` flag to ``CompareCases`` CLI. (`PR#1842 <https://github.com/terrapower/armi/pull/1842>`_)
#. Allow merging a component with zero area into another component (`PR#1858 <https://github.com/terrapower/armi/pull/1858>`_)
#. Plugins can provide the ``getAxialExpansionChanger`` hook to customize axial expansion
drewj-tp marked this conversation as resolved.
Show resolved Hide resolved
(`PR#1870 <https://github.com/terrapower/armi/pull/1870`_)
#. TBD

API Changes
Expand Down
Loading