Skip to content

Commit

Permalink
Merging in main branch - reqs revamp
Browse files Browse the repository at this point in the history
  • Loading branch information
john-science committed Feb 7, 2025
2 parents 59be4e7 + 6febc65 commit 5fb8e8b
Show file tree
Hide file tree
Showing 37 changed files with 253 additions and 714 deletions.
39 changes: 10 additions & 29 deletions armi/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,35 +115,17 @@ def pluginManager(self) -> pluginManager.ArmiPluginManager:
return self._pm

def getSettings(self) -> Dict[str, Setting]:
"""
Return a dictionary containing all Settings defined by the framework and all plugins.
.. impl:: Applications will not allow duplicate settings.
:id: I_ARMI_SETTINGS_UNIQUE
:implements: R_ARMI_SETTINGS_UNIQUE
Each ARMI application includes a collection of Plugins. Among other
things, these plugins can register new settings in addition to
the default settings that come with ARMI. This feature provides a
lot of utility, so application developers can easily configure
their ARMI appliction in customizable ways.
However, it would get confusing if two different plugins registered
a setting with the same name string. Or if a plugin registered a
setting with the same name as an ARMI default setting. So this
method throws an error if such a situation arises.
"""
"""Return a dictionary containing all Settings defined by the framework and all plugins."""
# Start with framework settings
settingDefs = {
setting.name: setting for setting in fwSettings.getFrameworkSettings()
}

# The optionsCache stores options that may have come from a plugin before the
# setting to which they apply. Whenever a new setting is added, we check to see
# if there are any options in the cache, popping them out and adding them to the
# setting. If all plugins' settings have been processed and the cache is not
# empty, that's an error, because a plugin must have provided options to a
# setting that doesn't exist.
# The optionsCache stores options that may have come from a plugin before the setting to
# which they apply. Whenever a new setting is added, we check to see if there are any
# options in the cache, popping them out and adding them to the setting. If all plugins'
# settings have been processed and the cache is not empty, that's an error, because a plugin
# must have provided options to a setting that doesn't exist.
optionsCache: Dict[str, List[settings.Option]] = collections.defaultdict(list)
defaultsCache: Dict[str, settings.Default] = {}

Expand Down Expand Up @@ -205,11 +187,10 @@ def getParamRenames(self) -> Dict[str, str]:
"""
Return the parameter renames from all registered plugins.
This renders a merged dictionary containing all parameter renames from all of
the registered plugins. It also performs simple error checking. The result of
this operation is cached, since it is somewhat expensive to perform. If the App
detects that its plugin manager's set of registered plugins has changed, the
cache will be invalidated and recomputed.
This renders a merged dictionary containing all parameter renames from all of the registered
plugins. It also performs simple error checking. The result of this operation is cached,
since it is somewhat expensive to perform. If the App detects that its plugin manager's set
of registered plugins has changed, the cache will be invalidated and recomputed.
"""
cacheInvalid = False
if self._paramRenames is not None:
Expand Down
7 changes: 3 additions & 4 deletions armi/bookkeeping/db/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -724,17 +724,16 @@ def load(
is read from the database.
.. impl:: Users can load a reactor from a DB.
:id: I_ARMI_DB_R_LOAD
:implements: R_ARMI_DB_R_LOAD
:id: I_ARMI_DB_TIME1
:implements: R_ARMI_DB_TIME
This method creates a ``Reactor`` object by reading the reactor state out
of an ARMI database file. This is done by passing in mandatory arguements
that specify the exact place in time you want to load the reactor from.
(That is, the cycle and node numbers.) Users can either pass the settings
and blueprints directly into this method, or it will attempt to read them
from the database file. The primary work done here is to read the hierarchy
of reactor objects from the data file, then reconstruct them in the correct
order.
of reactor objects from the data file, then reconstruct them in the correct order.
Parameters
----------
Expand Down
5 changes: 2 additions & 3 deletions armi/bookkeeping/db/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ def writeToDB(self, h5group):
"""Write a chunk of data to the database.
.. impl:: Write data to the DB for a given time step.
:id: I_ARMI_DB_TIME
:id: I_ARMI_DB_TIME0
:implements: R_ARMI_DB_TIME
This method writes a snapshot of the current state of the reactor to the
Expand All @@ -393,8 +393,7 @@ def writeToDB(self, h5group):
objects are written to the file. Though, this turns out to still be very
powerful. For instance, the data for all ``HexBlock`` children of a given
parent are stored contiguously within the ``HexBlock`` group, and will not
be interleaved with data from the ``HexBlock`` children of any of the
parent's siblings.
be interleaved with data from the ``HexBlock`` children of any of the parent's siblings.
"""
if "layout/type" in h5group:
# It looks like we have already written the layout to DB, skip for now
Expand Down
6 changes: 3 additions & 3 deletions armi/bookkeeping/db/tests/test_database3.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ def test_load(self):
"""Load a reactor at different time steps, from the database.
.. test:: Load the reactor from the database.
:id: T_ARMI_DB_R_LOAD
:tests: R_ARMI_DB_R_LOAD
:id: T_ARMI_DB_TIME1
:tests: R_ARMI_DB_TIME
"""
self.makeShuffleHistory()
with self.assertRaises(KeyError):
Expand Down Expand Up @@ -301,7 +301,7 @@ def test_writeToDB(self):
"""Test writing to the database.
.. test:: Write a single time step of data to the database.
:id: T_ARMI_DB_TIME
:id: T_ARMI_DB_TIME0
:tests: R_ARMI_DB_TIME
"""
self.r.p.cycle = 0
Expand Down
8 changes: 1 addition & 7 deletions armi/cases/tests/test_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,13 +648,7 @@ def test_copyInterfaceInputs_nonFilePath(self):
self.assertEqual(newSettings[testSetting], fakeShuffle)

def test_failOnDuplicateSetting(self):
"""
That that if a plugin attempts to add a duplicate setting, it raises an error.
.. test:: Plugins cannot register duplicate settings.
:id: T_ARMI_SETTINGS_UNIQUE
:tests: R_ARMI_SETTINGS_UNIQUE
"""
"""That that if a plugin attempts to add a duplicate setting, it raises an error."""
# register the new Plugin
app = getApp()
app.pluginManager.register(TestPluginWithDuplicateSetting)
Expand Down
19 changes: 7 additions & 12 deletions armi/materials/material.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,18 +146,7 @@ def __repr__(self):

@property
def name(self):
"""
Getter for the private name attribute of this Material.
.. impl:: The name of a material is accessible.
:id: I_ARMI_MAT_NAME
:implements: R_ARMI_MAT_NAME
Every instance of an ARMI material must have a simple, human-readable string name. And,
if possible, we want this string to match the class name. (This, of course, puts some
limits on both the string and the class name.) These names are easily retrievable as a
class property.
"""
"""Getter for the private name attribute of this Material."""
return self._name

@name.setter
Expand Down Expand Up @@ -757,6 +746,12 @@ def adjustTD(self, val):
class Fluid(Material):
"""A material that fills its container. Could also be a gas."""

def __init_subclass__(cls):
# Undo the parent-aware density wrapping. Fluids do not expand in the same way solids, so
# Fluid.density(T) is correct. This does not hold for solids because they thermally expand.
if hasattr(cls.density, "__wrapped__"):
cls.density = cls.density.__wrapped__

def getThermalExpansionDensityReduction(self, prevTempInC, newTempInC):
"""Return the factor required to update thermal expansion going from one temperature (in
Celcius) to a new temperature.
Expand Down
63 changes: 63 additions & 0 deletions armi/materials/tests/test_fluids.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Copyright 2025 TerraPower, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for fluid-specific behaviors.
The ARMI framework has a lot of thermal expansion machinery that applies to all components
but doesn't make sense for fluids. The tests here help show fluid materials still
play nice with the rest of the framework.
"""

from unittest import TestCase

from armi.materials.material import Fluid, Material
from armi.reactor.components import Circle
from armi.tests import mockRunLogs


class TestFluids(TestCase):
class MyFluid(Fluid):
"""Stand-in fluid that doesn't provide lots of functionality."""

class MySolid(Material):
"""Stand-in solid that doesn't provide lots of functionality."""

def test_fluidDensityWrapperNoWarning(self):
"""Test that Component.material.density does not raise a warning for fluids.
The ARMI Framework contains a mechanism to warn users if they ask for the density of a
material attached to a component. But the component is the source of truth for volume and
composition. And can be thermally expanded during operation. Much of the framework operates
on ``Component.density`` and other ``Component`` methods for mass accounting. However,
``comp.material.density`` does not know about the new composition or volumes and can diverge
from ``component.density``.
Additionally, the framework does not do any thermal expansion on fluids. So the above calls
to ``component.material.density`` are warranted for fluids.
"""
self._checkCompDensityLogs(
mat=self.MySolid(),
nExpectedWarnings=1,
msg="Solids should have the density warning logged.",
)
self._checkCompDensityLogs(
mat=self.MyFluid(),
nExpectedWarnings=0,
msg="Fluids should not have the density warning logged.",
)

def _checkCompDensityLogs(self, mat: Material, nExpectedWarnings: int, msg: str):
comp = Circle(name="test", material=mat, Tinput=20, Thot=20, id=0, od=1, mult=1)
with mockRunLogs.LogCounter() as logs:
comp.material.density(Tc=comp.temperatureInC)
self.assertEqual(logs.messageCounts["warning"], nExpectedWarnings, msg=msg)
40 changes: 10 additions & 30 deletions armi/materials/tests/test_materials.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,12 @@ def test_pseudoDensityKgM3(self):
self.assertEqual(dens * 1000.0, densKgM3)

def test_wrappedDensity(self):
"""Test that the density decorator is applied."""
self.assertTrue(hasattr(self.mat.density, "__wrapped__"))
"""Test that the density decorator is applied to non-fluids."""
self.assertEqual(
hasattr(self.mat.density, "__wrapped__"),
not isinstance(self.mat, materials.Fluid),
msg=self.mat,
)


class MaterialConstructionTests(unittest.TestCase):
Expand All @@ -116,10 +120,6 @@ def test_findMaterial(self):
.. test:: Materials can be grabbed from a list of namespaces.
:id: T_ARMI_MAT_NAMESPACE0
:tests: R_ARMI_MAT_NAMESPACE
.. test:: You can find a material by name.
:id: T_ARMI_MAT_NAME
:tests: R_ARMI_MAT_NAME
"""
self.assertIs(
materials.resolveMaterialClassByName(
Expand Down Expand Up @@ -889,47 +889,27 @@ class Void_TestCase(_Material_Test, unittest.TestCase):
MAT_CLASS = materials.Void

def test_pseudoDensity(self):
"""This material has a no pseudo-density.
.. test:: There is a void material.
:id: T_ARMI_MAT_VOID0
:tests: R_ARMI_MAT_VOID
"""
"""This material has a no pseudo-density."""
self.mat.setDefaultMassFracs()
cur = self.mat.pseudoDensity()
self.assertEqual(cur, 0.0)

def test_density(self):
"""This material has no density.
.. test:: There is a void material.
:id: T_ARMI_MAT_VOID1
:tests: R_ARMI_MAT_VOID
"""
"""This material has no density."""
self.assertEqual(self.mat.density(500), 0)

self.mat.setDefaultMassFracs()
cur = self.mat.density()
self.assertEqual(cur, 0.0)

def test_linearExpansion(self):
"""This material does not expand linearly.
.. test:: There is a void material.
:id: T_ARMI_MAT_VOID2
:tests: R_ARMI_MAT_VOID
"""
"""This material does not expand linearly."""
cur = self.mat.linearExpansion(400)
ref = 0.0
self.assertEqual(cur, ref)

def test_propertyValidTemperature(self):
"""This material has no valid temperatures.
.. test:: There is a void material.
:id: T_ARMI_MAT_VOID3
:tests: R_ARMI_MAT_VOID
"""
"""This material has no valid temperatures."""
self.assertEqual(len(self.mat.propertyValidTemperature), 0)


Expand Down
13 changes: 1 addition & 12 deletions armi/materials/void.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,7 @@


class Void(material.Fluid):
"""A Void material is a bookkeeping material with zero density.
.. impl:: Define a void material with zero density.
:id: I_ARMI_MAT_VOID
:implements: R_ARMI_MAT_VOID
To help with expansion, it is sometimes useful to put a small section of void
material into the reactor model. This is not meant to represent a true void,
that would cause negative pressure in a system, but just as a bookkeeping tool.
Sometimes this helps users define the geometry of an expanding and conctracting
reactor. It is called a "void" because it has zero density at all temperatures.
"""
"""A Void material is a bookkeeping material with zero density."""

def pseudoDensity(self, Tk: float = None, Tc: float = None) -> float:
return 0.0
Expand Down
34 changes: 11 additions & 23 deletions armi/physics/fuelCycle/fuelHandlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,34 +706,22 @@ def _getAssembliesInRings(
def swapAssemblies(self, a1, a2):
"""Moves a whole assembly from one place to another.
.. impl:: Assemblies can be moved from one place to another.
:id: I_ARMI_SHUFFLE_MOVE
:implements: R_ARMI_SHUFFLE_MOVE
For the two assemblies that are passed in, call to their :py:meth:`~armi.reactor.assemblies.Assembly.moveTo`
methods to transfer their underlying ``spatialLocator`` attributes to
each other. This will also update the ``childrenByLocator`` list on the
core as well as the assembly parameters ``numMoves`` and ``daysSinceLastMove``.
.. impl:: User-specified blocks can be left in place during within-core swaps.
:id: I_ARMI_SHUFFLE_STATIONARY0
:implements: R_ARMI_SHUFFLE_STATIONARY
Before assemblies are moved,
the ``_transferStationaryBlocks`` class method is called to
check if there are any block types specified by the user as stationary
via the ``stationaryBlockFlags`` case setting. Using these flags, blocks
are gathered from each assembly which should remain stationary and
checked to make sure that both assemblies have the same number
and same height of stationary blocks. If not, return an error.
Before assemblies are moved, the ``_transferStationaryBlocks`` class method is called to
check if there are any block types specified by the user as stationary via the
``stationaryBlockFlags`` case setting. Using these flags, blocks are gathered from each
assembly which should remain stationary and checked to make sure that both assemblies
have the same number and same height of stationary blocks. If not, return an error.
If all checks pass, the :py:meth:`~armi.reactor.assemblies.Assembly.remove`
and :py:meth:`~armi.reactor.assemblies.Assembly.insert`
methods are used to swap the stationary blocks between the two assemblies.
If all checks pass, the :py:meth:`~armi.reactor.assemblies.Assembly.remove` and
:py:meth:`~armi.reactor.assemblies.Assembly.insert` methods are used to swap the
stationary blocks between the two assemblies.
Once this process is complete, the actual assembly movement can take
place. Through this process, the stationary blocks remain in the same
core location.
Once this process is complete, the actual assembly movement can take place. Through this
process, the stationary blocks remain in the same core location.
Parameters
----------
Expand Down Expand Up @@ -928,7 +916,7 @@ def swapCascade(self, assemList):
self.swapAssemblies(assemList[0], assemList[level + 1])

def repeatShufflePattern(self, explicitRepeatShuffles):
r"""
"""
Repeats the fuel management from a previous ARMI run.
Parameters
Expand Down
Loading

0 comments on commit 5fb8e8b

Please sign in to comment.