From f4e6dfd239fa66cd3147a96cc617eb491f866972 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Wed, 6 Sep 2023 14:06:53 -0700 Subject: [PATCH 001/176] Enforcing no-relative-imports via Ruff (#1401) --- armi/bookkeeping/db/__init__.py | 10 +++---- armi/bookkeeping/visualization/vtk.py | 2 +- armi/nucDirectory/thermalScattering.py | 4 +-- armi/nuclearDataIO/__init__.py | 2 +- armi/physics/fuelPerformance/executers.py | 2 +- armi/physics/neutronics/__init__.py | 2 +- armi/physics/neutronics/energyGroups.py | 2 +- .../lumpedFissionProduct.py | 5 +++- armi/physics/safety/__init__.py | 2 +- armi/reactor/__init__.py | 14 ++++------ armi/reactor/blueprints/__init__.py | 2 +- armi/reactor/grids/__init__.py | 17 ++++++------ armi/reactor/grids/axial.py | 10 +++---- armi/reactor/grids/cartesian.py | 10 +++---- armi/reactor/grids/constants.py | 2 +- armi/reactor/grids/grid.py | 26 +++++++++---------- armi/reactor/grids/hexagonal.py | 12 ++++----- armi/reactor/grids/locations.py | 10 +++---- armi/reactor/grids/structuredgrid.py | 24 +++++++++-------- armi/reactor/grids/tests/test_grids.py | 10 +++---- armi/reactor/grids/thetarz.py | 4 +-- armi/scripts/migration/__init__.py | 2 +- armi/settings/fwSettings/__init__.py | 6 ++--- armi/utils/directoryChangers.py | 2 +- ruff.toml | 4 +++ 25 files changed, 93 insertions(+), 93 deletions(-) diff --git a/armi/bookkeeping/db/__init__.py b/armi/bookkeeping/db/__init__.py index 6a1d69cdd..bc4e82894 100644 --- a/armi/bookkeeping/db/__init__.py +++ b/armi/bookkeeping/db/__init__.py @@ -66,11 +66,11 @@ from armi import runLog # re-export package components for easier import -from .permissions import Permissions -from .database3 import Database3 -from .databaseInterface import DatabaseInterface -from .compareDB3 import compareDatabases -from .factory import databaseFactory +from armi.bookkeeping.db.permissions import Permissions +from armi.bookkeeping.db.database3 import Database3 +from armi.bookkeeping.db.databaseInterface import DatabaseInterface +from armi.bookkeeping.db.compareDB3 import compareDatabases +from armi.bookkeeping.db.factory import databaseFactory __all__ = [ diff --git a/armi/bookkeeping/visualization/vtk.py b/armi/bookkeeping/visualization/vtk.py index 506c13aad..ab59c3201 100644 --- a/armi/bookkeeping/visualization/vtk.py +++ b/armi/bookkeeping/visualization/vtk.py @@ -43,7 +43,7 @@ from armi.reactor import parameters from armi.bookkeeping.db import database3 from armi.bookkeeping.visualization import dumper -from . import utils +from armi.bookkeeping.visualization import utils class VtkDumper(dumper.VisFileDumper): diff --git a/armi/nucDirectory/thermalScattering.py b/armi/nucDirectory/thermalScattering.py index b494d9585..ed0857f05 100644 --- a/armi/nucDirectory/thermalScattering.py +++ b/armi/nucDirectory/thermalScattering.py @@ -55,8 +55,8 @@ """ from typing import Tuple, Union -from . import nuclideBases as nb -from . import elements +from armi.nucDirectory import nuclideBases as nb +from armi.nucDirectory import elements BE_METAL = "Be-metal" diff --git a/armi/nuclearDataIO/__init__.py b/armi/nuclearDataIO/__init__.py index 0097f1330..89a2ca925 100644 --- a/armi/nuclearDataIO/__init__.py +++ b/armi/nuclearDataIO/__init__.py @@ -19,7 +19,7 @@ # export the cccc modules here to keep external clients happy, # though prefer full imports in new code -from .cccc import ( +from armi.nuclearDataIO.cccc import ( compxs, dif3d, dlayxs, diff --git a/armi/physics/fuelPerformance/executers.py b/armi/physics/fuelPerformance/executers.py index 3fb7acb70..7cc0af2fb 100644 --- a/armi/physics/fuelPerformance/executers.py +++ b/armi/physics/fuelPerformance/executers.py @@ -23,7 +23,7 @@ from armi.physics import executers -from .settings import ( +from armi.physics.fuelPerformance.settings import ( CONF_FUEL_PERFORMANCE_ENGINE, CONF_AXIAL_EXPANSION, CONF_BOND_REMOVAL, diff --git a/armi/physics/neutronics/__init__.py b/armi/physics/neutronics/__init__.py index e844e5ab0..bc5ac4e27 100644 --- a/armi/physics/neutronics/__init__.py +++ b/armi/physics/neutronics/__init__.py @@ -60,7 +60,7 @@ def exposeInterfaces(cs): @staticmethod @plugins.HOOKIMPL def defineParameters(): - from . import parameters as neutronicsParameters + from armi.physics.neutronics import parameters as neutronicsParameters return neutronicsParameters.getNeutronicsParameterDefinitions() diff --git a/armi/physics/neutronics/energyGroups.py b/armi/physics/neutronics/energyGroups.py index 6c58d1170..5ecda047d 100644 --- a/armi/physics/neutronics/energyGroups.py +++ b/armi/physics/neutronics/energyGroups.py @@ -21,7 +21,7 @@ from armi import runLog from armi.utils.mathematics import findNearestValue -from .const import ( +from armi.physics.neutronics.const import ( FAST_FLUX_THRESHOLD_EV, MAXIMUM_XS_LIBRARY_ENERGY, ULTRA_FINE_GROUP_LETHARGY_WIDTH, diff --git a/armi/physics/neutronics/fissionProductModel/lumpedFissionProduct.py b/armi/physics/neutronics/fissionProductModel/lumpedFissionProduct.py index 3610b2ca7..d937b81b2 100644 --- a/armi/physics/neutronics/fissionProductModel/lumpedFissionProduct.py +++ b/armi/physics/neutronics/fissionProductModel/lumpedFissionProduct.py @@ -24,7 +24,10 @@ from armi import runLog from armi.nucDirectory import elements -from .fissionProductModelSettings import CONF_LFP_COMPOSITION_FILE_PATH, CONF_FP_MODEL +from armi.physics.neutronics.fissionProductModel.fissionProductModelSettings import ( + CONF_LFP_COMPOSITION_FILE_PATH, + CONF_FP_MODEL, +) class LumpedFissionProduct: diff --git a/armi/physics/safety/__init__.py b/armi/physics/safety/__init__.py index a9bbdbf26..adaf6b2b4 100644 --- a/armi/physics/safety/__init__.py +++ b/armi/physics/safety/__init__.py @@ -15,7 +15,7 @@ """Safety package for generic safety-related code.""" from armi import plugins -from . import settings +from armi.physics.safety import settings class SafetyPlugin(plugins.ArmiPlugin): diff --git a/armi/reactor/__init__.py b/armi/reactor/__init__.py index 818d0ebec..dc3b1fdd8 100644 --- a/armi/reactor/__init__.py +++ b/armi/reactor/__init__.py @@ -49,9 +49,9 @@ class ReactorPlugin(plugins.ArmiPlugin): @staticmethod @plugins.HOOKIMPL def defineBlockTypes(): - from .components.basicShapes import Rectangle, Hexagon - from .components.volumetricShapes import RadialSegment - from . import blocks + from armi.reactor.components.basicShapes import Rectangle, Hexagon + from armi.reactor.components.volumetricShapes import RadialSegment + from armi.reactor import blocks return [ (Rectangle, blocks.CartesianBlock), @@ -62,12 +62,8 @@ def defineBlockTypes(): @staticmethod @plugins.HOOKIMPL def defineAssemblyTypes(): - from .blocks import HexBlock, CartesianBlock, ThRZBlock - from .assemblies import ( - HexAssembly, - CartesianAssembly, - ThRZAssembly, - ) + from armi.reactor.blocks import HexBlock, CartesianBlock, ThRZBlock + from armi.reactor.assemblies import HexAssembly, CartesianAssembly, ThRZAssembly return [ (HexBlock, HexAssembly), diff --git a/armi/reactor/blueprints/__init__.py b/armi/reactor/blueprints/__init__.py index 548ff0476..7f068d4e2 100644 --- a/armi/reactor/blueprints/__init__.py +++ b/armi/reactor/blueprints/__init__.py @@ -566,7 +566,7 @@ def load(cls, stream, roundTrip=False): return super().load(stream, Loader=loader) def addDefaultSFP(self): - """Create a default SFP if it's not in the blueprints""" + """Create a default SFP if it's not in the blueprints.""" if self.systemDesigns is not None: if not any(structure.typ == "sfp" for structure in self.systemDesigns): sfp = SystemBlueprint("Spent Fuel Pool", "sfp", Triplet()) diff --git a/armi/reactor/grids/__init__.py b/armi/reactor/grids/__init__.py index 598295a65..db5eb3492 100644 --- a/armi/reactor/grids/__init__.py +++ b/armi/reactor/grids/__init__.py @@ -60,16 +60,17 @@ while the word **local** refers to within the current coordinate system defined by the current grid. """ +# ruff: noqa: F401 from typing import Tuple, Optional -from .constants import ( +from armi.reactor.grids.constants import ( BOUNDARY_CENTER, BOUNDARY_0_DEGREES, BOUNDARY_120_DEGREES, BOUNDARY_60_DEGREES, ) -from .locations import ( +from armi.reactor.grids.locations import ( LocationBase, IndexLocation, MultiIndexLocation, @@ -77,12 +78,12 @@ addingIsValid, ) -from .grid import Grid -from .structuredgrid import StructuredGrid, GridParameters, _tuplify -from .axial import AxialGrid, axialUnitGrid -from .cartesian import CartesianGrid -from .hexagonal import HexGrid, COS30, SIN30, TRIANGLES_IN_HEXAGON -from .thetarz import ThetaRZGrid, TAU +from armi.reactor.grids.grid import Grid +from armi.reactor.grids.structuredgrid import StructuredGrid, GridParameters, _tuplify +from armi.reactor.grids.axial import AxialGrid, axialUnitGrid +from armi.reactor.grids.cartesian import CartesianGrid +from armi.reactor.grids.hexagonal import HexGrid, COS30, SIN30, TRIANGLES_IN_HEXAGON +from armi.reactor.grids.thetarz import ThetaRZGrid, TAU def locatorLabelToIndices(label: str) -> Tuple[int, int, Optional[int]]: diff --git a/armi/reactor/grids/axial.py b/armi/reactor/grids/axial.py index 1bfaa997a..d2a02a25a 100644 --- a/armi/reactor/grids/axial.py +++ b/armi/reactor/grids/axial.py @@ -16,15 +16,15 @@ import numpy -from .locations import IJType, LocationBase -from .structuredgrid import StructuredGrid +from armi.reactor.grids.locations import IJType, LocationBase +from armi.reactor.grids.structuredgrid import StructuredGrid if TYPE_CHECKING: from armi.reactor.composites import ArmiObject class AxialGrid(StructuredGrid): - """1-D grid in the k-direction (z) + """1-D grid in the k-direction (z). .. note::: @@ -37,7 +37,7 @@ class AxialGrid(StructuredGrid): def fromNCells( cls, numCells: int, armiObject: Optional["ArmiObject"] = None ) -> "AxialGrid": - """Produces an unit grid where each bin is 1-cm tall + """Produces an unit grid where each bin is 1-cm tall. ``numCells + 1`` mesh boundaries are added, since one block would require a bottom and a top. @@ -77,7 +77,7 @@ def overlapsWhichSymmetryLine(indices: IJType) -> None: @property def pitch(self) -> float: - """Grid spacing in the z-direction + """Grid spacing in the z-direction. Returns ------- diff --git a/armi/reactor/grids/cartesian.py b/armi/reactor/grids/cartesian.py index c36f8923f..14aa69e9e 100644 --- a/armi/reactor/grids/cartesian.py +++ b/armi/reactor/grids/cartesian.py @@ -18,13 +18,13 @@ from armi.reactor import geometry -from .locations import IJType -from .structuredgrid import StructuredGrid +from armi.reactor.grids.locations import IJType +from armi.reactor.grids.structuredgrid import StructuredGrid class CartesianGrid(StructuredGrid): """ - Grid class representing a conformal Cartesian mesh + Grid class representing a conformal Cartesian mesh. It is recommended to call :meth:`fromRectangle` to construct, rather than directly constructing with ``__init__`` @@ -108,7 +108,7 @@ def fromRectangle( ) def overlapsWhichSymmetryLine(self, indices: IJType) -> None: - """Return lines of symmetry position at a given index can be found + """Return lines of symmetry position at a given index can be found. .. warning:: @@ -301,7 +301,7 @@ def _isThroughCenter(self): @property def pitch(self) -> Tuple[float, float]: - """Grid pitch in the x and y dimension + """Grid pitch in the x and y dimension. Returns ------- diff --git a/armi/reactor/grids/constants.py b/armi/reactor/grids/constants.py index f2fa78bd8..5401fe5e9 100644 --- a/armi/reactor/grids/constants.py +++ b/armi/reactor/grids/constants.py @@ -11,7 +11,7 @@ # 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. -"""Some constants often used in grid manipulation""" +"""Some constants often used in grid manipulation.""" BOUNDARY_0_DEGREES = 1 BOUNDARY_60_DEGREES = 2 diff --git a/armi/reactor/grids/grid.py b/armi/reactor/grids/grid.py index 3146e122f..7d509eda4 100644 --- a/armi/reactor/grids/grid.py +++ b/armi/reactor/grids/grid.py @@ -18,14 +18,14 @@ from armi.reactor import geometry -from .locations import LocationBase, IndexLocation, IJType, IJKType +from armi.reactor.grids.locations import LocationBase, IndexLocation, IJType, IJKType if TYPE_CHECKING: from armi.reactor.composites import ArmiObject class Grid(ABC): - """Base class that defines the interface for grids + """Base class that defines the interface for grids. Most work will be done with structured grids, e.g., hexagonal grid, Cartesian grids, but some physics codes accept irregular or unstructured grids. Consider @@ -69,7 +69,7 @@ def __init__( @property def geomType(self) -> geometry.GeomType: - """Geometric representation""" + """Geometric representation.""" return geometry.GeomType.fromStr(self._geomType) @geomType.setter @@ -81,7 +81,7 @@ def geomType(self, geomType: Union[str, geometry.GeomType]): @property def symmetry(self) -> str: - """Symmetry applied to the grid""" + """Symmetry applied to the grid.""" return geometry.SymmetryType.fromStr(self._symmetry) @symmetry.setter @@ -93,7 +93,7 @@ def symmetry(self, symmetry: Union[str, geometry.SymmetryType]): def __getstate__(self) -> Dict: """ - Pickling removes reference to ``armiObject`` + Pickling removes reference to ``armiObject``. Removing the ``armiObject`` allows us to pickle an assembly without pickling the entire reactor. An ``Assembly.spatialLocator.grid.armiObject`` is the @@ -109,7 +109,7 @@ def __getstate__(self) -> Dict: def __setstate__(self, state: Dict): """ - Pickling removes reference to ``armiObject`` + Pickling removes reference to ``armiObject``. This relies on the ``ArmiObject.__setstate__`` to assign itself. """ @@ -121,11 +121,11 @@ def __setstate__(self, state: Dict): @property @abstractmethod def isAxialOnly(self) -> bool: - """Indicate to parts of ARMI if this Grid handles only axial cells""" + """Indicate to parts of ARMI if this Grid handles only axial cells.""" @abstractmethod def __len__(self) -> int: - """Number of items in the grid""" + """Number of items in the grid.""" @abstractmethod def items(self) -> Iterable[Tuple[IJKType, IndexLocation]]: @@ -170,7 +170,7 @@ def getSymmetricEquivalents(self, indices: IJType) -> List[IJType]: @abstractmethod def overlapsWhichSymmetryLine(self, indices: IJType) -> Optional[int]: - """Return lines of symmetry position at a given index can be found + """Return lines of symmetry position at a given index can be found. Parameters ---------- @@ -196,19 +196,19 @@ def getCoordinates( @abstractmethod def backUp(self): - """Subclasses should modify the internal backup variable""" + """Subclasses should modify the internal backup variable.""" @abstractmethod def restoreBackup(self): - """Restore state from backup""" + """Restore state from backup.""" @abstractmethod def getCellBase(self, indices: IJKType) -> numpy.ndarray: - """Return the lower left case of this cell in cm""" + """Return the lower left case of this cell in cm.""" @abstractmethod def getCellTop(self, indices: IJKType) -> numpy.ndarray: - """Get the upper right of this cell in cm""" + """Get the upper right of this cell in cm.""" @staticmethod def getLabel(indices): diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py index 2d0391de5..d11bdaf0a 100644 --- a/armi/reactor/grids/hexagonal.py +++ b/armi/reactor/grids/hexagonal.py @@ -19,14 +19,14 @@ from armi.reactor import geometry from armi.utils import hexagon -from .constants import ( +from armi.reactor.grids.constants import ( BOUNDARY_0_DEGREES, BOUNDARY_120_DEGREES, BOUNDARY_60_DEGREES, BOUNDARY_CENTER, ) -from .locations import IndexLocation, IJKType, IJType -from .structuredgrid import StructuredGrid +from armi.reactor.grids.locations import IndexLocation, IJKType, IJType +from armi.reactor.grids.structuredgrid import StructuredGrid COS30 = sqrt(3) / 2.0 SIN30 = 1.0 / 2.0 @@ -184,9 +184,7 @@ def getMinimumRings(self, n: int) -> int: return hexagon.numRingsToHoldNumCells(n) def getPositionsInRing(self, ring: int) -> int: - """ - Return the number of positions within a ring. - """ + """Return the number of positions within a ring.""" return hexagon.numPositionsInRing(ring) def getNeighboringCellIndices( @@ -417,7 +415,7 @@ def generateSortedHexLocationList(self, nLocs: int): # in a ring, not the actual positions def allPositionsInThird(self, ring, includeEdgeAssems=False): """ - Returns a list of all the positions in a ring (in the first third) + Returns a list of all the positions in a ring (in the first third). Parameters ---------- diff --git a/armi/reactor/grids/locations.py b/armi/reactor/grids/locations.py index 854b1a1ac..5622f4d7b 100644 --- a/armi/reactor/grids/locations.py +++ b/armi/reactor/grids/locations.py @@ -20,7 +20,7 @@ if TYPE_CHECKING: # Avoid some circular imports - from . import Grid + from armi.reactor.grids import Grid IJType = Tuple[int, int] @@ -57,9 +57,7 @@ def __repr__(self) -> str: ) def __getstate__(self) -> Hashable: - """ - Used in pickling and deepcopy, this detaches the grid. - """ + """Used in pickling and deepcopy, this detaches the grid.""" return (self._i, self._j, self._k, None) def __setstate__(self, state: Hashable): @@ -375,9 +373,7 @@ def __init__(self, grid: "Grid"): self._locations = [] def __getstate__(self) -> List[IndexLocation]: - """ - Used in pickling and deepcopy, this detaches the grid. - """ + """Used in pickling and deepcopy, this detaches the grid.""" return self._locations def __setstate__(self, state: List[IndexLocation]): diff --git a/armi/reactor/grids/structuredgrid.py b/armi/reactor/grids/structuredgrid.py index 09f7929ff..39267445f 100644 --- a/armi/reactor/grids/structuredgrid.py +++ b/armi/reactor/grids/structuredgrid.py @@ -17,8 +17,13 @@ import numpy -from .locations import IJKType, LocationBase, IndexLocation, MultiIndexLocation -from .grid import Grid +from armi.reactor.grids.locations import ( + IJKType, + LocationBase, + IndexLocation, + MultiIndexLocation, +) +from armi.reactor.grids.grid import Grid # data structure for database-serialization of grids GridParameters = collections.namedtuple( @@ -186,7 +191,7 @@ def isAxialOnly(self) -> bool: return self._isAxialOnly def reduce(self) -> GridParameters: - """Recreate the parameter necessary to create this grid""" + """Recreate the parameter necessary to create this grid.""" offset = None if not self._offset.any() else tuple(self._offset) bounds = _tuplify(self._bounds) @@ -219,7 +224,7 @@ def reduce(self) -> GridParameters: @property def offset(self) -> numpy.ndarray: - """Offset in cm for each axis""" + """Offset in cm for each axis.""" return self._offset @offset.setter @@ -296,14 +301,14 @@ def getCoordinates(self, indices, nativeCoords=False) -> numpy.ndarray: ) def getCellBase(self, indices) -> numpy.ndarray: - """Get the mesh base (lower left) of this mesh cell in cm""" + """Get the mesh base (lower left) of this mesh cell in cm.""" indices = numpy.array(indices) return self._evaluateMesh( indices, self._meshBaseBySteps, self._meshBaseByBounds ) def getCellTop(self, indices) -> numpy.ndarray: - """Get the mesh top (upper right) of this mesh cell in cm""" + """Get the mesh top (upper right) of this mesh cell in cm.""" indices = numpy.array(indices) + 1 return self._evaluateMesh( indices, self._meshBaseBySteps, self._meshBaseByBounds @@ -392,9 +397,7 @@ def getBounds( ) -> Tuple[ Optional[Sequence[float]], Optional[Sequence[float]], Optional[Sequence[float]] ]: - """ - Return the grid bounds for each dimension, if present. - """ + """Return the grid bounds for each dimension, if present.""" return self._bounds def getLocatorFromRingAndPos(self, ring, pos, k=0): @@ -458,7 +461,6 @@ def getRingPos(self, indices) -> Tuple[int, int]: A tuple is returned so that it is easy to compare pairs of indices. """ - # Regular grids don't know about ring and position. Check the parent. if ( self.armiObject is not None @@ -486,7 +488,7 @@ def _buildLocations(self): @property @abstractmethod def pitch(self) -> Union[float, Tuple[float, float]]: - """Grid pitch + """Grid pitch. Some implementations may rely on a single pitch, such as axial or hexagonal grids. Cartesian grids may use diff --git a/armi/reactor/grids/tests/test_grids.py b/armi/reactor/grids/tests/test_grids.py index 61244ac68..915b7a594 100644 --- a/armi/reactor/grids/tests/test_grids.py +++ b/armi/reactor/grids/tests/test_grids.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Tests for grids""" +"""Tests for grids.""" # pylint: disable=missing-function-docstring,missing-class-docstring,abstract-method,protected-access,no-self-use,attribute-defined-outside-init from io import BytesIO import math @@ -51,7 +51,7 @@ def __init__(self, parent=None): class MockStructuredGrid(grids.StructuredGrid): - """Need a concrete class to test a lot of inherited methods + """Need a concrete class to test a lot of inherited methods. Abstract methods from the parent now raise ``NotImplementedError`` """ @@ -223,7 +223,7 @@ def test_ringPosFromIndicesIncorrect(self): class TestHexGrid(unittest.TestCase): - """A set of tests for the Hexagonal Grid + """A set of tests for the Hexagonal Grid. .. test: Tests of the Hexagonal grid. :id: TEST_REACTOR_MESH_0 @@ -490,7 +490,7 @@ def test_getIndexBounds(self): class TestThetaRZGrid(unittest.TestCase): - """A set of tests for the RZTheta Grid + """A set of tests for the RZTheta Grid. .. test: Tests of the RZTheta grid. :id: TEST_REACTOR_MESH_1 @@ -513,7 +513,7 @@ def test_positions(self): class TestCartesianGrid(unittest.TestCase): - """A set of tests for the Cartesian Grid + """A set of tests for the Cartesian Grid. .. test: Tests of the Cartesian grid. :id: TEST_REACTOR_MESH_2 diff --git a/armi/reactor/grids/thetarz.py b/armi/reactor/grids/thetarz.py index 53116e5db..c0bd9c9dc 100644 --- a/armi/reactor/grids/thetarz.py +++ b/armi/reactor/grids/thetarz.py @@ -16,8 +16,8 @@ import numpy -from .locations import IJType, IJKType -from .structuredgrid import StructuredGrid +from armi.reactor.grids.locations import IJType, IJKType +from armi.reactor.grids.structuredgrid import StructuredGrid if TYPE_CHECKING: # Avoid circular imports diff --git a/armi/scripts/migration/__init__.py b/armi/scripts/migration/__init__.py index 46b21d76b..9ec4fa366 100644 --- a/armi/scripts/migration/__init__.py +++ b/armi/scripts/migration/__init__.py @@ -32,7 +32,7 @@ like happens in mainstream applications like word processors and spreadsheets. """ -from . import ( +from armi.scripts.migration import ( m0_1_3, m0_1_0_newDbFormat, crossSectionBlueprintsToSettings, diff --git a/armi/settings/fwSettings/__init__.py b/armi/settings/fwSettings/__init__.py index 739219c05..a7caffdb4 100644 --- a/armi/settings/fwSettings/__init__.py +++ b/armi/settings/fwSettings/__init__.py @@ -16,9 +16,9 @@ from typing import List from armi.settings import setting -from . import globalSettings -from . import databaseSettings -from . import reportSettings +from armi.settings.fwSettings import globalSettings +from armi.settings.fwSettings import databaseSettings +from armi.settings.fwSettings import reportSettings def getFrameworkSettings() -> List[setting.Setting]: diff --git a/armi/utils/directoryChangers.py b/armi/utils/directoryChangers.py index a705eb63f..aeb6b46de 100644 --- a/armi/utils/directoryChangers.py +++ b/armi/utils/directoryChangers.py @@ -355,7 +355,7 @@ def __enter__(self): def directoryChangerFactory(): if context.MPI_SIZE > 1: - from .directoryChangersMpi import MpiDirectoryChanger + from armi.utils.directoryChangersMpi import MpiDirectoryChanger return MpiDirectoryChanger else: diff --git a/ruff.toml b/ruff.toml index d852f3cf2..a221eaf5f 100644 --- a/ruff.toml +++ b/ruff.toml @@ -72,6 +72,10 @@ target-version = "py39" # Setting line-length to 88 to match Black line-length = 88 +[flake8-tidy-imports] +# Disallow all relative imports. +ban-relative-imports = "all" + [per-file-ignores] # D1XX - enforces writing docstrings # E741 - ambiguous variable name From 738f47e8cf578cfbbb52b664465921d3df5cdb5e Mon Sep 17 00:00:00 2001 From: Drew Johnson <83778714+drewj-usnctech@users.noreply.github.com> Date: Fri, 8 Sep 2023 11:08:25 -0700 Subject: [PATCH 002/176] Don't validate material modifications if they are None (#1369) --- armi/reactor/blueprints/assemblyBlueprint.py | 25 +++++++- armi/reactor/blueprints/blockBlueprint.py | 67 +++++++++++++------- 2 files changed, 67 insertions(+), 25 deletions(-) diff --git a/armi/reactor/blueprints/assemblyBlueprint.py b/armi/reactor/blueprints/assemblyBlueprint.py index 727437cea..7abb8bfbc 100644 --- a/armi/reactor/blueprints/assemblyBlueprint.py +++ b/armi/reactor/blueprints/assemblyBlueprint.py @@ -192,6 +192,29 @@ def _constructAssembly(self, cs, blueprint): return a + @staticmethod + def _shouldMaterialModiferBeApplied(value) -> bool: + """Determine if a material modifier entry is applicable + + Two exceptions: + + 1. Modifiers that are empty strings are not applied. + 2. Modifiers that are ``None`` are not applied + + Parameters + ---------- + value : object + Entry in a material modifications array + + Returns + ------- + bool + Result of the check + """ + if value != "" and value is not None: + return True + return False + def _createBlock(self, cs, blueprint, bDesign, axialIndex): """Create a block based on the block design and the axial index.""" meshPoints = self.axialMeshPoints[axialIndex] @@ -207,7 +230,7 @@ def _createBlock(self, cs, blueprint, bDesign, axialIndex): materialInput[key] = { modName: modList[axialIndex] for modName, modList in mod.items() - if modList[axialIndex] != "" + if self._shouldMaterialModiferBeApplied(modList[axialIndex]) } b = bDesign.construct( diff --git a/armi/reactor/blueprints/blockBlueprint.py b/armi/reactor/blueprints/blockBlueprint.py index 2eb0276a6..5212bf351 100644 --- a/armi/reactor/blueprints/blockBlueprint.py +++ b/armi/reactor/blueprints/blockBlueprint.py @@ -15,12 +15,14 @@ """This module defines the ARMI input for a block definition, and code for constructing an ARMI ``Block``.""" import collections from inspect import signature +from typing import Iterable, Set, Iterator import yamlize from armi import getPluginManagerOrFail, runLog from armi.materials.material import Material from armi.reactor import blocks +from armi.reactor.composites import Composite from armi.reactor import parameters from armi.reactor.flags import Flags from armi.reactor.blueprints import componentBlueprint @@ -127,16 +129,7 @@ def construct( if isinstance(c, Component): # there are other things like composite groups that don't get # material modifications -- skip those - validMatModOptions = set() - for materialParentClass in c.material.__class__.__mro__: - # we must loop over parents as well, since applyInputParams - # could call to Parent.applyInputParams() - if issubclass(materialParentClass, Material): - validMatModOptions.update( - signature( - materialParentClass.applyInputParams - ).parameters.keys() - ) + validMatModOptions = self._getMaterialModsFromBlockChildren(c) for key in byComponentMatModKeys: if key not in validMatModOptions: raise ValueError( @@ -166,20 +159,9 @@ def construct( # check that the block level mat mods use valid options in the same way # as we did for the by-component mods above - validMatModOptions = set() - for c in components.values(): - if isinstance(c, Component): - # there are other things like composite groups that don't get - # material modifications -- skip those - for materialParentClass in c.material.__class__.__mro__: - # we must loop over parents as well, since applyInputParams - # could call to Parent.applyInputParams() - if issubclass(materialParentClass, Material): - validMatModOptions.update( - signature( - materialParentClass.applyInputParams - ).parameters.keys() - ) + validMatModOptions = self._getBlockwiseMaterialModifierOptions( + components.values() + ) if "byBlock" in materialInput: for key in materialInput["byBlock"]: @@ -241,6 +223,43 @@ def construct( runLog.warning(str(e), single=True) return b + def _getBlockwiseMaterialModifierOptions( + self, children: Iterable[Composite] + ) -> Set[str]: + """Collect all the material modifiers that exist on a block""" + validMatModOptions = set() + for c in children: + perChildModifiers = self._getMaterialModsFromBlockChildren(c) + validMatModOptions.update(perChildModifiers) + return validMatModOptions + + def _getMaterialModsFromBlockChildren(self, c: Composite) -> Set[str]: + """Collect all the material modifiers from a child of a block""" + perChildModifiers = set() + for material in self._getMaterialsInComposite(c): + for materialParentClass in material.__class__.__mro__: + # we must loop over parents as well, since applyInputParams + # could call to Parent.applyInputParams() + if issubclass(materialParentClass, Material): + perChildModifiers.update( + signature( + materialParentClass.applyInputParams + ).parameters.keys() + ) + # self is a parameter to class methods, so it gets picked up here + # but that's obviously not a real material modifier + perChildModifiers.discard("self") + return perChildModifiers + + def _getMaterialsInComposite(self, child: Composite) -> Iterator[Material]: + """Collect all the materials in a composite""" + # Leaf node, no need to traverse further down + if isinstance(child, Component): + yield child.material + return + # Don't apply modifications to other things that could reside + # in a block e.g., component groups + def _checkByComponentMaterialInput(self, materialInput): for component in materialInput: if component != "byBlock": From 5c4c5cfd80434e66363bd849d4ac32d456277ea7 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:46:47 -0700 Subject: [PATCH 003/176] Removing mako from dependencies. (#1405) I can find no use of the "mako" library in ARMI. So either it is unused and can be deleted, or it is a secondary dependency and can be deleted. Either way, less is more. --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index f8c702647..7b21398dd 100644 --- a/setup.py +++ b/setup.py @@ -85,7 +85,6 @@ def collectExtraFiles(): "ipykernel", "jupyter-contrib-nbextensions", "jupyter_client", - "mako", "nbsphinx", "nbsphinx-link", "pytest", From 7f8936723f6413fcf57ca648a58bc6f6f2ced706 Mon Sep 17 00:00:00 2001 From: Tony Alberti Date: Wed, 13 Sep 2023 11:35:57 -0700 Subject: [PATCH 004/176] Movnig setOptionsFromCs to Core.processLoading (#1406) --- armi/reactor/blueprints/reactorBlueprint.py | 6 ------ armi/reactor/reactors.py | 1 + 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/armi/reactor/blueprints/reactorBlueprint.py b/armi/reactor/blueprints/reactorBlueprint.py index 1216f7fae..b58b25323 100644 --- a/armi/reactor/blueprints/reactorBlueprint.py +++ b/armi/reactor/blueprints/reactorBlueprint.py @@ -139,12 +139,6 @@ def construct(self, cs, bp, reactor, geom=None, loadAssems=True): system = self._resolveSystemType(self.typ)(self.name) - # TODO: This could be somewhere better. If system blueprints could be - # subclassed, this could live in the CoreBlueprint. setOptionsFromCS() also isnt - # great to begin with, so ideally it could be removed entirely. - if isinstance(system, reactors.Core): - system.setOptionsFromCs(cs) - # Some systems may not require a prescribed grid design. Only try to use one if # it was provided if gridDesign is not None: diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index 4782f15af..d2b8c3488 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -2349,6 +2349,7 @@ def processLoading(self, cs, dbLoad: bool = False): -------- updateAxialMesh : Perturbs the axial mesh originally set up here. """ + self.setOptionsFromCs(cs) runLog.header( "=========== Initializing Mesh, Assembly Zones, and Nuclide Categories =========== " ) From d7591686ce130a1369e3372d49e29701af3fbe94 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 14 Sep 2023 13:42:23 -0700 Subject: [PATCH 005/176] Reducing warning spam from zones (#1411) --- armi/reactor/blueprints/assemblyBlueprint.py | 2 +- armi/reactor/blueprints/blockBlueprint.py | 6 +++--- armi/reactor/zones.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/armi/reactor/blueprints/assemblyBlueprint.py b/armi/reactor/blueprints/assemblyBlueprint.py index 7abb8bfbc..99fd33293 100644 --- a/armi/reactor/blueprints/assemblyBlueprint.py +++ b/armi/reactor/blueprints/assemblyBlueprint.py @@ -194,7 +194,7 @@ def _constructAssembly(self, cs, blueprint): @staticmethod def _shouldMaterialModiferBeApplied(value) -> bool: - """Determine if a material modifier entry is applicable + """Determine if a material modifier entry is applicable. Two exceptions: diff --git a/armi/reactor/blueprints/blockBlueprint.py b/armi/reactor/blueprints/blockBlueprint.py index 5212bf351..68443f8af 100644 --- a/armi/reactor/blueprints/blockBlueprint.py +++ b/armi/reactor/blueprints/blockBlueprint.py @@ -226,7 +226,7 @@ def construct( def _getBlockwiseMaterialModifierOptions( self, children: Iterable[Composite] ) -> Set[str]: - """Collect all the material modifiers that exist on a block""" + """Collect all the material modifiers that exist on a block.""" validMatModOptions = set() for c in children: perChildModifiers = self._getMaterialModsFromBlockChildren(c) @@ -234,7 +234,7 @@ def _getBlockwiseMaterialModifierOptions( return validMatModOptions def _getMaterialModsFromBlockChildren(self, c: Composite) -> Set[str]: - """Collect all the material modifiers from a child of a block""" + """Collect all the material modifiers from a child of a block.""" perChildModifiers = set() for material in self._getMaterialsInComposite(c): for materialParentClass in material.__class__.__mro__: @@ -252,7 +252,7 @@ def _getMaterialModsFromBlockChildren(self, c: Composite) -> Set[str]: return perChildModifiers def _getMaterialsInComposite(self, child: Composite) -> Iterator[Material]: - """Collect all the materials in a composite""" + """Collect all the materials in a composite.""" # Leaf node, no need to traverse further down if isinstance(child, Component): yield child.material diff --git a/armi/reactor/zones.py b/armi/reactor/zones.py index d9074ec79..718ea89ca 100644 --- a/armi/reactor/zones.py +++ b/armi/reactor/zones.py @@ -393,7 +393,7 @@ def findZoneItIsIn(self, a: Union[Assembly, Block]) -> Optional[Zone]: return zone if not zoneFound: - runLog.warning("Was not able to find which zone {} is in".format(a)) + runLog.debug(f"Was not able to find which zone {a} is in", single=True) return None From 290db96a635224ae9b8bf3152a1b84444dcfd670 Mon Sep 17 00:00:00 2001 From: Tony Alberti Date: Thu, 14 Sep 2023 16:27:06 -0700 Subject: [PATCH 006/176] Replacing some cs["string"] with cs[CONF] (#1404) --- armi/reactor/reactors.py | 43 +++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index d2b8c3488..39cdde1a9 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -49,8 +49,20 @@ from armi.reactor.assemblyLists import SpentFuelPool from armi.reactor.flags import Flags from armi.reactor.systemLayoutInput import SystemLayoutInput -from armi.settings.fwSettings.globalSettings import CONF_MATERIAL_NAMESPACE_ORDER -from armi.settings.fwSettings.globalSettings import CONF_SORT_REACTOR +from armi.settings.fwSettings.globalSettings import ( + CONF_MATERIAL_NAMESPACE_ORDER, + CONF_FRESH_FEED_TYPE, + CONF_SORT_REACTOR, + CONF_GEOM_FILE, + CONF_NON_UNIFORM_ASSEM_FLAGS, + CONF_STATIONARY_BLOCK_FLAGS, + CONF_ZONE_DEFINITIONS, + CONF_TRACK_ASSEMS, + CONF_CIRCULAR_RING_PITCH, + CONF_AUTOMATIC_VARIABLE_MESH, + CONF_MIN_MESH_SIZE_RATIO, + CONF_DETAILED_AXIAL_EXPANSION, +) from armi.utils import createFormattedStrWithDelimiter, units from armi.utils import directoryChangers from armi.utils.iterables import Sequence @@ -186,7 +198,7 @@ def factory(cs, bp, geom: Optional[SystemLayoutInput] = None) -> Reactor: materials.setMaterialNamespaceOrder(cs[CONF_MATERIAL_NAMESPACE_ORDER]) r = Reactor(cs.caseTitle, bp) - if cs["geomFile"]: + if cs[CONF_GEOM_FILE]: blueprints.migrate(bp, cs) if not any(structure.typ == "sfp" for structure in bp.systemDesigns.values()): @@ -275,18 +287,20 @@ def __init__(self, name): self._detailedAxialExpansion = False def setOptionsFromCs(self, cs): - from armi.physics.fuelCycle.settings import CONF_CIRCULAR_RING_MODE - from armi.physics.fuelCycle.settings import CONF_JUMP_RING_NUM + from armi.physics.fuelCycle.settings import ( + CONF_JUMP_RING_NUM, + CONF_CIRCULAR_RING_MODE, + ) # these are really "user modifiable modeling constants" self.p.jumpRing = cs[CONF_JUMP_RING_NUM] - self._freshFeedType = cs["freshFeedType"] - self._trackAssems = cs["trackAssems"] + self._freshFeedType = cs[CONF_FRESH_FEED_TYPE] + self._trackAssems = cs[CONF_TRACK_ASSEMS] self._circularRingMode = cs[CONF_CIRCULAR_RING_MODE] - self._circularRingPitch = cs["circularRingPitch"] - self._automaticVariableMesh = cs["automaticVariableMesh"] - self._minMeshSizeRatio = cs["minMeshSizeRatio"] - self._detailedAxialExpansion = cs["detailedAxialExpansion"] + self._circularRingPitch = cs[CONF_CIRCULAR_RING_PITCH] + self._automaticVariableMesh = cs[CONF_AUTOMATIC_VARIABLE_MESH] + self._minMeshSizeRatio = cs[CONF_MIN_MESH_SIZE_RATIO] + self._detailedAxialExpansion = cs[CONF_DETAILED_AXIAL_EXPANSION] def __getstate__(self): """Applies a settings and parent to the core and components.""" @@ -2373,7 +2387,8 @@ def processLoading(self, cs, dbLoad: bool = False): else: # set reactor level meshing params nonUniformAssems = [ - Flags.fromStringIgnoreErrors(t) for t in cs["nonUniformAssemFlags"] + Flags.fromStringIgnoreErrors(t) + for t in cs[CONF_NON_UNIFORM_ASSEM_FLAGS] ] # some assemblies, like control assemblies, have a non-conforming mesh # and should not be included in self.p.referenceBlockAxialMesh and self.p.axialMesh @@ -2398,7 +2413,7 @@ def processLoading(self, cs, dbLoad: bool = False): # Generate list of flags that are to be stationary during assembly shuffling stationaryBlockFlags = [] - for stationaryBlockFlagString in cs["stationaryBlockFlags"]: + for stationaryBlockFlagString in cs[CONF_STATIONARY_BLOCK_FLAGS]: stationaryBlockFlags.append(Flags.fromString(stationaryBlockFlagString)) self.stationaryBlockFlagsList = stationaryBlockFlags @@ -2443,7 +2458,7 @@ def buildManualZones(self, cs): self.zones = zones.Zones() # parse the special input string for zone definitions - for zoneString in cs["zoneDefinitions"]: + for zoneString in cs[CONF_ZONE_DEFINITIONS]: zoneName, zoneLocs = zoneString.split(":") zoneLocs = zoneLocs.split(",") zone = zones.Zone(zoneName.strip()) From 2dab1222f7ea79d3083db3aabf35b00510bf8d2a Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Fri, 15 Sep 2023 10:08:49 -0700 Subject: [PATCH 007/176] Handling the new matplotlib 3.8.0 API changes. (#1413) --- armi/reactor/converters/blockConverters.py | 2 +- armi/utils/plotting.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/armi/reactor/converters/blockConverters.py b/armi/reactor/converters/blockConverters.py index db8154024..245f0eae4 100644 --- a/armi/reactor/converters/blockConverters.py +++ b/armi/reactor/converters/blockConverters.py @@ -445,7 +445,7 @@ def plotConvertedBlock(self, fName=None): circleComp, innerR, outerR ) ) - circle = Wedge((0.0, 0.0), outerR, 0, 360.0, outerR - innerR) + circle = Wedge((0.0, 0.0), outerR, 0, 360.0, width=outerR - innerR) patches.append(circle) colors.append(circleComp.density()) colorMap = matplotlib.cm diff --git a/armi/utils/plotting.py b/armi/utils/plotting.py index 87d2ad635..64696ec84 100644 --- a/armi/utils/plotting.py +++ b/armi/utils/plotting.py @@ -445,7 +445,7 @@ def _makeAssemPatches(core): x, y, _ = a.spatialLocator.getLocalCoordinates() if nSides == 6: assemPatch = matplotlib.patches.RegularPolygon( - (x, y), nSides, pitch / math.sqrt(3), orientation=math.pi / 2.0 + (x, y), nSides, radius=pitch / math.sqrt(3), orientation=math.pi / 2.0 ) elif nSides == 4: # for rectangle x, y is defined as sides instead of center @@ -529,7 +529,7 @@ def legend_artist(self, _legend, orig_handle, _fontsize, handlebox): patch = matplotlib.patches.RegularPolygon( (x, y), 6, - height, + radius=height, orientation=math.pi / 2.0, facecolor=colorRgb, transform=handlebox.get_transform(), @@ -538,14 +538,14 @@ def legend_artist(self, _legend, orig_handle, _fontsize, handlebox): patch = matplotlib.patches.Rectangle( (x - height / 2, y - height / 2), height * 2, - height, + height * 2, facecolor=colorRgb, transform=handlebox.get_transform(), ) else: patch = matplotlib.patches.Circle( (x, y), - height, + radius=height, facecolor=colorRgb, transform=handlebox.get_transform(), ) @@ -1185,7 +1185,7 @@ def _makeBlockPinPatches(block, cold): x, y, _ = location.getLocalCoordinates() if isinstance(comp, Hexagon): derivedPatch = matplotlib.patches.RegularPolygon( - (x, y), 6, largestPitch / math.sqrt(3) + (x, y), 6, radius=largestPitch / math.sqrt(3) ) elif isinstance(comp, Square): derivedPatch = matplotlib.patches.Rectangle( @@ -1301,7 +1301,7 @@ def _makeComponentPatch(component, position, cold): else: # Just make it a hexagon... blockPatch = matplotlib.patches.RegularPolygon( - (x, y), 6, component.getDimension("op", cold=cold) / math.sqrt(3) + (x, y), 6, radius=component.getDimension("op", cold=cold) / math.sqrt(3) ) elif isinstance(component, Rectangle): @@ -1504,7 +1504,7 @@ def plotTriangleFlux( triangle = patches.mpatches.RegularPolygon( (xInCm, yInCm), 3, - sideLengthInCm / math.sqrt(3), + radius=sideLengthInCm / math.sqrt(3), orientation=math.pi * flipped, linewidth=0.0, ) From aeac447f0c94b2acc8ad7d4c40010a9d2e950c13 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 26 Sep 2023 09:19:05 -0700 Subject: [PATCH 008/176] Switching from setup.py to pyproject.toml (#1409) Here we are switching from `setup.py` to `pyproject.toml`, but the new `pyproject.toml` file actually replaces: * `MANIFEST.in` * `pytest.ini` * `requirements-docs.txt` * `requirements-testing.txt` * `requirements.txt` * `ruff.toml` * `setup.py` Please notice this means that the version of ARMI is now defined in `pyproject.toml` not `armi/meta.py`. So the code has to get the version information from `importlib`. --- .github/pull_request_template.md | 2 +- .github/workflows/validatemanifest.py | 39 +-- MANIFEST.in | 70 ----- README.rst | 11 +- armi/meta.py | 8 +- armi/reactor/blueprints/reactorBlueprint.py | 1 - armi/reactor/reactors.py | 18 +- armi/runLog.py | 6 +- armi/utils/dochelpers.py | 6 +- doc/conf.py | 2 +- doc/developer/documenting.rst | 2 +- doc/developer/first_time_contributors.rst | 6 +- doc/developer/tooling.rst | 26 +- doc/release/0.2.rst | 1 + doc/requirements-docs.txt | 46 --- doc/tutorials/making_your_first_app.rst | 6 +- doc/user/user_install.rst | 8 +- pyproject.toml | 305 ++++++++++++++++++++ pytest.ini | 8 - requirements-testing.txt | 4 - requirements.txt | 5 - ruff.toml | 89 ------ setup.py | 109 ------- tox.ini | 23 +- 24 files changed, 377 insertions(+), 424 deletions(-) delete mode 100644 MANIFEST.in delete mode 100644 doc/requirements-docs.txt create mode 100644 pyproject.toml delete mode 100644 pytest.ini delete mode 100644 requirements-testing.txt delete mode 100644 requirements.txt delete mode 100644 ruff.toml delete mode 100644 setup.py diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e554498a2..77e424a45 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -32,4 +32,4 @@ - [ ] The [release notes](https://terrapower.github.io/armi/release/index.html) (location `doc/release/0.X.rst`) are up-to-date with any important changes. - [ ] The documentation is still up-to-date in the `doc` folder. -- [ ] The dependencies are still up-to-date in `setup.py`. +- [ ] The dependencies are still up-to-date in `pyproject.toml`. diff --git a/.github/workflows/validatemanifest.py b/.github/workflows/validatemanifest.py index 08ac8303a..2e1fd901a 100644 --- a/.github/workflows/validatemanifest.py +++ b/.github/workflows/validatemanifest.py @@ -13,39 +13,44 @@ # limitations under the License. """ -Validating the MANIFEST.in. +Validating the package-data in the pyproject.toml. -Currently, the only validation we do of the MANIFEST.in file is to make sure -that we are trying to include files that don't exist. +Validate that we aren't trying to include files that don't exist. """ +from glob import glob import os - +import toml # CONSTANTS -INCLUDE_STR = "include " -MANIFEST_PATH = "MANIFEST.in" +ARMI_DIR = "armi/" +PRPROJECT = "pyproject.toml" def main(): - # loop through each line in the manifest file and find all the file paths - errors = [] - lines = open(MANIFEST_PATH, "r", encoding="utf-8") - for i, line in enumerate(lines): - # if this is anything but an include line, move on - if not line.startswith(INCLUDE_STR): - continue + # parse the data files out of the pyproject.toml + txt = open(PRPROJECT, "r").read() + data = toml.loads(txt) + fileChunks = data["tool"]["setuptools"]["package-data"]["armi"] + # loop through each line in the package-data and find all the file paths + errors = [] + for i, line in enumerate(fileChunks): # make sure the file exists - path = line.strip()[len(INCLUDE_STR) :] - if not os.path.exists(path): - errors.append((i, path)) + path = ARMI_DIR + line.strip() + if "*" in path: + paths = [f for f in glob(path) if len(f) > 3] + if not len(paths): + errors.append((i, path)) + else: + if not os.path.exists(path): + errors.append((i, path)) # If there were any missing files, raise an Error. if errors: for (i, line) in errors: print("Nonexistant file on line {}: {}".format(i, line)) - raise ValueError("MANIFEST file is incorrect: includes non-existant files.") + raise ValueError("Package-data file is incorrect: includes non-existant files.") if __name__ == "__main__": diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 372ebd74e..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,70 +0,0 @@ -include armi/bookkeeping/tests/armiRun-A0032-aHist-ref.txt -include armi/nuclearDataIO/cccc/tests/fixtures/labels.ascii -include armi/nuclearDataIO/cccc/tests/fixtures/labels.binary -include armi/nuclearDataIO/cccc/tests/fixtures/mc2v3.dlayxs -include armi/nuclearDataIO/cccc/tests/fixtures/simple_cartesian.pwdint -include armi/nuclearDataIO/cccc/tests/fixtures/simple_cartesian.rtflux -include armi/nuclearDataIO/cccc/tests/fixtures/simple_cartesian.rzflux -include armi/nuclearDataIO/cccc/tests/fixtures/simple_hexz.dif3d -include armi/nuclearDataIO/cccc/tests/fixtures/simple_hexz.geodst -include armi/nuclearDataIO/cccc/tests/fixtures/simple_hexz.nhflux -include armi/nuclearDataIO/tests/fixtures/AA.gamiso -include armi/nuclearDataIO/tests/fixtures/AA.pmatrx -include armi/nuclearDataIO/tests/fixtures/AB.gamiso -include armi/nuclearDataIO/tests/fixtures/AB.pmatrx -include armi/nuclearDataIO/tests/fixtures/ISOAA -include armi/nuclearDataIO/tests/fixtures/ISOAB -include armi/nuclearDataIO/tests/fixtures/combined-AA-AB.gamiso -include armi/nuclearDataIO/tests/fixtures/combined-AA-AB.isotxs -include armi/nuclearDataIO/tests/fixtures/combined-AA-AB.pmatrx -include armi/nuclearDataIO/tests/fixtures/combined-and-lumped-AA-AB.gamiso -include armi/nuclearDataIO/tests/fixtures/combined-and-lumped-AA-AB.isotxs -include armi/nuclearDataIO/tests/fixtures/combined-and-lumped-AA-AB.pmatrx -include armi/nuclearDataIO/tests/fixtures/mc2v3-AA.flux_ufg -include armi/nuclearDataIO/tests/fixtures/mc2v3-AA.gamiso -include armi/nuclearDataIO/tests/fixtures/mc2v3-AA.isotxs -include armi/nuclearDataIO/tests/fixtures/mc2v3-AA.pmatrx -include armi/nuclearDataIO/tests/fixtures/mc2v3-AB.gamiso -include armi/nuclearDataIO/tests/fixtures/mc2v3-AB.isotxs -include armi/nuclearDataIO/tests/fixtures/mc2v3-AB.pmatrx -include armi/nuclearDataIO/tests/library-file-generation -include armi/nuclearDataIO/tests/library-file-generation/combine-AA-AB.inp -include armi/nuclearDataIO/tests/library-file-generation/combine-and-lump-AA-AB.inp -include armi/nuclearDataIO/tests/library-file-generation/mc2v2-dlayxs.inp -include armi/nuclearDataIO/tests/library-file-generation/mc2v3-AA.inp -include armi/nuclearDataIO/tests/library-file-generation/mc2v3-AB.inp -include armi/nuclearDataIO/tests/library-file-generation/mc2v3-dlayxs.inp -include armi/nuclearDataIO/tests/simple_hexz.inp -include armi/tests/1DslabXSByCompTest.yaml -include armi/tests/COMPXS.ascii -include armi/tests/ISOAA -include armi/tests/ThRZGeom.xml -include armi/tests/ThRZSettings.yaml -include armi/tests/ThRZloading.yaml -include armi/tests/armiRun-SHUFFLES.txt -include armi/tests/armiRun.yaml -include armi/tests/detailedAxialExpansion/armiRun.yaml -include armi/tests/detailedAxialExpansion/refSmallReactor.yaml -include armi/tests/detailedAxialExpansion/refSmallReactorBase.yaml -include armi/tests/detailedAxialExpansion/refSmallCoreGrid.yaml -include armi/tests/geom.xml -include armi/tests/geom1Assem.xml -include armi/tests/refOneBlockReactor.yaml -include armi/tests/refSmallCartesian.yaml -include armi/tests/refSmallCoreGrid.yaml -include armi/tests/refSmallReactor.yaml -include armi/tests/refSmallReactorBase.yaml -include armi/tests/refSmallSfpGrid.yaml -include armi/tests/refTestCartesian.yaml -include armi/tests/sfpGeom.yaml -include armi/tests/trz_geom.xml -include armi/tests/tutorials -include armi/tests/tutorials/anl-afci-177-blueprints.yaml -include armi/tests/tutorials/anl-afci-177-coreMap.yaml -include armi/tests/tutorials/anl-afci-177-fuelManagement.py -include armi/tests/tutorials/anl-afci-177.yaml -include armi/tests/tutorials/data_model.ipynb -include armi/tests/zpprTest.yaml -include armi/tests/zpprTestGeom.xml -include armi/physics/neutronics/tests/ISOXA -include armi/physics/neutronics/tests/rzmflxYA diff --git a/README.rst b/README.rst index f44420b5c..a5a9e43f7 100644 --- a/README.rst +++ b/README.rst @@ -72,14 +72,13 @@ dependencies could conflict with your system dependencies. $ git clone https://github.com/terrapower/armi $ cd armi - $ pip3 install -r requirements.txt - $ python3 setup.py install + $ pip install -e . $ armi The easiest way to run the tests is to install `tox `_ and then run:: - $ pip3 install -r requirements-testing.txt + $ pip install -e .[test] $ tox -- -n 6 This runs the unit tests in parallel on 6 processes. Omit the ``-n 6`` argument @@ -87,7 +86,7 @@ to run on a single process. The tests can also be run directly, using ``pytest``:: - $ pip3 install -r requirements-testing.txt + $ pip install -e .[test] $ pytest -n 4 armi From here, we recommend going through a few of our `gallery examples @@ -313,7 +312,6 @@ see if their idea has wings, and if it does, they can then find a way to bring it to engineering and commercial reality. - History of ARMI --------------- ARMI was originally created by TerraPower, LLC near Seattle WA starting in 2009. Its @@ -397,7 +395,6 @@ Most of our code is in the ``camelCase`` style, which is not the normal style fo Python. This started in 2009 and we have stuck with the convention. - License ------- TerraPower and ARMI are registered trademarks of TerraPower, LLC. @@ -422,7 +419,7 @@ The ARMI system is licensed as follows: See the License for the specific language governing permissions and limitations under the License. -Be careful when including any dependency in ARMI (say in a requirements.txt file) not +Be careful when including any dependency in ARMI (say in the ``pyproject.toml`` file) not to include anything with a license that superceeds our Apache license. For instance, any third-party Python library included in ARMI with a GPL license will make the whole project fall under the GPL license. But a lot of potential users of ARMI will want to diff --git a/armi/meta.py b/armi/meta.py index 311ff1d76..e167c7580 100644 --- a/armi/meta.py +++ b/armi/meta.py @@ -13,5 +13,11 @@ # limitations under the License. """Metadata describing an ARMI distribution.""" +try: + # Python 3.x < 3.8 + from importlib import metadata +except ImportError: + # Python >= 3.8 + import importlib_metadata as metadata -__version__ = "0.2.8" +__version__ = metadata.version("armi") diff --git a/armi/reactor/blueprints/reactorBlueprint.py b/armi/reactor/blueprints/reactorBlueprint.py index b58b25323..6a67b7e45 100644 --- a/armi/reactor/blueprints/reactorBlueprint.py +++ b/armi/reactor/blueprints/reactorBlueprint.py @@ -126,7 +126,6 @@ def construct(self, cs, bp, reactor, geom=None, loadAssems=True): runLog.info("Constructing the `{}`".format(self.name)) - # TODO: We should consider removing automatic geom file migration. if geom is not None and self.name == "core": gridDesign = geom.toGridBlueprints("core")[0] else: diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index 39cdde1a9..f3215cfd7 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -801,7 +801,6 @@ def getNumAssembliesWithAllRingsFilledOut(self, nRings): nAssmWithBlanks: int The number of assemblies that WOULD exist in this core if all outer assembly hex rings were "filled out". - """ if self.powerMultiplier == 1: return 3 * nRings * (nRings - 1) + 1 @@ -846,9 +845,8 @@ def countBlocksWithFlags(self, blockTypeSpec, assemTypeSpec=None): return 0 def countFuelAxialBlocks(self): - r""" - return the maximum number of fuel type blocks in any assembly in - the reactor. + """ + Return the maximum number of fuel type blocks in any assembly in the core. See Also -------- @@ -945,7 +943,6 @@ def getMaxAssembliesInHexRing(self, ring, fullCore=False): ----- Assumes that odd rings do not have an edge assembly in third core geometry. These should be removed in: self._modifyGeometryAfterLoad during importGeom - """ numAssemsUpToOuterRing = self.getNumAssembliesWithAllRingsFilledOut(ring) numAssemsUpToInnerRing = self.getNumAssembliesWithAllRingsFilledOut(ring - 1) @@ -1033,7 +1030,6 @@ def getAssembliesInCircularRing( ------- assems : list of assemblies A list of assemblies that match the criteria within the ring - """ if self.geomType == geometry.GeomType.CARTESIAN: # a ring in cartesian is basically a square. @@ -2177,7 +2173,7 @@ def getMinimumPercentFluxInFuel(self, target=0.005): return targetRing, fluxFraction def getAvgTemp(self, typeSpec, blockList=None, flux2Weight=False): - r""" + """ get the volume-average fuel, cladding, coolant temperature in core. Parameters @@ -2197,7 +2193,6 @@ def getAvgTemp(self, typeSpec, blockList=None, flux2Weight=False): ------- avgTemp : float The average temperature in C. - """ num = 0.0 denom = 0.0 @@ -2255,7 +2250,7 @@ def getAllNuclidesIn(self, mats): return list(allNucNames) def growToFullCore(self, cs): - r"""Copies symmetric assemblies to build a full core model out of a 1/3 core model. + """Copies symmetric assemblies to build a full core model out of a 1/3 core model. Returns ------- @@ -2280,7 +2275,7 @@ def setPitchUniform(self, pitchInCm): self.spatialGrid.changePitch(pitchInCm) def calcBlockMaxes(self): - r""" + """ Searches all blocks for maximum values of key params. See Also @@ -2320,7 +2315,7 @@ def calcBlockMaxes(self): ) def getFuelBottomHeight(self): - r""" + """ Obtain the height of the lowest fuel in the core. This is the "axial coordinate shift" between ARMI and SASSYS. @@ -2331,7 +2326,6 @@ def getFuelBottomHeight(self): ------- lowestFuelHeightInCm : float The height (cm) of the lowest fuel in this core model. - """ lowestFuelHeightInCm = self[0].getHeight() fuelBottoms = [] diff --git a/armi/runLog.py b/armi/runLog.py index b69ecca88..faa4aa974 100644 --- a/armi/runLog.py +++ b/armi/runLog.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -r""" +""" This module handles logging of console during a simulation. The default way of calling and the global armi logger is to just import it: @@ -44,7 +44,6 @@ runLog.setVerbosity('debug') """ -from __future__ import print_function from glob import glob import collections import logging @@ -70,7 +69,8 @@ class _RunLog: """ - Handles all the logging + Handles all the logging. + For the parent process, things are allowed to print to stdout and stderr, but the stdout prints are formatted like log statements. For the child processes, everything is piped to log files. diff --git a/armi/utils/dochelpers.py b/armi/utils/dochelpers.py index 00332d69f..f0cc57d6a 100644 --- a/armi/utils/dochelpers.py +++ b/armi/utils/dochelpers.py @@ -31,7 +31,7 @@ def create_figure(path, caption=None, align=None, alt=None, width=None): - r""" + """ This method is available within ``.. exec::``. It allows someone to create a figure with a caption. """ @@ -50,7 +50,7 @@ def create_figure(path, caption=None, align=None, alt=None, width=None): def create_table(rst_table, caption=None, align=None, widths=None, width=None): - r""" + """ This method is available within ``.. exec::``. It allows someone to create a table with a caption. @@ -119,7 +119,7 @@ def usermethod(): class PyReverse(Directive): - r"""Runs pyreverse to generate UML for specified module name and options. + """Runs pyreverse to generate UML for specified module name and options. The directive accepts the same arguments as pyreverse, except you should not specify ``--project`` or ``-o`` (output format). These are automatically specified. diff --git a/doc/conf.py b/doc/conf.py index 3d0624215..952fa4989 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -91,7 +91,7 @@ def autodoc_skip_member_handler(app, what, name, obj, skip, options): def setup(app): - """Method to make `python setup.py build_sphinx` generate api documentation.""" + """Method to make `make html` generate api documentation.""" app.connect("autodoc-skip-member", autodoc_skip_member_handler) app.add_domain(PatchedPythonDomain, override=True) diff --git a/doc/developer/documenting.rst b/doc/developer/documenting.rst index dcbe3187f..5544000e6 100644 --- a/doc/developer/documenting.rst +++ b/doc/developer/documenting.rst @@ -21,7 +21,7 @@ Building the documentation Before building documentation, ensure that you have installed the test requirements into your ARMI virtual environment with:: - pip3 install -r requirements-testing.txt + pip install -e .[test] You also need to have the following utilities available in your PATH: diff --git a/doc/developer/first_time_contributors.rst b/doc/developer/first_time_contributors.rst index 93a0d64ae..f85c80120 100644 --- a/doc/developer/first_time_contributors.rst +++ b/doc/developer/first_time_contributors.rst @@ -30,7 +30,7 @@ new your code does. The standard way to run the tests is to install and use `tox `_:: - $ pip3 install tox + $ pip install tox $ tox -- -n 6 This runs the unit tests in parallel on 6 processes. Omit the ``-n 6`` argument @@ -38,7 +38,7 @@ to run on a single process. Or the tests can also be run using ``pytest`` directly:: - $ pip3 install -r requirements-testing.txt + $ pip intall -e .[test] $ pytest -n 4 armi Submitting Changes @@ -66,7 +66,7 @@ Also, please check out our (quick) synopsis on :doc:`good commit messages `_ about dependecy management in -Python projects. +challenging and nuanced. The contents of our ``pyproject.toml`` follow existing conventions as +much as possible. In particular, we follow `the official Python packaging guidance +`_. + +pyproject.toml +^^^^^^^^^^^^^^ +As much as possible, the ARMI team will try to centralize our installation and build systems +through the top-level ``pyproject.toml`` file. The only exception will be our documentation, +which has much customization done through the Sphinx ``doc/conf.py`` file. -setup.py -^^^^^^^^ The packages listed in the ``install_requires`` argument to ``setup()`` are meant to express, as abstractly as possible, the packages that need to be installed **somehow** for the package to work. In addition, ``extras_require`` are used to specify other packages that are not strictly required, but if installed enable extra functionality, like unit testing or building documentation. -requirements.txt -^^^^^^^^^^^^^^^^ -The ``requirements***.txt`` files exist to describe a complete environment more -specifically. If specific versions of packages are required, they should be defined here. -Any extra arguments to ``pip`` will also be placed here. For instance, there is a ``-e`` -that tells ``pip`` to install ARMI itself and defer to ``setup.py`` for a version-agnostic -list of dependencies. We also have multiple requirements files for different needs, like -testing. - Third-Party Licensing ^^^^^^^^^^^^^^^^^^^^^ -Be careful when including any dependency in ARMI (say in a requirements.txt file) not +Be careful when including any dependency in ARMI (say in the ``pyproject.toml`` file) not to include anything with a license that superceeds our Apache license. For instance, any third-party Python library included in ARMI with a GPL license will make the whole project fall under the GPL license. But a lot of potential users of ARMI will want to diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst index d04ee4171..2a41d5945 100644 --- a/doc/release/0.2.rst +++ b/doc/release/0.2.rst @@ -14,6 +14,7 @@ What's new in ARMI #. Moved the ``Reactor`` assembly number from the global scope to a ``Parameter``. (`PR#1383 `_) #. Added ``powerDensity`` as a high-level alternative to ``power`` to configure a Reactor. (`PR#1395 `_) #. Added SHA1 hashes of XS control files to the welcome text. (`PR#1334 `_) +#. Moved from ``setup.py`` to ``pyproject.toml``. (`PR#1409 `_) #. Add python 3.11 to ARMI's CI testing GH actions! (`PR#1341 `_) #. Put back ``avgFuelTemp`` block parameter. (`PR#1362 `_) #. Make cylindrical component block collection less strict about pre-homogenization checks. (`PR#1347 `_) diff --git a/doc/requirements-docs.txt b/doc/requirements-docs.txt deleted file mode 100644 index fccddce61..000000000 --- a/doc/requirements-docs.txt +++ /dev/null @@ -1,46 +0,0 @@ -# these are most specified that usual, because Sphinx docs seem to be quite fragile -# limited <7 by sphinx-rtd-theme at the moment. -# sphinx-rtd-theme requires docutils <0.19 but sphinx dropped support for 0.18 in 6.0.0 -# so we're stuck at these versions -Sphinx==5.3.0 -docutils==0.18.1 - -# Read-The-Docs theme for Sphinx -sphinx-rtd-theme==1.2.2 - -# Parses Jupyter notebooks -nbsphinx==0.9.2 - -# Includes Jupyter notebooks in Sphinx source root -nbsphinx-link==1.3.0 - -# builds a HTML version of a Python script and puts it into a gallery -sphinx-gallery==0.13.0 - -# allows us to more easily document our API -sphinxcontrib-apidoc==0.3.0 - -# generates OpenGraph metadata to make good-looking cards on social media -sphinxext-opengraph==0.8.2 - -# supporting requirements traceability matrices for QA -sphinx-needs==1.2.2 - -# uml support in sphinx-needs -sphinxcontrib-plantuml==0.25 - -# This is not strictly necessary via PIP, but MUST be in the path. -# Used to convert between file formats. -pandoc - -# iPython kernel to run Jupyter notebooks -ipykernel==6.25.1 - -# used to generate UML diagrams -pylint==2.17.5 - -# used in numpydoc and nbconvert -Jinja2==3.0.3 - -# deal with missing jquery errors -sphinxcontrib-jquery==4.1 diff --git a/doc/tutorials/making_your_first_app.rst b/doc/tutorials/making_your_first_app.rst index 558ea35ec..a5f62b0de 100644 --- a/doc/tutorials/making_your_first_app.rst +++ b/doc/tutorials/making_your_first_app.rst @@ -51,7 +51,7 @@ files for us to fill in, like this:: materials.py thermalSolver.py doc/ - setup.py + pyproject.toml README.md LICENSE.md @@ -82,8 +82,8 @@ These files are: * :file:`myapp/thermalSolver.py` contains the thermal/hydraulics solver -* :file:`setup.py` the `python package installation file - `_ to help users install your +* :file:`pyproject.toml` the `python package installation file + `_ to help users install your application. * :file:`README.md` and :file:`LICENSE.md` are an optional description and license of your diff --git a/doc/user/user_install.rst b/doc/user/user_install.rst index 9f3106928..4313d6528 100644 --- a/doc/user/user_install.rst +++ b/doc/user/user_install.rst @@ -82,14 +82,10 @@ the git repository with:: .. tip:: If you plan to contribute to ARMI (please do!), you may want to use SSH keys and use ``git clone git@github.com:terrapower/armi.git``. -Now install the ARMI dependencies:: +Now install ARMI with all its dependencies:: (armi-venv) $ cd armi - (armi-venv) $ pip install -r requirements.txt - -Then, install ARMI into your venv with:: - - (armi-venv) $ pip install -e . + (armi-venv) $ pip install -e .[test] .. tip:: If you don't want to install ARMI into your venv, you will need to add the ARMI source location to your system's ``PYTHONPATH`` environment variable so that diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..b1266ce4d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,305 @@ +# Copyright 2023 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. + +####################################################################### +# GENERAL PYTHON CONFIG # +####################################################################### +[build-system] +requires = ["setuptools>=61.2"] +build-backend = "setuptools.build_meta" + +[project] +name = "armi" +version = "0.2.8" +description = "An open-source nuclear reactor analysis automation framework that helps design teams increase efficiency and quality." +license = {file = "LICENSE.md"} +requires-python = ">3.6" +readme = "README.rst" +authors = [ + { name="TerraPower, LLC", email="armi-devs@terrapower.com" }, +] +dependencies = [ + "configparser", + "coverage", + "h5py>=3.0", + "htmltree", + "matplotlib", + "numpy>=1.21,<=1.23.5", + "ordered-set", + "pillow", + "pluggy", + "pyDOE", + "pyevtk", + "ruamel.yaml<=0.17.21", + "ruamel.yaml.clib<=0.2.7", + "scipy", + "tabulate", + "toml>0.9.5", + "voluptuous", + "xlrd", + "yamlize==0.7.1", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Scientific/Engineering :: Information Analysis", +] + +[project.urls] +Homepage = "https://terrapower.github.io/armi/" +Documentation = "https://terrapower.github.io/armi" +Changelog = "https://github.com/terrapower/armi/releases" +Repository = "https://github.com/terrapower/armi" +"Bug Tracker" = "https://github.com/terrapower/armi/issues" + +[project.optional-dependencies] +grids = ["wxpython==4.2.1"] +memprof = ["psutil"] +mpi = ["mpi4py"] +test = [ + "black==22.6.0", + "docutils", + "ipykernel", + "jupyter-contrib-nbextensions", + "jupyter_client", + "nbconvert", + "pytest", + "pytest-cov", + "pytest-html", + "pytest-xdist", + "ruff==0.0.272", +] +docs = [ +####################################################################### +# These are most specified that usual, because Sphinx docs seem to be +# quite fragile limited <7 by sphinx-rtd-theme at the moment. +# +# sphinx-rtd-theme requires docutils <0.19 but sphinx dropped support +# for 0.18 in 6.0.0 so we're stuck at these versions. +# +# We are only building our docs with Python 3.9. +####################################################################### + "Sphinx==5.3.0", + "docutils==0.18.1", + "sphinx-rtd-theme==1.2.2", # Read-The-Docs theme for Sphinx + "nbsphinx==0.9.2", # Parses Jupyter notebooks + "nbsphinx-link==1.3.0", # Adds Jupyter NBs to Sphinx source root + "sphinx-gallery==0.13.0", # Builds an HTML version of a Python script and puts it into a gallery + "sphinxcontrib-apidoc==0.3.0", # More easily document our API + "sphinxext-opengraph==0.8.2", # Generates OpenGraph metadata to make good-looking cards on social media + "sphinx-needs==1.2.2", # Requirements traceability matrices for QA + "sphinxcontrib-plantuml==0.25", # UML support in sphinx-needs + "pandoc", # Must be in the path (to convert file formats) + "ipykernel==6.25.1", # iPython kernel to run Jupyter notebooks + "pylint==2.17.5", # Generates UML diagrams + "Jinja2==3.0.3", # Used in numpydoc and nbconvert + "sphinxcontrib-jquery==4.1", # Handle missing jquery errors +] + +[project.scripts] +armi = "armi.__main__:main" + +[tool.setuptools.packages] +find = {} + + +####################################################################### +# RUFF CONFIG # +####################################################################### +[tool.ruff] +# This is the exact version of Ruff we use. +required-version = "0.0.272" + +# Assume Python 3.9 +target-version = "py39" + +# Setting line-length to 88 to match Black +line-length = 88 + +# Enable pycodestyle (E) and Pyflakes (F) codes by default. +# D - NumPy docstring rules +# N801 - Class name should use CapWords convention +# SIM - code simplification rules +# TID - tidy imports +# TODO: We want to support PLW0603 - don't use the global keyword +select = ["E", "F", "D", "N801", "SIM", "TID"] + +# Ruff rules we ignore (for now) because they are not 100% automatable +# +# D100 - Missing docstring in public module +# D101 - Missing docstring in public class +# D102 - Missing docstring in public method +# D103 - Missing docstring in public function +# D106 - Missing docstring in public nested class +# D401 - First line of docstring should be in imperative mood +# D404 - First word of the docstring should not be "This" +# SIM102 - Use a single if statement instead of nested if statements +# SIM105 - Use contextlib.suppress({exception}) instead of try-except-pass +# SIM108 - Use ternary operator {contents} instead of if-else-block +# SIM114 - Combine if branches using logical or operator +# SIM115 - Use context handler for opening files +# SIM117 - Use a single with statement with multiple contexts instead of nested with statements + +# Ruff rules we ignore because we don't want them +# +# D105 - we don't need to document well-known magic methods +# D205 - 1 blank line required between summary line and description +# E501 - line length, because we use Black for that +# E731 - we can use lambdas however we want +# RUF100 - no unused noqa statements (not consistent enough yet) +# SIM118 - this does not work where we overload the .keys() method +# +ignore = ["D100", "D101", "D102", "D103", "D105", "D106", "D205", "D401", "D404", "E501", "E731", "RUF100", "SIM102", "SIM105", "SIM108", "SIM114", "SIM115", "SIM117", "SIM118"] + +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pycache__", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", +] + +[tool.ruff.per-file-ignores] +# D1XX - enforces writing docstrings +# E741 - ambiguous variable name +# N - We have our own naming conventions for unit tests. +# SLF001 - private member access +"*/tests/*" = ["D1", "E741", "N", "SLF001"] +"doc/gallery-src/*" = ["D400"] + +[tool.ruff.flake8-tidy-imports] +ban-relative-imports = "all" + +[tool.ruff.pydocstyle] +convention = "numpy" + + +####################################################################### +# PYTEST CONFIG # +####################################################################### +[tool.pytest.ini_options] +python_files = "test_*.py" +python_functions = "nothing matches this pattern" +addopts = "--durations=30 --tb=native" +filterwarnings = [ + "ignore: the matrix subclass is not the recommended way:PendingDeprecationWarning", +] + + +####################################################################### +# DATA FILES TO BE INCLUDED WITH THE PROJECT # +####################################################################### +[tool.setuptools.package-data] +armi = [ + "bookkeeping/tests/armiRun-A0032-aHist-ref.txt", + "nuclearDataIO/cccc/tests/fixtures/labels.ascii", + "nuclearDataIO/cccc/tests/fixtures/labels.binary", + "nuclearDataIO/cccc/tests/fixtures/mc2v3.dlayxs", + "nuclearDataIO/cccc/tests/fixtures/simple_cartesian.pwdint", + "nuclearDataIO/cccc/tests/fixtures/simple_cartesian.rtflux", + "nuclearDataIO/cccc/tests/fixtures/simple_cartesian.rzflux", + "nuclearDataIO/cccc/tests/fixtures/simple_hexz.dif3d", + "nuclearDataIO/cccc/tests/fixtures/simple_hexz.geodst", + "nuclearDataIO/cccc/tests/fixtures/simple_hexz.nhflux", + "nuclearDataIO/tests/fixtures/AA.gamiso", + "nuclearDataIO/tests/fixtures/AA.pmatrx", + "nuclearDataIO/tests/fixtures/AB.gamiso", + "nuclearDataIO/tests/fixtures/AB.pmatrx", + "nuclearDataIO/tests/fixtures/combined-AA-AB.gamiso", + "nuclearDataIO/tests/fixtures/combined-AA-AB.isotxs", + "nuclearDataIO/tests/fixtures/combined-AA-AB.pmatrx", + "nuclearDataIO/tests/fixtures/combined-and-lumped-AA-AB.gamiso", + "nuclearDataIO/tests/fixtures/combined-and-lumped-AA-AB.isotxs", + "nuclearDataIO/tests/fixtures/combined-and-lumped-AA-AB.pmatrx", + "nuclearDataIO/tests/fixtures/ISOAA", + "nuclearDataIO/tests/fixtures/ISOAB", + "nuclearDataIO/tests/fixtures/mc2v3-AA.flux_ufg", + "nuclearDataIO/tests/fixtures/mc2v3-AA.gamiso", + "nuclearDataIO/tests/fixtures/mc2v3-AA.isotxs", + "nuclearDataIO/tests/fixtures/mc2v3-AA.pmatrx", + "nuclearDataIO/tests/fixtures/mc2v3-AB.gamiso", + "nuclearDataIO/tests/fixtures/mc2v3-AB.isotxs", + "nuclearDataIO/tests/fixtures/mc2v3-AB.pmatrx", + "nuclearDataIO/tests/library-file-generation", + "nuclearDataIO/tests/library-file-generation/combine-AA-AB.inp", + "nuclearDataIO/tests/library-file-generation/combine-and-lump-AA-AB.inp", + "nuclearDataIO/tests/library-file-generation/mc2v2-dlayxs.inp", + "nuclearDataIO/tests/library-file-generation/mc2v3-AA.inp", + "nuclearDataIO/tests/library-file-generation/mc2v3-AB.inp", + "nuclearDataIO/tests/library-file-generation/mc2v3-dlayxs.inp", + "nuclearDataIO/tests/simple_hexz.inp", + "physics/neutronics/tests/ISOXA", + "physics/neutronics/tests/rzmflxYA", + "resources/*", + "resources/**/*", + "tests/1DslabXSByCompTest.yaml", + "tests/armiRun-SHUFFLES.txt", + "tests/armiRun.yaml", + "tests/COMPXS.ascii", + "tests/detailedAxialExpansion/armiRun.yaml", + "tests/detailedAxialExpansion/refSmallCoreGrid.yaml", + "tests/detailedAxialExpansion/refSmallReactor.yaml", + "tests/detailedAxialExpansion/refSmallReactorBase.yaml", + "tests/geom.xml", + "tests/geom1Assem.xml", + "tests/ISOAA", + "tests/refOneBlockReactor.yaml", + "tests/refSmallCartesian.yaml", + "tests/refSmallCoreGrid.yaml", + "tests/refSmallReactor.yaml", + "tests/refSmallReactorBase.yaml", + "tests/refSmallSfpGrid.yaml", + "tests/refTestCartesian.yaml", + "tests/sfpGeom.yaml", + "tests/ThRZGeom.xml", + "tests/ThRZloading.yaml", + "tests/ThRZSettings.yaml", + "tests/trz_geom.xml", + "tests/tutorials", + "tests/tutorials/anl-afci-177-blueprints.yaml", + "tests/tutorials/anl-afci-177-coreMap.yaml", + "tests/tutorials/anl-afci-177-fuelManagement.py", + "tests/tutorials/anl-afci-177.yaml", + "tests/tutorials/data_model.ipynb", + "tests/zpprTest.yaml", + "tests/zpprTestGeom.xml", +] diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 84c958473..000000000 --- a/pytest.ini +++ /dev/null @@ -1,8 +0,0 @@ -[pytest] -python_files=test_*.py -python_functions=nothing matches this pattern -addopts = --durations=30 --tb=native -filterwarnings = - ignore:\s*the matrix subclass is not the recommended way:PendingDeprecationWarning -xvfb_width = 1200 -xvfb_height = 1050 diff --git a/requirements-testing.txt b/requirements-testing.txt deleted file mode 100644 index a30ad27ce..000000000 --- a/requirements-testing.txt +++ /dev/null @@ -1,4 +0,0 @@ -numpy>=1.21,<=1.23.5 -ipython>=7.34.0,<=8.12.0 - --e .[memprof,dev] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 52edb286b..000000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -# see https://github.com/advisories/GHSA-6p56-wp2h-9hxr -# This is included in requirements.txt because of a security alert numpy released -numpy>=1.21,<=1.23.5 - --e .[memprof] diff --git a/ruff.toml b/ruff.toml deleted file mode 100644 index a221eaf5f..000000000 --- a/ruff.toml +++ /dev/null @@ -1,89 +0,0 @@ -# This is the ruff config file for ARMI. -# We use Ruff along with Black to format/lint our code. - -# This is the exact version of Ruff we use. -required-version = "0.0.272" - -# Enable pycodestyle (E) and Pyflakes (F) codes by default. -# D - NumPy docstring rules -# N801 - Class name should use CapWords convention -# SIM - code simplification rules -# TID - tidy imports -select = ["E", "F", "D", "N801", "SIM", "TID"] - -# TODO: We want to support PLW0603 - don't use the global keyword - -# Ruff rules we ignore (for now) because they are not 100% automatable -# -# D100 - Missing docstring in public module -# D101 - Missing docstring in public class -# D102 - Missing docstring in public method -# D103 - Missing docstring in public function -# D106 - Missing docstring in public nested class -# D401 - First line of docstring should be in imperative mood -# D404 - First word of the docstring should not be "This" -# SIM102 - Use a single if statement instead of nested if statements -# SIM105 - Use contextlib.suppress({exception}) instead of try-except-pass -# SIM108 - Use ternary operator {contents} instead of if-else-block -# SIM114 - Combine if branches using logical or operator -# SIM115 - Use context handler for opening files -# SIM117 - Use a single with statement with multiple contexts instead of nested with statements - -# Ruff rules we ignore because we don't want them -# -# D105 - we don't need to document well-known magic methods -# D205 - 1 blank line required between summary line and description -# E501 - line length, because we use Black for that -# E731 - we can use lambdas however we want -# RUF100 - no unused noqa statements (not consistent enough yet) -# SIM118 - this does not work where we overload the .keys() method -# -ignore = ["D100", "D101", "D102", "D103", "D105", "D106", "D205", "D401", "D404", "E501", "E731", "RUF100", "SIM102", "SIM105", "SIM108", "SIM114", "SIM115", "SIM117", "SIM118"] - -# Exclude a variety of commonly ignored directories. -exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".mypy_cache", - ".nox", - ".pants.d", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - "__pycache__", - "__pypackages__", - "_build", - "buck-out", - "build", - "dist", - "node_modules", - "venv", -] - -# Assume Python 3.9 -target-version = "py39" - -# Setting line-length to 88 to match Black -line-length = 88 - -[flake8-tidy-imports] -# Disallow all relative imports. -ban-relative-imports = "all" - -[per-file-ignores] -# D1XX - enforces writing docstrings -# E741 - ambiguous variable name -# N - We have our own naming conventions for unit tests. -# SLF001 - private member access -"*/tests/*" = ["D1", "E741", "N", "SLF001"] -"doc/gallery-src/*" = ["D400"] - -[pydocstyle] -# Use numpy-style docstrings. -convention = "numpy" diff --git a/setup.py b/setup.py deleted file mode 100644 index 7b21398dd..000000000 --- a/setup.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright 2019 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. - -"""Setup.py script for the Advanced Reactor Modeling Interface (ARMI).""" -from setuptools import setup, find_packages -import os -import pathlib -import re - -# grab __version__ from meta.py, without calling __init__.py -this_file = pathlib.Path(__file__).parent.absolute() -exec(open(os.path.join(this_file, "armi", "meta.py"), "r").read()) - -with open("README.rst") as f: - README = f.read() - - -def collectExtraFiles(): - extraFiles = [] - with open("MANIFEST.in", "r") as fin: - # include everything from the MANIFEST.in. MANIFEST.in is somewhat unreliable, - # in that it only shuffles files around when making an `sdist`; it doesn't - # install files. `package_data` does though, which we want. - for f in fin: - extraFiles.append(re.sub(r"^include\s+armi/", "", f).strip()) - - return extraFiles - - -EXTRA_FILES = collectExtraFiles() - - -setup( - name="armi", - version=__version__, # noqa: undefined-name - description="The Advanced Reactor Modeling Interface", - author="TerraPower, LLC", - author_email="armi-devs@terrapower.com", - url="https://github.com/terrapower/armi/", - license="Apache 2.0", - long_description=README, - python_requires=">=3.7", - packages=find_packages(), - package_data={"armi": ["resources/*", "resources/**/*"] + EXTRA_FILES}, - entry_points={"console_scripts": ["armi = armi.__main__:main"]}, - install_requires=[ - "configparser", - "coverage", - "future", - "h5py>=3.0", - "htmltree", - "matplotlib", - "numpy>=1.21,<=1.23.5", - "ordered-set", - "pillow", - "pluggy", - "pyDOE", - "pyevtk", - "ruamel.yaml<=0.17.21", - "ruamel.yaml.clib<=0.2.7", - "scipy", - "tabulate", - "voluptuous", - "xlrd", - "yamlize==0.7.1", - ], - extras_require={ - "mpi": ["mpi4py"], - "grids": ["wxpython==4.2.1"], - "memprof": ["psutil"], - "dev": [ - "black==22.6.0", - "docutils", - "ipykernel", - "jupyter-contrib-nbextensions", - "jupyter_client", - "nbsphinx", - "nbsphinx-link", - "pytest", - "pytest-cov", - "pytest-html", - "pytest-xdist", - "ruff==0.0.272", - "sphinx", - "sphinx-gallery", - "sphinx-rtd-theme", - "sphinxcontrib-apidoc", # for running Jupyter in docs - "sphinxext-opengraph", - ], - }, - tests_require=["nbconvert", "jupyter_client", "ipykernel"], - classifiers=[ - "Development Status :: 4 - Beta", - "Intended Audience :: Science/Research", - "Topic :: Scientific/Engineering :: Information Analysis", - "License :: OSI Approved :: Apache Software License", - ], -) diff --git a/tox.ini b/tox.ini index 8795e5a92..9c330e78e 100644 --- a/tox.ini +++ b/tox.ini @@ -5,26 +5,22 @@ requires = [testenv] basepython = {env:PYTHON3_PATH:python3} -deps= - pip >= 20.2 - -r{toxinidir}/requirements.txt - -r{toxinidir}/requirements-testing.txt setenv = PYTHONPATH = {toxinidir} USERNAME = armi [testenv:test] commands = + pip install -e .[memprof,mpi,test] pytest -n 4 armi [testenv:doc] allowlist_externals = /usr/bin/git /usr/bin/make -deps= - -r{toxinidir}/doc/requirements-docs.txt changedir = doc commands = + pip install -e ..[memprof,mpi,test,docs] git submodule init git submodule update make html @@ -32,25 +28,21 @@ commands = # First, run code coverage over the unit tests that run MPI library code. [testenv:cov1] deps= - pip>=20.2 mpi4py - -r{toxinidir}/requirements.txt - -r{toxinidir}/requirements-testing.txt allowlist_externals = /usr/bin/mpiexec commands = + pip install -e .[memprof,mpi,test] mpiexec -n 2 --use-hwthread-cpus coverage run --rcfile=.coveragerc -m pytest --cov=armi --cov-config=.coveragerc --cov-report=lcov --ignore=venv --cov-fail-under=80 armi/tests/test_mpiFeatures.py # Second, run code coverage over the rest of the unit tests, and combine the coverage results together [testenv:cov2] deps= - pip>=20.2 mpi4py - -r{toxinidir}/requirements.txt - -r{toxinidir}/requirements-testing.txt allowlist_externals = /usr/bin/mpiexec commands = + pip install -e .[memprof,mpi,test] coverage run --rcfile=.coveragerc -m pytest -n 4 --cov=armi --cov-config=.coveragerc --cov-report=lcov --cov-append --ignore=venv armi coverage combine --rcfile=.coveragerc --keep -a @@ -58,13 +50,11 @@ commands = # NOTE: This will only work in POSIX/BASH Linux. [testenv:mpitest] deps= - pip >= 20.2 mpi4py allowlist_externals = /usr/bin/mpiexec commands = - pip install -r requirements.txt - pip install -r requirements-testing.txt + pip install -e .[memprof,mpi,test] mpiexec -n 2 --use-hwthread-cpus pytest armi/tests/test_mpiFeatures.py [testenv:lint] @@ -76,10 +66,7 @@ commands = [testenv:report] skip_install = true deps= - pip>=20.2 mpi4py - -r{toxinidir}/requirements.txt - -r{toxinidir}/requirements-testing.txt commands = coverage report coverage html From 180d16a5fe27693b0989169ad676582e8004374e Mon Sep 17 00:00:00 2001 From: Arrielle Opotowsky Date: Wed, 27 Sep 2023 10:30:08 -0500 Subject: [PATCH 009/176] Cleanup allowing xml file as a settings file (#1417) --- armi/cases/suite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armi/cases/suite.py b/armi/cases/suite.py index f105649c6..3ed419ec0 100644 --- a/armi/cases/suite.py +++ b/armi/cases/suite.py @@ -103,7 +103,7 @@ def discover( """ csFiles = settings.recursivelyLoadSettingsFiles( rootDir or os.path.abspath(os.getcwd()), - patterns or ["*.yaml", "*.xml"], # xml temporary to transistion + patterns or ["*.yaml"], recursive=recursive, ignorePatterns=ignorePatterns, handleInvalids=False, From 55e809761de41b7183a84d98e1aa52a2b55fb7a7 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Wed, 27 Sep 2023 10:13:36 -0700 Subject: [PATCH 010/176] Removed globacl CS, getMasterCs, and setMasterCs (#1399) --- armi/__init__.py | 2 -- armi/bookkeeping/db/__init__.py | 3 -- armi/bookkeeping/db/database3.py | 7 +---- .../db/tests/test_databaseInterface.py | 3 -- armi/bookkeeping/tests/test_historyTracker.py | 4 +-- armi/cases/suite.py | 1 - armi/cases/tests/test_cases.py | 4 --- armi/cli/entryPoint.py | 1 - armi/conftest.py | 3 +- armi/interfaces.py | 10 +++--- armi/mpiActions.py | 4 +-- armi/operators/operator.py | 22 +------------ armi/operators/operatorMPI.py | 3 +- armi/operators/tests/test_operators.py | 10 +----- .../tests/test_globalFluxInterface.py | 11 ++++--- .../tests/test_crossSectionManager.py | 5 +-- armi/reactor/tests/test_assemblies.py | 6 ++-- armi/reactor/tests/test_blocks.py | 1 - armi/reactor/tests/test_reactors.py | 10 ------ armi/settings/__init__.py | 31 ------------------- armi/settings/caseSettings.py | 6 ---- armi/tests/test_mpiFeatures.py | 1 - .../run_programmaticReactorDefinition.py | 2 -- doc/release/0.2.rst | 1 + 24 files changed, 26 insertions(+), 125 deletions(-) diff --git a/armi/__init__.py b/armi/__init__.py index dff87ddd3..abb40fff0 100644 --- a/armi/__init__.py +++ b/armi/__init__.py @@ -142,8 +142,6 @@ def init(choice=None, fName=None, cs=None): if fName is None: fName = settings.promptForSettingsFile(choice) cs = settings.Settings(fName) - # clear out any old masterCs objects - settings.setMasterCs(cs) armiCase = cases.Case(cs=cs) armiCase.checkInputs() diff --git a/armi/bookkeeping/db/__init__.py b/armi/bookkeeping/db/__init__.py index bc4e82894..336d4c85d 100644 --- a/armi/bookkeeping/db/__init__.py +++ b/armi/bookkeeping/db/__init__.py @@ -62,7 +62,6 @@ import os from typing import Optional, List, Tuple -from armi import settings from armi import runLog # re-export package components for easier import @@ -142,8 +141,6 @@ def loadOperator(pathToDb, loadCycle, loadNode, allowMissing=False): r = db.load(loadCycle, loadNode, allowMissing=allowMissing) - settings.setMasterCs(cs) - o = thisCase.initializeOperator(r=r) runLog.important( "The operator will not be in the same state that it was at that cycle and " diff --git a/armi/bookkeeping/db/database3.py b/armi/bookkeeping/db/database3.py index 5f8cc3bfc..e3f7ccb44 100644 --- a/armi/bookkeeping/db/database3.py +++ b/armi/bookkeeping/db/database3.py @@ -680,10 +680,7 @@ def load( updateGlobalAssemNum : bool, optional DeprecationWarning: This is unused. updateMasterCs : bool, optional - Whether to apply the cs (whether provided as an argument or read from - the database) as the primary for the case. Default True. Can be useful - if you don't intend to use the loaded reactor as the basis for further - computations in the current operator. + TODO: Deprecated. Slated for removal. Returns ------- @@ -693,8 +690,6 @@ def load( runLog.info("Loading reactor state for time node ({}, {})".format(cycle, node)) cs = cs or self.loadCS() - if updateMasterCs: - settings.setMasterCs(cs) bp = bp or self.loadBlueprints() if node < 0: diff --git a/armi/bookkeeping/db/tests/test_databaseInterface.py b/armi/bookkeeping/db/tests/test_databaseInterface.py index 2b653b1a3..860d32362 100644 --- a/armi/bookkeeping/db/tests/test_databaseInterface.py +++ b/armi/bookkeeping/db/tests/test_databaseInterface.py @@ -53,7 +53,6 @@ def getSimpleDBOperator(cs): newSettings["nCycles"] = 1 cs = cs.modified(newSettings=newSettings) genDBCase = case.Case(cs) - settings.setMasterCs(cs) runLog.setVerbosity("info") o = genDBCase.initializeOperator() @@ -264,8 +263,6 @@ def setUpClass(cls): o, r = loadTestReactor(customSettings=newSettings) reduceTestReactorRings(r, o.cs, 3) - settings.setMasterCs(o.cs) - o.interfaces = [i for i in o.interfaces if isinstance(i, (DatabaseInterface))] dbi = o.getInterface("database") dbi.enabled(True) diff --git a/armi/bookkeeping/tests/test_historyTracker.py b/armi/bookkeeping/tests/test_historyTracker.py index 179d5df1a..8a2bbb0a2 100644 --- a/armi/bookkeeping/tests/test_historyTracker.py +++ b/armi/bookkeeping/tests/test_historyTracker.py @@ -84,7 +84,6 @@ def setUp(self): c = case.Case(cs) case2 = c.clone(title="armiRun") - settings.setMasterCs(case2.cs) self.o = case2.initializeOperator() self.r = self.o.r @@ -187,7 +186,8 @@ class TestHistoryTrackerNoModel(unittest.TestCase): """History tracker tests that do not require a Reactor Model.""" def setUp(self): - self.history = historyTracker.HistoryTrackerInterface(None, None) + cs = settings.Settings() + self.history = historyTracker.HistoryTrackerInterface(None, cs=cs) self._origCaseTitle = ( self.history.cs.caseTitle ) # to avoid parallel test interference. diff --git a/armi/cases/suite.py b/armi/cases/suite.py index 3ed419ec0..9320cd359 100644 --- a/armi/cases/suite.py +++ b/armi/cases/suite.py @@ -204,7 +204,6 @@ def run(self): for ci, case in enumerate(self): runLog.important(f"Running case {ci+1}/{len(self)}: {case}") with directoryChangers.DirectoryChanger(case.directory): - settings.setMasterCs(case.cs) try: case.run() except: # noqa: bare-except diff --git a/armi/cases/tests/test_cases.py b/armi/cases/tests/test_cases.py index 49644fbae..94356c212 100644 --- a/armi/cases/tests/test_cases.py +++ b/armi/cases/tests/test_cases.py @@ -30,7 +30,6 @@ from armi.physics.fuelCycle.settings import CONF_SHUFFLE_LOGIC from armi.reactor import blueprints from armi.reactor import systemLayoutInput -from armi.settings import setMasterCs from armi.tests import ARMI_RUN_PATH from armi.tests import mockRunLogs from armi.tests import TEST_ROOT @@ -211,7 +210,6 @@ def test_run(self): "verbosity": "important", } cs = cs.modified(newSettings=newSettings) - setMasterCs(cs) case = cases.Case(cs) with mockRunLogs.BufferLog() as mock: @@ -231,7 +229,6 @@ def test_clone(self): # test the short write style with directoryChangers.TemporaryDirectoryChanger(): cs = settings.Settings(ARMI_RUN_PATH) - setMasterCs(cs) case = cases.Case(cs) shortCase = case.clone( additionalFiles=["ISOAA"], @@ -254,7 +251,6 @@ def test_clone(self): # test the medium write style with directoryChangers.TemporaryDirectoryChanger(): cs = settings.Settings(ARMI_RUN_PATH) - setMasterCs(cs) case = cases.Case(cs) case.clone(writeStyle="medium") clonedYaml = "armiRun.yaml" diff --git a/armi/cli/entryPoint.py b/armi/cli/entryPoint.py index 052cc8b64..2a9c2fc82 100644 --- a/armi/cli/entryPoint.py +++ b/armi/cli/entryPoint.py @@ -94,7 +94,6 @@ def __init__(self): ) self.cs = self._initSettings() - settings.setMasterCs(self.cs) self.parser = argparse.ArgumentParser( prog="{} {}".format(context.APP_NAME, self.name), diff --git a/armi/conftest.py b/armi/conftest.py index 7eb1a52e8..c26d1513e 100644 --- a/armi/conftest.py +++ b/armi/conftest.py @@ -28,7 +28,7 @@ import matplotlib -from armi import apps, configure, context, settings +from armi import apps, configure, context from armi.settings import caseSettings from armi.tests import TEST_ROOT @@ -51,7 +51,6 @@ def bootstrapArmiTestEnv(): cs = caseSettings.Settings() context.Mode.setMode(context.Mode.BATCH) - settings.setMasterCs(cs) # Need to init burnChain. # see armi.cases.case.Case._initBurnChain with open(cs["burnChainFileName"]) as burnChainStream: diff --git a/armi/interfaces.py b/armi/interfaces.py index 5d48075e1..8e96b3ffb 100644 --- a/armi/interfaces.py +++ b/armi/interfaces.py @@ -274,7 +274,7 @@ class Distribute: SKIP = 4 def __init__(self, r, cs): - r""" + """ Construct an interface. The ``r`` and ``cs`` arguments are required, but may be ``None``, where @@ -300,7 +300,7 @@ def __init__(self, r, cs): self._enabled = True self.reverseAtEOL = False self._bolForce = False # override disabled flag in interactBOL if true. - self.cs = settings.getMasterCs() if cs is None else cs + self.cs = cs self.r = r self.o = r.o if r else None self.coupler = _setTightCouplerByInterfaceFunction(self, cs) @@ -623,7 +623,7 @@ def __init__(self, r=None, externalCodeInterface=None, cs=None): self.externalCodeInterface = externalCodeInterface self.eci = externalCodeInterface self.r = r - self.cs = cs or settings.getMasterCs() + self.cs = cs def getInterface(self, name): """Get another interface by name.""" @@ -652,11 +652,11 @@ class OutputReader: and would rather just have an apply(reactor) method. """ - def __init__(self, r=None, externalCodeInterface=None, fName=None): + def __init__(self, r=None, externalCodeInterface=None, fName=None, cs=None): self.externalCodeInterface = externalCodeInterface self.eci = self.externalCodeInterface self.r = r - self.cs = settings.getMasterCs() + self.cs = cs if fName: self.output = textProcessors.TextProcessor(fName) else: diff --git a/armi/mpiActions.py b/armi/mpiActions.py index 383adf9aa..ce9b32237 100644 --- a/armi/mpiActions.py +++ b/armi/mpiActions.py @@ -52,12 +52,11 @@ In order to create a new, custom MPI Action, inherit from :py:class:`~armi.mpiActions.MpiAction`, and override the :py:meth:`~armi.mpiActions.MpiAction.invokeHook` method. - """ import collections import gc -import timeit import math +import timeit from six.moves import cPickle import tabulate @@ -583,7 +582,6 @@ def _distributeSettings(self): raise RuntimeError("Failed to transmit settings, received: {}".format(cs)) if context.MPI_RANK != 0: - settings.setMasterCs(cs) self.o.cs = cs return cs diff --git a/armi/operators/operator.py b/armi/operators/operator.py index 6196c2155..b9381b198 100644 --- a/armi/operators/operator.py +++ b/armi/operators/operator.py @@ -26,16 +26,15 @@ :id: IMPL_EVOLVING_STATE_0 :links: REQ_EVOLVING_STATE """ +import collections import os import re import shutil import time -import collections from armi import context from armi import interfaces from armi import runLog -from armi import settings from armi.bookkeeping import memoryProfiler from armi.bookkeeping.report import reportingUtils from armi.operators import settingsValidation @@ -484,8 +483,6 @@ def _interactAll(self, interactionName, activeInterfaces, *args): ) ) - self._checkCsConsistency() - runLog.header( "=========== Completed {} Event ===========\n".format( interactionName + cycleNodeTag @@ -564,23 +561,6 @@ def _debugDB(self, interactionName, interfaceName, statePointIndex=0): db.writeToDB(self.r, statePointName=statePointName) - def _checkCsConsistency(self): - """Debugging check to verify that CS objects are not unexpectedly multiplying.""" - cs = settings.getMasterCs() - wrong = (self.cs is not cs) or any((i.cs is not cs) for i in self.interfaces) - if wrong: - msg = ["Primary cs ID is {}".format(id(cs))] - for i in self.interfaces: - msg.append("{:30s} has cs ID: {:12d}".format(str(i), id(i.cs))) - msg.append("{:30s} has cs ID: {:12d}".format(str(self), id(self.cs))) - raise RuntimeError("\n".join(msg)) - - runLog.debug( - "Reactors, operators, and interfaces all share primary cs: {}".format( - id(cs) - ) - ) - def interactAllInit(self): """Call interactInit on all interfaces in the stack after they are initialized.""" allInterfaces = self.interfaces[:] # copy just in case diff --git a/armi/operators/operatorMPI.py b/armi/operators/operatorMPI.py index 17a7418ee..6cf7d64a1 100644 --- a/armi/operators/operatorMPI.py +++ b/armi/operators/operatorMPI.py @@ -41,7 +41,6 @@ from armi import getPluginManager from armi import mpiActions from armi import runLog -from armi import settings from armi.operators.operator import Operator from armi.reactor import reactors @@ -213,7 +212,7 @@ def _resetWorker(self): xsGroups = self.getInterface("xsGroups") if xsGroups: xsGroups.clearRepresentativeBlocks() - cs = settings.getMasterCs() + cs = self.cs bp = self.r.blueprints spatialGrid = self.r.core.spatialGrid self.detach() diff --git a/armi/operators/tests/test_operators.py b/armi/operators/tests/test_operators.py index 183b54f7d..e75d1b734 100644 --- a/armi/operators/tests/test_operators.py +++ b/armi/operators/tests/test_operators.py @@ -92,13 +92,6 @@ def test_addInterfaceSubclassCollision(self): self.assertEqual(self.o.getInterface("Second"), interfaceB) self.assertEqual(self.o.getInterface("Third"), interfaceC) - def test_checkCsConsistency(self): - self.o._checkCsConsistency() # passes without error - - self.o.cs = self.o.cs.modified(newSettings={"nCycles": 66}) - with self.assertRaises(RuntimeError): - self.o._checkCsConsistency() - def test_interfaceIsActive(self): self.o, _r = test_reactors.loadTestReactor() self.assertTrue(self.o.interfaceIsActive("main")) @@ -111,7 +104,6 @@ def test_loadStateError(self): self.o.loadState(0, 1) def test_setStateToDefault(self): - # reset the runType for testing self.assertEqual(self.o.cs[CONF_RUN_TYPE], "Standard") self.o.cs = self.o.cs.modified(newSettings={"runType": "fake"}) @@ -166,7 +158,7 @@ def test_snapshotRequest(self, fakeDirList, fakeCopy): class TestTightCoupling(unittest.TestCase): def setUp(self): - self.cs = settings.getMasterCs() + self.cs = settings.Settings() self.cs[CONF_TIGHT_COUPLING] = True self.o = Operator(self.cs) self.o.r = Reactor("empty", None) diff --git a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py index 9bbae2d5a..ff8957660 100644 --- a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py @@ -262,7 +262,7 @@ def test_mapper(self): o, r = test_reactors.loadTestReactor(customSettings={CONF_XS_KERNEL: "MC2v2"}) applyDummyFlux(r) r.core.lib = isotxs.readBinary(ISOAA_PATH) - mapper = globalFluxInterface.GlobalFluxResultMapper() + mapper = globalFluxInterface.GlobalFluxResultMapper(cs=o.cs) mapper.r = r mapper._renormalizeNeutronFluxByBlock(100) self.assertAlmostEqual(r.core.calcTotalParam("power", generationNum=2), 100) @@ -295,7 +295,8 @@ def test_mapper(self): self.assertEqual(len(block.p.mgFlux), 0) def test_getDpaXs(self): - mapper = globalFluxInterface.GlobalFluxResultMapper() + cs = settings.Settings() + mapper = globalFluxInterface.GlobalFluxResultMapper(cs=cs) # test fuel block b = HexBlock("fuel", height=10.0) @@ -320,7 +321,8 @@ def test_getDpaXs(self): mapper.getDpaXs(b) def test_getBurnupPeakingFactor(self): - mapper = globalFluxInterface.GlobalFluxResultMapper() + cs = settings.Settings() + mapper = globalFluxInterface.GlobalFluxResultMapper(cs=cs) # test fuel block mapper.cs["burnupPeakingFactor"] = 0.0 @@ -331,7 +333,8 @@ def test_getBurnupPeakingFactor(self): self.assertEqual(factor, 2.5) def test_getBurnupPeakingFactorZero(self): - mapper = globalFluxInterface.GlobalFluxResultMapper() + cs = settings.Settings() + mapper = globalFluxInterface.GlobalFluxResultMapper(cs=cs) # test fuel block without any peaking factor set b = HexBlock("fuel", height=10.0) diff --git a/armi/physics/neutronics/tests/test_crossSectionManager.py b/armi/physics/neutronics/tests/test_crossSectionManager.py index af2843868..2282651c3 100644 --- a/armi/physics/neutronics/tests/test_crossSectionManager.py +++ b/armi/physics/neutronics/tests/test_crossSectionManager.py @@ -24,7 +24,7 @@ from six.moves import cPickle -from armi import settings # noqa: unused-import +from armi import settings from armi.physics.neutronics import crossSectionGroupManager from armi.physics.neutronics.const import CONF_CROSS_SECTION from armi.physics.neutronics.crossSectionGroupManager import ( @@ -473,8 +473,9 @@ def test_invalidWeights(self): class Test_CrossSectionGroupManager(unittest.TestCase): def setUp(self): + cs = settings.Settings() self.blockList = makeBlocks(20) - self.csm = CrossSectionGroupManager(self.blockList[0].r, None) + self.csm = CrossSectionGroupManager(self.blockList[0].r, cs) for bi, b in enumerate(self.blockList): b.p.percentBu = bi / 19.0 * 100 self.csm._setBuGroupBounds([3, 10, 30, 100]) diff --git a/armi/reactor/tests/test_assemblies.py b/armi/reactor/tests/test_assemblies.py index 9c897ee00..47ee795d5 100644 --- a/armi/reactor/tests/test_assemblies.py +++ b/armi/reactor/tests/test_assemblies.py @@ -13,10 +13,10 @@ # limitations under the License. """Tests assemblies.py.""" +import numpy as np import pathlib import random import unittest -import numpy as np from numpy.testing import assert_allclose from armi import settings @@ -58,8 +58,7 @@ def buildTestAssemblies(): * One with half UZr pins and half UTh pins * One with all UThZr pins """ - caseSetting = settings.Settings() - settings.setMasterCs(caseSetting) + settings.Settings() temperature = 273.0 fuelID = 0.0 @@ -1282,7 +1281,6 @@ def setUp(self): newSettings = {CONF_XS_KERNEL: "MC2v2"} # don't try to expand elementals self.cs = self.cs.modified(newSettings=newSettings) - settings.setMasterCs(self.cs) bp = blueprints.Blueprints() self.r = reactors.Reactor("test", bp) self.r.add(reactors.Core("Core")) diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py index 5a38a107e..381bce355 100644 --- a/armi/reactor/tests/test_blocks.py +++ b/armi/reactor/tests/test_blocks.py @@ -75,7 +75,6 @@ def buildSimpleFuelBlock(): def loadTestBlock(cold=True): """Build an annular test block for evaluating unit tests.""" caseSetting = settings.Settings() - settings.setMasterCs(caseSetting) caseSetting[CONF_XS_KERNEL] = "MC2v2" runLog.setVerbosity("error") caseSetting["nCycles"] = 1 diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 43056fbd7..f1b4b1f2b 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -54,16 +54,12 @@ def buildOperatorOfEmptyHexBlocks(customSettings=None): customSettings : dict Dictionary of off-default settings to update """ - settings.setMasterCs(None) # clear cs = settings.Settings() # fetch new - settings.setMasterCs(cs) # reset - if customSettings is None: customSettings = {} customSettings["db"] = False # stop use of database cs = cs.modified(newSettings=customSettings) - settings.setMasterCs(cs) # reset so everything matches the primary Cs r = tests.getEmptyHexReactor() r.core.setOptionsFromCs(cs) @@ -95,16 +91,12 @@ def buildOperatorOfEmptyCartesianBlocks(customSettings=None): customSettings : dict Off-default settings to update """ - settings.setMasterCs(None) # clear cs = settings.Settings() # fetch new - settings.setMasterCs(cs) # reset - if customSettings is None: customSettings = {} customSettings["db"] = False # stop use of database cs = cs.modified(newSettings=customSettings) - settings.setMasterCs(cs) # reset r = tests.getEmptyCartesianReactor() r.core.setOptionsFromCs(cs) @@ -176,7 +168,6 @@ def loadTestReactor( if isPickeledReactor and TEST_REACTOR: # return test reactor only if no custom settings are needed. o, r, assemNum = cPickle.loads(TEST_REACTOR) - settings.setMasterCs(o.cs) o.reattach(r, o.cs) return o, r @@ -191,7 +182,6 @@ def loadTestReactor( newSettings = {} cs = cs.modified(newSettings=newSettings) - settings.setMasterCs(cs) o = operators.factory(cs) r = reactors.loadFromCs(cs) diff --git a/armi/settings/__init__.py b/armi/settings/__init__.py index 65d109fd8..1a4566280 100644 --- a/armi/settings/__init__.py +++ b/armi/settings/__init__.py @@ -151,34 +151,3 @@ def promptForSettingsFile(choice=None): choice = int(input("Enter choice: ")) return files[choice] - - -def getMasterCs(): - """ - Return the global case-settings object (cs). - - This can be called at any time to create or obtain the primary Cs, a module-level CS - intended to be shared by many other objects. - - It can have multiple instances in multiprocessing cases. - - Returns - ------- - cs : Settings - The loaded cs object - """ - cs = Settings.instance - if cs is None: - cs = Settings() - setMasterCs(cs) - return cs - - -def setMasterCs(cs): - """ - Set the primary Cs to be the one that is passed in. - - These are kept track of independently on a PID basis to allow independent multiprocessing. - """ - Settings.instance = cs - runLog.debug("Primary cs set to {} with ID: {}".format(cs, id(cs))) diff --git a/armi/settings/caseSettings.py b/armi/settings/caseSettings.py index d94f634a1..1ce2dd426 100644 --- a/armi/settings/caseSettings.py +++ b/armi/settings/caseSettings.py @@ -64,10 +64,6 @@ class Settings: The actual settings in any instance of this class are immutable. """ - # Settings is not a singleton, but there is a globally - # shared instance considered most germane to the current run - instance = None - defaultCaseTitle = "armi" def __init__(self, fName=None): @@ -98,8 +94,6 @@ def __init__(self, fName=None): app = getApp() assert app is not None self.__settings = app.getSettings() - if not Settings.instance: - Settings.instance = self if fName: self.loadFromInputFile(fName) diff --git a/armi/tests/test_mpiFeatures.py b/armi/tests/test_mpiFeatures.py index 03893096b..4ee46b494 100644 --- a/armi/tests/test_mpiFeatures.py +++ b/armi/tests/test_mpiFeatures.py @@ -159,7 +159,6 @@ def setUp(self): self.cs = settings.Settings(fName=ARMI_RUN_PATH) bp = blueprints.loadFromCs(self.cs) - settings.setMasterCs(self.cs) self.o = OperatorMPI(self.cs) self.o.r = reactors.factory(self.cs, bp) self.action = DistributeStateAction() diff --git a/doc/gallery-src/framework/run_programmaticReactorDefinition.py b/doc/gallery-src/framework/run_programmaticReactorDefinition.py index e6fc92d28..e270afeb7 100644 --- a/doc/gallery-src/framework/run_programmaticReactorDefinition.py +++ b/doc/gallery-src/framework/run_programmaticReactorDefinition.py @@ -36,7 +36,6 @@ configure(permissive=True) from armi import cases -from armi import settings from armi.reactor import blueprints from armi.reactor.blueprints import assemblyBlueprint from armi.reactor.blueprints import blockBlueprint @@ -61,7 +60,6 @@ def buildCase(): bp.systemDesigns = buildSystems() cs = caseSettings.Settings() - settings.setMasterCs(cs) # remove once we eliminate masterCs cs.path = None cs.caseTitle = "scripted-case" case = cases.Case(cs=cs, bp=bp) diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst index 2a41d5945..3b594476f 100644 --- a/doc/release/0.2.rst +++ b/doc/release/0.2.rst @@ -9,6 +9,7 @@ Release Date: TBD What's new in ARMI ------------------ #. The Spent Fuel Pool (``sfp``) was moved from the ``Core`` out to the ``Reactor``. (`PR#1336 `_) +#. Removed ``getMasterCs()`` and ``setMasterCs()``. (`PR#1399 `_) #. Broad cleanup of ``Parameters``: filled in all empty units and descriptions, removed unused params. (`PR#1345 `_) #. Removed redundant ``Material.name`` variable. (`PR#1335 `_) #. Moved the ``Reactor`` assembly number from the global scope to a ``Parameter``. (`PR#1383 `_) From 232699045693316538d53fbf395a54bd47c8ca5d Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 28 Sep 2023 13:40:13 -0700 Subject: [PATCH 011/176] Version bump to v0.2.9 (#1418) --- doc/developer/tooling.rst | 2 +- doc/release/0.2.rst | 41 +++++++++++++++++++++++++-------------- pyproject.toml | 2 +- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/doc/developer/tooling.rst b/doc/developer/tooling.rst index 478d033c0..37b366771 100644 --- a/doc/developer/tooling.rst +++ b/doc/developer/tooling.rst @@ -128,7 +128,7 @@ repos on GitHub. Every release should follow this process: 1. Ensure all unit tests pass and the documentation is building correctly. -2. Bump the ``__version__`` string in ``armi/meta.py``. +2. Bump the ``version`` string in ``pyproject.toml``. 3. Add release notes to the documentation: `here `__. 4. Tag the commit after it goes into the repo: diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst index 3b594476f..5316f6691 100644 --- a/doc/release/0.2.rst +++ b/doc/release/0.2.rst @@ -2,33 +2,44 @@ ARMI v0.2 Release Notes ======================= +ARMI v0.2.10 +============ +Release Date: TBD + +What's new in ARMI +------------------ +#. TBD + +Bug fixes +--------- +#. TBD + ARMI v0.2.9 =========== -Release Date: TBD +Release Date: 2023-09-27 What's new in ARMI ------------------ -#. The Spent Fuel Pool (``sfp``) was moved from the ``Core`` out to the ``Reactor``. (`PR#1336 `_) -#. Removed ``getMasterCs()`` and ``setMasterCs()``. (`PR#1399 `_) +#. Moved the ``Reactor`` assembly number from the global scope to a ``Parameter``. (`PR#1383 `_) +#. Removed the global ``CaseSettings`` object, ``getMasterCs()``, and ``setMasterCs()``. (`PR#1399 `_) +#. Moved the Spent Fuel Pool (``sfp``) from the ``Core`` to the ``Reactor``. (`PR#1336 `_) +#. Made the ``sfp`` a child of the ``Reactor`` so it is stored in the database. (`PR#1349 `_) #. Broad cleanup of ``Parameters``: filled in all empty units and descriptions, removed unused params. (`PR#1345 `_) +#. Updated some parameter definitions and defaults. (`PR#1355 `_) #. Removed redundant ``Material.name`` variable. (`PR#1335 `_) -#. Moved the ``Reactor`` assembly number from the global scope to a ``Parameter``. (`PR#1383 `_) -#. Added ``powerDensity`` as a high-level alternative to ``power`` to configure a Reactor. (`PR#1395 `_) +#. Added ``powerDensity`` as a high-level alternative to ``power`` to configure a ``Reactor``. (`PR#1395 `_) #. Added SHA1 hashes of XS control files to the welcome text. (`PR#1334 `_) + +Build changes +------------- #. Moved from ``setup.py`` to ``pyproject.toml``. (`PR#1409 `_) -#. Add python 3.11 to ARMI's CI testing GH actions! (`PR#1341 `_) -#. Put back ``avgFuelTemp`` block parameter. (`PR#1362 `_) -#. Make cylindrical component block collection less strict about pre-homogenization checks. (`PR#1347 `_) -#. Updated some parameter definitions and defaults. (`PR#1355 `_) -#. Make the SFP a child of the reactor so it is stored in database. (`PR#1349 `_) -#. Update black to version 22.6 (`PR#1396 `_) -#. TBD +#. Added Python 3.11 to ARMI's CI on GH actions. (`PR#1341 `_) +#. Updated ``black`` to version 22.6. (`PR#1396 `_) Bug fixes --------- -#. Fix _processIncludes to handle StringIO input (`PR#1333 `_) -#. Logic improvements for computing thermal expansion factors for axial expansion. (`PR#1342 `_) -#. TBD +#. Fixed ``_processIncludes()`` to handle ``StringIO`` input. (`PR#1333 `_) +#. Fixed logic for computing thermal expansion factors for axial expansion. (`PR#1342 `_) ARMI v0.2.8 =========== diff --git a/pyproject.toml b/pyproject.toml index b1266ce4d..80b548419 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ build-backend = "setuptools.build_meta" [project] name = "armi" -version = "0.2.8" +version = "0.2.9" description = "An open-source nuclear reactor analysis automation framework that helps design teams increase efficiency and quality." license = {file = "LICENSE.md"} requires-python = ">3.6" From fd67d13951054b698790442e0a89224c2a921a17 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 28 Sep 2023 15:19:01 -0700 Subject: [PATCH 012/176] Enforcing discrete options for neutronics BCs (#1410) --- armi/physics/neutronics/settings.py | 1 + armi/physics/neutronics/tests/test_neutronicsPlugin.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/armi/physics/neutronics/settings.py b/armi/physics/neutronics/settings.py index b181234ef..45841188c 100644 --- a/armi/physics/neutronics/settings.py +++ b/armi/physics/neutronics/settings.py @@ -155,6 +155,7 @@ def defineSettings(): "ZeroInwardCurrent", "Generalized", ], + enforcedOptions=True, ), setting.Setting( CONF_NEUTRONICS_KERNEL, diff --git a/armi/physics/neutronics/tests/test_neutronicsPlugin.py b/armi/physics/neutronics/tests/test_neutronicsPlugin.py index 8b79919bf..dbeac4fcc 100644 --- a/armi/physics/neutronics/tests/test_neutronicsPlugin.py +++ b/armi/physics/neutronics/tests/test_neutronicsPlugin.py @@ -264,8 +264,8 @@ def test_neutronicsSettingsValidators(self): sv = getNeutronicsSettingValidators(inspector) self.assertEqual(len(sv), 9) - # Test the Query: boundaries are now "Extrapolated", not "Normal" - cs = cs.modified(newSettings={CONF_BOUNDARIES: "Normal"}) + # Test the Query: boundaries are now "Extrapolated", not "Generalized" + cs = cs.modified(newSettings={CONF_BOUNDARIES: "Generalized"}) inspector = settingsValidation.Inspector(cs) sv = getNeutronicsSettingValidators(inspector) From 8041ad889a0203da4602f0be321b54ebef5995b6 Mon Sep 17 00:00:00 2001 From: Arrielle Opotowsky Date: Fri, 29 Sep 2023 11:05:48 -0500 Subject: [PATCH 013/176] Changed _copyInputsHelper() to return relative paths (#1416) --- armi/cases/case.py | 15 +++++++---- armi/cases/tests/test_cases.py | 37 +++++++++++++++++++--------- armi/operators/settingsValidation.py | 4 +-- doc/release/0.2.rst | 1 + 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/armi/cases/case.py b/armi/cases/case.py index 7ad725320..e42879e6c 100644 --- a/armi/cases/case.py +++ b/armi/cases/case.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -r""" +""" The ``Case`` object is responsible for running, and executing a set of user inputs. Many entry points redirect into ``Case`` methods, such as ``clone``, ``compare``, and ``run``. @@ -872,8 +872,11 @@ def _copyInputsHelper( try: pathTools.copyOrWarn(fileDescription, sourcePath, destFilePath) if pathlib.Path(destFilePath).exists(): - return destFilePath + # the basename gets written back to the settings file to protect against + # potential future dir structure changes + return os.path.basename(destFilePath) else: + # keep original filepath in the settings file if file copy was unsuccessful return origFile except Exception: return origFile @@ -961,6 +964,7 @@ def copyInterfaceInputs( continue except OSError: pass + # Attempt to construct an absolute file path sourceFullPath = os.path.join(sourceDirPath, f) if WILDCARD: @@ -982,11 +986,11 @@ def copyInterfaceInputs( label, sourceFullPath, destination, f ) newFiles.append(str(destFilePath)) + if destFilePath == f: - runLog.info( + runLog.debug( f"No input files for `{label}` setting could be resolved with " - f"the following path: `{sourceFullPath}`. Will not update " - f"`{label}`." + f"the following path: `{sourceFullPath}`. Will not update `{label}`." ) # Some settings are a single filename. Others are lists of files. Make @@ -995,4 +999,5 @@ def copyInterfaceInputs( newSettings[label] = newFiles[0] else: newSettings[label] = newFiles + return newSettings diff --git a/armi/cases/tests/test_cases.py b/armi/cases/tests/test_cases.py index 94356c212..f030c5077 100644 --- a/armi/cases/tests/test_cases.py +++ b/armi/cases/tests/test_cases.py @@ -505,8 +505,9 @@ def test_copyInputsHelper(self): destPath=newDir.destination, origFile=shuffleFile, ) - newFilepath = os.path.join(newDir.destination, shuffleFile) - self.assertEqual(destFilePath, str(newFilepath)) + newFilePath = os.path.join(newDir.destination, shuffleFile) + self.assertTrue(os.path.exists(newFilePath)) + self.assertEqual(destFilePath, os.path.basename(newFilePath)) # test with bad file path, should return original file # ensure we are not in TEST_ROOT @@ -514,9 +515,10 @@ def test_copyInputsHelper(self): destFilePath = cases.case._copyInputsHelper( testSetting, sourcePath=sourceFullPath, - destPath="", + destPath="fakeDest", origFile=shuffleFile, ) + self.assertFalse(os.path.exists(destFilePath)) self.assertEqual(destFilePath, shuffleFile) def test_copyInterfaceInputs_singleFile(self): @@ -529,8 +531,9 @@ def test_copyInterfaceInputs_singleFile(self): newSettings = cases.case.copyInterfaceInputs( cs, destination=newDir.destination ) - newFilepath = os.path.join(newDir.destination, shuffleFile) - self.assertEqual(newSettings[testSetting], str(newFilepath)) + newFilePath = os.path.join(newDir.destination, shuffleFile) + self.assertTrue(os.path.exists(newFilePath)) + self.assertEqual(newSettings[testSetting], os.path.basename(newFilePath)) def test_copyInterfaceInputs_nonFilePath(self): testSetting = CONF_SHUFFLE_LOGIC @@ -543,6 +546,7 @@ def test_copyInterfaceInputs_nonFilePath(self): newSettings = cases.case.copyInterfaceInputs( cs, destination=newDir.destination ) + self.assertFalse(os.path.exists(newSettings[testSetting])) self.assertEqual(newSettings[testSetting], fakeShuffle) def test_copyInterfaceInputs_multipleFiles(self): @@ -569,8 +573,10 @@ def test_copyInterfaceInputs_multipleFiles(self): newSettings = cases.case.copyInterfaceInputs( cs, destination=newDir.destination ) - newFilepaths = [os.path.join(newDir.destination, f) for f in settingFiles] - self.assertEqual(newSettings[testSetting], newFilepaths) + newFilePaths = [os.path.join(newDir.destination, f) for f in settingFiles] + for newFilePath in newFilePaths: + self.assertTrue(os.path.exists(newFilePath)) + self.assertEqual(newSettings[testSetting], settingFiles) def test_copyInterfaceInputs_wildcardFile(self): testSetting = CONF_SHUFFLE_LOGIC @@ -584,8 +590,11 @@ def test_copyInterfaceInputs_wildcardFile(self): newSettings = cases.case.copyInterfaceInputs( cs, destination=newDir.destination ) - newFilepath = [os.path.join(newDir.destination, "ISOAA")] - self.assertEqual(newSettings[testSetting], newFilepath) + newFilePath = [os.path.join(newDir.destination, "ISOAA")] + self.assertTrue(os.path.exists(newFilePath[0])) + self.assertEqual( + newSettings[testSetting], [os.path.basename(newFilePath[0])] + ) # Check on a file that doesn't exist (so globFilePaths len is 0) wcFile = "fakeFile*" @@ -594,6 +603,7 @@ def test_copyInterfaceInputs_wildcardFile(self): newSettings = cases.case.copyInterfaceInputs( cs, destination=newDir.destination ) + self.assertFalse(os.path.exists(newSettings[testSetting][0])) self.assertEqual(newSettings[testSetting], [wcFile]) def test_copyInterfaceInputs_relPath(self): @@ -608,8 +618,9 @@ def test_copyInterfaceInputs_relPath(self): newSettings = cases.case.copyInterfaceInputs( cs, destination=newDir.destination ) - newFilepath = os.path.join(newDir.destination, shuffleFile) - self.assertEqual(newSettings[testSetting], newFilepath) + newFilePath = os.path.join(newDir.destination, shuffleFile) + self.assertTrue(os.path.exists(newFilePath)) + self.assertEqual(newSettings[testSetting], os.path.basename(newFilePath)) def test_copyInterfaceInputs_absPath(self): testSetting = CONF_SHUFFLE_LOGIC @@ -623,4 +634,8 @@ def test_copyInterfaceInputs_absPath(self): newSettings = cases.case.copyInterfaceInputs( cs, destination=newDir.destination ) + # file exists + self.assertTrue(os.path.exists(newSettings[testSetting])) + # but not copied to this dir + self.assertFalse(os.path.exists(os.path.basename(newSettings[testSetting]))) self.assertEqual(str(newSettings[testSetting]), absFile) diff --git a/armi/operators/settingsValidation.py b/armi/operators/settingsValidation.py index ebab2cbc8..e9b4ad37e 100644 --- a/armi/operators/settingsValidation.py +++ b/armi/operators/settingsValidation.py @@ -338,7 +338,7 @@ def _checkForBothSimpleAndDetailedCyclesInputs(self): against the default, if the user specifies all the simple cycle settings _exactly_ as the defaults, this won't be caught. But, it would be very coincidental for the user to _specify_ all the default values when - performing any real analysis, so whatever. + performing any real analysis. Also, we must bypass the `Settings` getter and reach directly into the underlying `__settings` dict to avoid triggering an error @@ -718,7 +718,7 @@ def decayCyclesHaveInputThatWillBeIgnored(): def createQueryRevertBadPathToDefault(inspector, settingName, initialLambda=None): - r""" + """ Return a query to revert a bad path to its default. Parameters diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst index 5316f6691..e1d0402ea 100644 --- a/doc/release/0.2.rst +++ b/doc/release/0.2.rst @@ -8,6 +8,7 @@ Release Date: TBD What's new in ARMI ------------------ +#. The ``_copyInputsHelper()`` gives relative path and not absolute after copy. (`PR#1416 `_) #. TBD Bug fixes From b76b75e9e69f4b6a850d510da1c13877609857d8 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Mon, 2 Oct 2023 08:12:51 -0700 Subject: [PATCH 014/176] Adding ruff check to the GH Actions (#1419) --- .github/workflows/linting.yaml | 22 ++++++++++++++++++++++ doc/release/0.2.rst | 1 + 2 files changed, 23 insertions(+) create mode 100644 .github/workflows/linting.yaml diff --git a/.github/workflows/linting.yaml b/.github/workflows/linting.yaml new file mode 100644 index 000000000..28c960695 --- /dev/null +++ b/.github/workflows/linting.yaml @@ -0,0 +1,22 @@ +name: Linting + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: '3.9' + - name: Update package index + run: sudo apt-get update + - name: Install Tox and any other packages + run: pip install tox + - name: Run Linter + continue-on-error: true + run: tox -e lint diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst index e1d0402ea..4f8bbd735 100644 --- a/doc/release/0.2.rst +++ b/doc/release/0.2.rst @@ -9,6 +9,7 @@ Release Date: TBD What's new in ARMI ------------------ #. The ``_copyInputsHelper()`` gives relative path and not absolute after copy. (`PR#1416 `_) +#. ARMI now mandates ``ruff`` linting. (`PR#1419 `_) #. TBD Bug fixes From d341941e570dc731a4b0ab9017acc65c35d43098 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Mon, 2 Oct 2023 09:04:10 -0700 Subject: [PATCH 015/176] Rename reqs using R_, T_, and I_ (#1420) --- armi/operators/operator.py | 4 +- armi/operators/tests/test_operators.py | 2 +- armi/reactor/blueprints/__init__.py | 4 +- .../blueprints/tests/test_blueprints.py | 8 +- armi/reactor/components/basicShapes.py | 4 +- armi/reactor/components/complexShapes.py | 4 +- armi/reactor/components/component.py | 4 +- armi/reactor/components/volumetricShapes.py | 4 +- armi/reactor/grids/cartesian.py | 4 +- armi/reactor/grids/hexagonal.py | 4 +- armi/reactor/grids/structuredgrid.py | 4 +- armi/reactor/grids/tests/test_grids.py | 12 +- armi/reactor/grids/thetarz.py | 4 +- armi/reactor/reactors.py | 4 +- armi/reactor/tests/test_components.py | 44 +++---- armi/reactor/tests/test_reactors.py | 4 +- armi/utils/tests/test_utils.py | 4 +- doc/requirements/srsd.rst | 124 +++++++++--------- 18 files changed, 121 insertions(+), 121 deletions(-) diff --git a/armi/operators/operator.py b/armi/operators/operator.py index b9381b198..f86a49e0f 100644 --- a/armi/operators/operator.py +++ b/armi/operators/operator.py @@ -23,8 +23,8 @@ the end of plant life. .. impl:: ARMI controls the time flow of the reactor, by running a sequence of Interfaces at each time step. - :id: IMPL_EVOLVING_STATE_0 - :links: REQ_EVOLVING_STATE + :id: I_EVOLVING_STATE_0 + :links: R_EVOLVING_STATE """ import collections import os diff --git a/armi/operators/tests/test_operators.py b/armi/operators/tests/test_operators.py index e75d1b734..55c795e8b 100644 --- a/armi/operators/tests/test_operators.py +++ b/armi/operators/tests/test_operators.py @@ -56,7 +56,7 @@ class InterfaceC(Interface): name = "Third" -# TODO: Add a test that shows time evolution of Reactor (REQ_EVOLVING_STATE) +# TODO: Add a test that shows time evolution of Reactor (R_EVOLVING_STATE) class OperatorTests(unittest.TestCase): def setUp(self): self.o, self.r = test_reactors.loadTestReactor() diff --git a/armi/reactor/blueprints/__init__.py b/armi/reactor/blueprints/__init__.py index 7f068d4e2..00930d788 100644 --- a/armi/reactor/blueprints/__init__.py +++ b/armi/reactor/blueprints/__init__.py @@ -175,8 +175,8 @@ class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector): Base Blueprintsobject representing all the subsections in the input file. .. impl:: ARMI represents a user-specified reactor by providing a "Blueprint" YAML interface. - :id: IMPL_REACTOR_0 - :links: REQ_REACTOR + :id: I_REACTOR_0 + :links: R_REACTOR """ nuclideFlags = yamlize.Attribute( diff --git a/armi/reactor/blueprints/tests/test_blueprints.py b/armi/reactor/blueprints/tests/test_blueprints.py index 81a54dfac..57ca1e943 100644 --- a/armi/reactor/blueprints/tests/test_blueprints.py +++ b/armi/reactor/blueprints/tests/test_blueprints.py @@ -68,8 +68,8 @@ def test_nuclides(self): """Tests the available sets of nuclides work as expected. .. test:: Tests that users can define their nuclides of interest. - :id: TEST_REACTOR_0 - :links: REQ_REACTOR + :id: T_REACTOR_0 + :links: R_REACTOR """ actives = set(self.blueprints.activeNuclides) inerts = set(self.blueprints.inertNuclides) @@ -95,8 +95,8 @@ def test_componentDimensions(self): """Tests that the user can specifiy the dimensions of a component with arbitray fidelity. .. test:: Tests that the user can specify the dimensions of a component with arbitrary fidelity. - :id: TEST_REACTOR_1 - :links: REQ_REACTOR + :id: T_REACTOR_1 + :links: R_REACTOR """ fuelAssem = self.blueprints.constructAssem(self.cs, name="igniter fuel") fuel = fuelAssem.getComponents(Flags.FUEL)[0] diff --git a/armi/reactor/components/basicShapes.py b/armi/reactor/components/basicShapes.py index e20c1bb38..8abc58f97 100644 --- a/armi/reactor/components/basicShapes.py +++ b/armi/reactor/components/basicShapes.py @@ -19,8 +19,8 @@ are defined in this subpackage. .. impl:: ARMI supports a reasonable set of basic shapes. - :id: IMPL_REACTOR_SHAPES_0 - :links: REQ_REACTOR_SHAPES + :id: I_REACTOR_SHAPES_0 + :links: R_REACTOR_SHAPES Here ARMI implements its support for: Circles, Hexagons, Rectangles, Solid Rectangles, Squares, and Triangles. diff --git a/armi/reactor/components/complexShapes.py b/armi/reactor/components/complexShapes.py index 628cf4295..468a16078 100644 --- a/armi/reactor/components/complexShapes.py +++ b/armi/reactor/components/complexShapes.py @@ -16,8 +16,8 @@ Components represented by complex shapes, and typically less widely used. .. impl:: ARMI supports a reasonable set of basic shapes. - :id: IMPL_REACTOR_SHAPES_1 - :links: REQ_REACTOR_SHAPES + :id: I_REACTOR_SHAPES_1 + :links: R_REACTOR_SHAPES Here ARMI implements its support for: Holed Hexagons, Holed Rectangles, Holed Squares, and Helixes. diff --git a/armi/reactor/components/component.py b/armi/reactor/components/component.py index 61dc3915d..d8f77953e 100644 --- a/armi/reactor/components/component.py +++ b/armi/reactor/components/component.py @@ -181,8 +181,8 @@ class Component(composites.Composite, metaclass=ComponentType): The material object that makes up this component and give it its thermo-mechanical properties. .. impl:: ARMI allows for thermal expansion of all components by user-defined custom curves. - :id: IMPL_REACTOR_THERMAL_EXPANSION_0 - :links: REQ_REACTOR_THERMAL_EXPANSION + :id: I_REACTOR_THERMAL_EXPANSION_0 + :links: R_REACTOR_THERMAL_EXPANSION """ DIMENSION_NAMES = tuple() # will be assigned by ComponentType diff --git a/armi/reactor/components/volumetricShapes.py b/armi/reactor/components/volumetricShapes.py index 4593b5830..d2aed5eaa 100644 --- a/armi/reactor/components/volumetricShapes.py +++ b/armi/reactor/components/volumetricShapes.py @@ -15,8 +15,8 @@ """3-dimensional shapes. .. impl:: ARMI supports a reasonable set of basic shapes. - :id: IMPL_REACTOR_SHAPES_2 - :links: REQ_REACTOR_SHAPES + :id: I_REACTOR_SHAPES_2 + :links: R_REACTOR_SHAPES Here ARMI implements its support for: Cubes, Spheres, RadialSegments, and more. """ diff --git a/armi/reactor/grids/cartesian.py b/armi/reactor/grids/cartesian.py index 14aa69e9e..e72a047a0 100644 --- a/armi/reactor/grids/cartesian.py +++ b/armi/reactor/grids/cartesian.py @@ -69,8 +69,8 @@ class CartesianGrid(StructuredGrid): location is offset from the origin. .. impl:: ARMI supports a Cartesian mesh. - :id: IMPL_REACTOR_MESH_1 - :links: REQ_REACTOR_MESH + :id: I_REACTOR_MESH_1 + :links: R_REACTOR_MESH """ @classmethod diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py index d11bdaf0a..d6bb29ac3 100644 --- a/armi/reactor/grids/hexagonal.py +++ b/armi/reactor/grids/hexagonal.py @@ -69,8 +69,8 @@ class HexGrid(StructuredGrid): (-1, 0) ( 0,-1) .. impl:: ARMI supports a Hexagonal mesh. - :id: IMPL_REACTOR_MESH_2 - :links: REQ_REACTOR_MESH + :id: I_REACTOR_MESH_2 + :links: R_REACTOR_MESH """ @staticmethod diff --git a/armi/reactor/grids/structuredgrid.py b/armi/reactor/grids/structuredgrid.py index 39267445f..50a4860bc 100644 --- a/armi/reactor/grids/structuredgrid.py +++ b/armi/reactor/grids/structuredgrid.py @@ -138,8 +138,8 @@ class StructuredGrid(Grid): indices. Thus we reduce the size of the unitSteps tuple accordingly. .. impl:: ARMI supports a number of structured mesh options. - :id: IMPL_REACTOR_MESH_0 - :links: REQ_REACTOR_MESH + :id: I_REACTOR_MESH_0 + :links: R_REACTOR_MESH """ def __init__( diff --git a/armi/reactor/grids/tests/test_grids.py b/armi/reactor/grids/tests/test_grids.py index 915b7a594..099338da6 100644 --- a/armi/reactor/grids/tests/test_grids.py +++ b/armi/reactor/grids/tests/test_grids.py @@ -226,8 +226,8 @@ class TestHexGrid(unittest.TestCase): """A set of tests for the Hexagonal Grid. .. test: Tests of the Hexagonal grid. - :id: TEST_REACTOR_MESH_0 - :link: REQ_REACTOR_MESH + :id: T_REACTOR_MESH_0 + :link: R_REACTOR_MESH """ def test_positions(self): @@ -493,8 +493,8 @@ class TestThetaRZGrid(unittest.TestCase): """A set of tests for the RZTheta Grid. .. test: Tests of the RZTheta grid. - :id: TEST_REACTOR_MESH_1 - :link: REQ_REACTOR_MESH + :id: T_REACTOR_MESH_1 + :link: R_REACTOR_MESH """ def test_positions(self): @@ -516,8 +516,8 @@ class TestCartesianGrid(unittest.TestCase): """A set of tests for the Cartesian Grid. .. test: Tests of the Cartesian grid. - :id: TEST_REACTOR_MESH_2 - :link: REQ_REACTOR_MESH + :id: T_REACTOR_MESH_2 + :link: R_REACTOR_MESH """ def test_ringPosNoSplit(self): diff --git a/armi/reactor/grids/thetarz.py b/armi/reactor/grids/thetarz.py index c0bd9c9dc..e954c291b 100644 --- a/armi/reactor/grids/thetarz.py +++ b/armi/reactor/grids/thetarz.py @@ -39,8 +39,8 @@ class ThetaRZGrid(StructuredGrid): See Figure 2.2 in Derstine 1984, ANL. [DIF3D]_. .. impl:: ARMI supports an RZTheta mesh. - :id: IMPL_REACTOR_MESH_3 - :links: REQ_REACTOR_MESH + :id: I_REACTOR_MESH_3 + :links: R_REACTOR_MESH """ def getSymmetricEquivalents(self, indices: IJType) -> NoReturn: diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index f3215cfd7..0a84d9b0f 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -21,8 +21,8 @@ plant-wide state variables such as keff, cycle, and node. .. impl:: ARMI represents the Reactor heirarchically. - :id: IMPL_REACTOR_HIERARCHY_0 - :links: REQ_REACTOR_HIERARCHY + :id: I_REACTOR_HIERARCHY_0 + :links: R_REACTOR_HIERARCHY The Reactor contains a Core, which contains a heirachical collection of Assemblies, which in turn each contain a collection of Blocks. diff --git a/armi/reactor/tests/test_components.py b/armi/reactor/tests/test_components.py index b94391868..64672f2b1 100644 --- a/armi/reactor/tests/test_components.py +++ b/armi/reactor/tests/test_components.py @@ -260,8 +260,8 @@ def test_preserveMassDuringThermalExpansion(self): """Test that when we thermally expand any arbitrary shape, mass is conserved. .. test:: Test that ARMI can thermally expand any arbitrary shape. - :id: TEST_REACTOR_THERMAL_EXPANSION_0 - :links: REQ_REACTOR_THERMAL_EXPANSION + :id: T_REACTOR_THERMAL_EXPANSION_0 + :links: R_REACTOR_THERMAL_EXPANSION """ if not self.component.THERMAL_EXPANSION_DIMS: return @@ -373,8 +373,8 @@ def test_getThermalExpansionFactorConservedMassByLinearExpansionPercent(self): """Test that when ARMI thermally expands a circle, mass is conserved. .. test:: Test that ARMI correctly thermally expands objects with circular shape. - :id: TEST_REACTOR_THERMAL_EXPANSION_1 - :links: REQ_REACTOR_THERMAL_EXPANSION + :id: T_REACTOR_THERMAL_EXPANSION_1 + :links: R_REACTOR_THERMAL_EXPANSION """ hotTemp = 700.0 dLL = self.component.material.linearExpansionFactor( @@ -394,8 +394,8 @@ def test_thermallyExpands(self): """Test that ARMI can thermally expands a circle. .. test:: Test that ARMI can thermally expands a circle - :id: TEST_REACTOR_THERMAL_EXPANSION_2 - :links: REQ_REACTOR_THERMAL_EXPANSION + :id: T_REACTOR_THERMAL_EXPANSION_2 + :links: R_REACTOR_THERMAL_EXPANSION """ self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) @@ -712,8 +712,8 @@ def test_thermallyExpands(self): """Test that ARMI can thermally expands a triangle. .. test:: Test that ARMI can thermally expands a triangle - :id: TEST_REACTOR_THERMAL_EXPANSION_3 - :links: REQ_REACTOR_THERMAL_EXPANSION + :id: T_REACTOR_THERMAL_EXPANSION_3 + :links: R_REACTOR_THERMAL_EXPANSION """ self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) @@ -780,8 +780,8 @@ def test_thermallyExpands(self): """Test that ARMI can thermally expands a rectangle. .. test:: Test that ARMI can thermally expands a rectangle - :id: TEST_REACTOR_THERMAL_EXPANSION_4 - :links: REQ_REACTOR_THERMAL_EXPANSION + :id: T_REACTOR_THERMAL_EXPANSION_4 + :links: R_REACTOR_THERMAL_EXPANSION """ self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) @@ -826,8 +826,8 @@ def test_thermallyExpands(self): """Test that ARMI can thermally expands a solid rectangle. .. test:: Test that ARMI can thermally expands a solid rectangle - :id: TEST_REACTOR_THERMAL_EXPANSION_5 - :links: REQ_REACTOR_THERMAL_EXPANSION + :id: T_REACTOR_THERMAL_EXPANSION_5 + :links: R_REACTOR_THERMAL_EXPANSION """ self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) @@ -889,8 +889,8 @@ def test_thermallyExpands(self): """Test that ARMI can thermally expands a square. .. test:: Test that ARMI can thermally expands a square - :id: TEST_REACTOR_THERMAL_EXPANSION_6 - :links: REQ_REACTOR_THERMAL_EXPANSION + :id: T_REACTOR_THERMAL_EXPANSION_6 + :links: R_REACTOR_THERMAL_EXPANSION """ self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) @@ -954,8 +954,8 @@ def test_thermallyExpands(self): """Test that ARMI can thermally expands a cube. .. test:: Test that ARMI can thermally expands a cube - :id: TEST_REACTOR_THERMAL_EXPANSION_7 - :links: REQ_REACTOR_THERMAL_EXPANSION + :id: T_REACTOR_THERMAL_EXPANSION_7 + :links: R_REACTOR_THERMAL_EXPANSION """ self.assertFalse(self.component.THERMAL_EXPANSION_DIMS) @@ -993,8 +993,8 @@ def test_thermallyExpands(self): """Test that ARMI can thermally expands a hexagon. .. test:: Test that ARMI can thermally expands a hexagon - :id: TEST_REACTOR_THERMAL_EXPANSION_8 - :links: REQ_REACTOR_THERMAL_EXPANSION + :id: T_REACTOR_THERMAL_EXPANSION_8 + :links: R_REACTOR_THERMAL_EXPANSION """ self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) @@ -1057,8 +1057,8 @@ def test_thermallyExpands(self): """Test that ARMI can thermally expands a holed hexagon. .. test:: Test that ARMI can thermally expands a holed hexagon - :id: TEST_REACTOR_THERMAL_EXPANSION_9 - :links: REQ_REACTOR_THERMAL_EXPANSION + :id: T_REACTOR_THERMAL_EXPANSION_9 + :links: R_REACTOR_THERMAL_EXPANSION """ self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) @@ -1108,8 +1108,8 @@ def test_thermallyExpands(self): """Test that ARMI can thermally expands a holed hexagon. .. test:: Test that ARMI can thermally expands a holed hexagon - :id: TEST_REACTOR_THERMAL_EXPANSION_10 - :links: REQ_REACTOR_THERMAL_EXPANSION + :id: T_REACTOR_THERMAL_EXPANSION_10 + :links: R_REACTOR_THERMAL_EXPANSION """ self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index f1b4b1f2b..14437fe71 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -374,8 +374,8 @@ def test_countFuelAxialBlocks(self): """Tests that the users definition of fuel blocks is preserved. .. test:: Tests that the users definition of fuel blocks is preserved. - :id: TEST_REACTOR_2 - :links: REQ_REACTOR + :id: T_REACTOR_2 + :links: R_REACTOR """ numFuelBlocks = self.r.core.countFuelAxialBlocks() self.assertEqual(numFuelBlocks, 3) diff --git a/armi/utils/tests/test_utils.py b/armi/utils/tests/test_utils.py index 9e0f64cfa..ba795f15f 100644 --- a/armi/utils/tests/test_utils.py +++ b/armi/utils/tests/test_utils.py @@ -140,8 +140,8 @@ def test_classesInHierarchy(self): """Tests the classesInHierarchy utility. .. test:: Tests that the Reactor is stored heirarchically - :id: TEST_REACTOR_HIERARCHY_0 - :links: REQ_REACTOR_HIERARCHY + :id: T_REACTOR_HIERARCHY_0 + :links: R_REACTOR_HIERARCHY This test shows that the Blocks and Assemblies are stored heirarchically inside the Core, which is inside the Reactor object. diff --git a/doc/requirements/srsd.rst b/doc/requirements/srsd.rst index 26039c939..0d1fbc31d 100644 --- a/doc/requirements/srsd.rst +++ b/doc/requirements/srsd.rst @@ -32,25 +32,25 @@ Functional Requirements ----------------------- .. req:: The database shall maintain fidelity of data. - :id: REQ_DB_FIDELITY + :id: R_DB_FIDELITY :status: needs implementation, needs test The database shall faithfully represent the possessed information and not alter its contents, retrieving the data exactly as input. .. req:: The database shall allow case restarts. - :id: REQ_DB_RESTARTS + :id: R_DB_RESTARTS :status: needs implementation, needs test The state information representing as near as possible the entirety of the run when the database write was executed, shall be retrievable to restore the previous case to a particular point in time for further use by analysts. .. req:: The database shall accept all pythonic primitive data types. - :id: REQ_DB_PRIMITIVES + :id: R_DB_PRIMITIVES :status: needs implementation, needs test Given the ubiquity of Python's ``None`` the database shall support its inclusion as a valid entry for data. There will be no support for any abstract data type beyond None. .. req:: The report package shall maintain data fidelity. - :id: REQ_REPORT_FIDELITY + :id: R_REPORT_FIDELITY :status: implemented, needs more tests The report package shall not modify or subvert data integrity as it reports the information out to the user. @@ -60,7 +60,7 @@ The report package shall not modify or subvert data integrity as it reports the TODO: blueprints need some interface and I/O reqs .. req:: The settings package shall not accept ambiguous setting definitions. - :id: REQ_SETTINGS_UNAMBIGUOUS + :id: R_SETTINGS_UNAMBIGUOUS :status: implemented, needs more tests Settings defined in the system must have both the intended data type and default value defined, or it is considered incomplete and therefore invalid. Additionally the system shall not accept multiple definitions of the same name. @@ -68,7 +68,7 @@ Settings defined in the system must have both the intended data type and default TODO: This may be tested by a unit test loading in duplicate setting definitions or cases where a definition does not provide adequate details. .. req:: Settings shall have unique, case-insensitive names. - :id: REQ_SETTINGS_UNAMBIGUOUS_NAME + :id: R_SETTINGS_UNAMBIGUOUS_NAME :status: implemented, needs more tests No two settings may share names. @@ -76,7 +76,7 @@ TODO: This may be tested by a unit test loading in duplicate setting definitions TODO: This may be tested by a unit test loading two similar names .. req:: Settings shall not allow dynamic typing. - :id: REQ_SETTINGS_UNAMBIGUOUS_TYPE + :id: R_SETTINGS_UNAMBIGUOUS_TYPE :status: implemented, needs more tests Settings shall exist exclusively as a well-defined data type, as chosen by the setting definition. @@ -84,7 +84,7 @@ TODO: This may be tested by a unit test loading in duplicate setting definitions TODO: This may be tested by unit tests attempting to subvert the contained data type. .. req:: The settings package shall contain a default state of all settings. - :id: REQ_SETTINGS_DEFAULTS + :id: R_SETTINGS_DEFAULTS :status: implemented, needs more tests Many of the settings will not be altered by the user of a run, and there will likely be too many for a user to deal with on an individual basis. Therefore, most settings will need to function sensibly with their default value. This default value shall always be accessible throughout the runs life cycle. @@ -92,7 +92,7 @@ TODO: This may be tested by a unit test loading in duplicate setting definitions TODO: This may be tested by unit tests loading and checking values on each setting. .. req:: Settings shall support more complex rule association to further customize each setting's behavior. - :id: REQ_SETTINGS_RULES + :id: R_SETTINGS_RULES :status: implemented, needs more tests It shall be possible to support a valid list or range of values for any given setting. @@ -100,7 +100,7 @@ It shall be possible to support a valid list or range of values for any given se TODO: This may be tested by a unit test attempting to set a value outside a given min/max range. .. req:: Setting addition, renaming, and removal shall be supported.setting's behavior. - :id: REQ_SETTINGS_CHANGES + :id: R_SETTINGS_CHANGES :status: implemented, needs more tests The setting package shall accomodate the introduction of new settings, renaming of old settings, and support the complex deprecation behaviors of settings. @@ -108,7 +108,7 @@ The setting package shall accomodate the introduction of new settings, renaming TODO: This may be tested by a unit test containing removed settings references in both input and code references, as well as an additional definition load and use .. req:: The settings package shall support version tracking. - :id: REQ_SETTINGS_VERSION + :id: R_SETTINGS_VERSION :status: implemented, needs more tests Each settings file is only genuinely valid with the version of ARMI that generated the file, as settings might change between versions. As a safegaurd of this, the settings system shall alert the user if the version of the settings file does not match the version of ARMI in-memory. @@ -116,7 +116,7 @@ Each settings file is only genuinely valid with the version of ARMI that generat TODO: This may be tested by unit tests with out of date or omitted version information .. req:: The settings system shall raise an error if the same setting is created twice. - :id: REQ_SETTINGS_DUPLICATES + :id: R_SETTINGS_DUPLICATES :status: implemented, needs more tests When a user creates a setting twice, it shall be detected as an error which is raised to the user. @@ -124,63 +124,63 @@ When a user creates a setting twice, it shall be detected as an error which is r TODO: This may be tested by unit tests loading and checking settings that have a setting created twice, and failing. .. req:: ARMI shall be able to represent a user-specified reactor. - :id: REQ_REACTOR + :id: R_REACTOR :status: implemented, needs more tests Given user input describing a reactor, ARMI shall construct with equivalent fidelity a software model of the reactor. In particular, ARMI shall appropriately represent the shape, arrangement, connectivity, dimensions, materials (including thermo-mechanical properties), isotopic composition, and temperatures of the reactor. .. req:: ARMI shall represent the reactor hierarchically. - :id: REQ_REACTOR_HIERARCHY + :id: R_REACTOR_HIERARCHY :status: completed To maintain consistency with the physical reactor being modeled, ARMI shall maintain a hierarchical definition of its components. For example, all the fuel pins in a single fuel assembly in a solid-fuel reactor shall be collected such that they can be queried or modified as a unit as well as individuals. .. req:: ARMI shall automatically handle thermal expansion. - :id: REQ_REACTOR_THERMAL_EXPANSION + :id: R_REACTOR_THERMAL_EXPANSION :status: completed ARMI shall automatically compute and applied thermal expansion and contraction of materials. .. req:: ARMI shall support a reasonable set of basic shapes. - :id: REQ_REACTOR_SHAPES + :id: R_REACTOR_SHAPES :status: implemented, needs more tests ARMI shall support the following basic shapes: Hexagonal prism (ducts in fast reactors), rectangular prism (ducts in thermal reactors), cylindrical prism (fuel pins, cladding, etc.), and helix (wire wrap). .. req:: ARMI shall support a number of structured mesh options. - :id: REQ_REACTOR_MESH + :id: R_REACTOR_MESH :status: completed ARMI shall support regular, repeating meshes in hexagonal, radial-zeta-theta (RZT), and Cartesian structures. .. req:: ARMI shall support the specification of symmetry options and boundary conditions. - :id: REQ_REACTOR_4 + :id: R_REACTOR_4 :status: implemented, need impl/test ARMI shall support symmetric models including 1/4, 1/8 core models for Cartesian meshes and 1/3 and full core for Hex meshes. For Cartesian 1/8 core symmetry, the core axial symmetry plane (midplane) will be located at the top of the reactor. .. req:: ARMI shall check for basic correctness. - :id: REQ_REACTOR_5 + :id: R_REACTOR_5 :status: implemented, need impl/test ARMI shall check its input for certain obvious errors including unphysical densities and proper fit. .. req:: ARMI shall allow for the definition of limited one-dimensional translation paths. - :id: REQ_REACTOR_6 + :id: R_REACTOR_6 :status: implemented, need impl/test ARMI shall allow the user specification of translation pathways for certain objects to follow, to support moving control mechanisms. .. req:: ARMI shall allow the definition of fuel management operations (i.e. shuffling) - :id: REQ_REACTOR_7 + :id: R_REACTOR_7 :status: implemented, need impl/test ARMI shall allow for the modeling of a reactor over multiple cycles. .. req:: ARMI shall represent and reflect the evolving state of a reactor. - :id: REQ_1 + :id: R_EVOLVE :status: needs implementation, needs test The reactor state shale be made available to users and plugins, which may in turn modify the state. ARMI shall fully define how all aspects of state may be accessed and modified and shall reflect any new state after it is applied. @@ -188,41 +188,41 @@ TODO: This may be tested by unit tests loading and checking settings that have a The reactor state shall be represented as evolving either through time (i.e. in a typical cycle-by-cycle analysis) or through a series of control configurations. .. req:: The operator package shall provide a means by which to communicate inputs and results between analysis plugins. - :id: REQ_OPERATOR_IO + :id: R_OPERATOR_IO :status: needs implementation, needs test The operator package shall receive output from calculation Plugins and store the results on a well-defined central model. A composite pattern shall be used, with a Reactor containing Assemblies containing Blocks, etc. .. req:: The operator package shall provide a means to perform computations in parallel on a high performance computer. - :id: REQ_OPERATOR_PARALLEL + :id: R_OPERATOR_PARALLEL :status: needs implementation, needs test Many analysis tasks require high performance computing (HPC), and the operator package shall contain utilities and routines to communicate with an HPC and to facilitate execution of simulations in parallel. .. req:: The operator package shall allow physics coupling between analysis plugins. - :id: REQ_OPERATOR_COUPLING + :id: R_OPERATOR_COUPLING :status: implemented, needs more tests For coupled physics (e.g. neutronics depends on thermal hydraulics depends on neutronics), the operator package shall allow loose and/or tight coupling. Loose coupling is using the values from the previous timestep to update the next timestep. Tight is an operator-splitting iteration until convergence between one or more plugins. .. req:: The operator package shall allow analysis plugins to be replaced without affecting interfaces in other plugins. - :id: REQ_OPERATOR_ANALYSIS + :id: R_OPERATOR_ANALYSIS :status: needs implementation, needs test Often, a plugin is replaced with a new plugin fulfilling some new requirement. When this happens, the operator package shall isolate required changes to the new plugin. For example, if a fuel performance plugin needs temperatures but the temperature-computing plugin is replaced, the fuel performance plugin should require no changes to work with the drop-in replacement. This requires modular design and standardization in state names. .. req:: The operator package shall coordinate calls to the various plugins. - :id: REQ_OPERATOR_COORD + :id: R_OPERATOR_COORD :status: needs implementation, needs test Based on user settings, the ordering, initialization, and calls to other plugins shall be coordinated by the operator package. The operator package must therefore be aware of dependencies of each plugin. .. req:: The latticePhysics package will execute the lattice physics code in a parallel or serial fashion depending on the mode. - :id: REQ_LATTICE_EXE + :id: R_LATTICE_EXE :status: needs implementation, needs test .. req:: The nucDirectory package shall contain basic nuclide information for a wide range of nuclides. - :id: REQ_NUCDIR_INFO + :id: R_NUCDIR_INFO :status: needs implementation, needs test The nucDirectory package shall contain the following general information for each nuclide: @@ -236,7 +236,7 @@ The nucDirectory package shall contain the following general information for eac - meta stable state .. req:: The nucDirectory package shall store data separately from code. - :id: REQ_NUCDIR_FILES + :id: R_NUCDIR_FILES :status: needs implementation, needs test The software shall be made flexible such that the definition of specific nuclides available (i.e. those used in a version of MCC), can be updated without modifying the code. @@ -244,7 +244,7 @@ The software shall be made flexible such that the definition of specific nuclide TODO: This can be tested by inspecting the logic of the code to retrieve data from a resource file, or by modifying the resource file to create an expected outcome. .. req:: The nucDirectory package shall enforce unique nuclide names. - :id: REQ_NUCDIR_UNIQUE + :id: R_NUCDIR_UNIQUE :status: needs implementation, needs test The nuclides names shall be unique, and consist of the nuclide's symbol, mass number, and an indication if it is in a meta-stable state. Elemental nuclides shall omit the mass number, since they represent more than a single mass number. Lumped nuclides shall also have unqiue, user-data identified names. @@ -252,7 +252,7 @@ The nuclides names shall be unique, and consist of the nuclide's symbol, mass nu TODO: A unit test can be used to demonstrate that all nuclide names are unique. .. req:: The nucDirectory package shall be capable of generating unique 4-character labels. - :id: REQ_NUCDIR_LABELS + :id: R_NUCDIR_LABELS :status: needs implementation, needs test Versions 2 and 3 of MCC allow for unique 6 character labels to be used to reference nuclides. Two characters need to be used to describe the different cross section sets used by the problem. Therefore, every nuclide in ARMI needs to have a unique 4 character representation to use in MCC and the downstream global flux solver. @@ -260,7 +260,7 @@ Versions 2 and 3 of MCC allow for unique 6 character labels to be used to refere TODO: A unit test can be used to demonstrate that all nuclides have unique 4-character labels. .. req:: The nucDirectory package shall allow for use of lumped nuclides. - :id: REQ_NUCDIR_LUMPED + :id: R_NUCDIR_LUMPED :status: needs implementation, needs test Lumped nuclides are bulk defined nuclides that are typically used when modeling fission products. Lumping the nuclides during burnup calculations lowers the problem size without having a significant impact on the results. Consequently, they do not always need to be modeled individually, but can be grouped. @@ -268,19 +268,19 @@ Lumped nuclides are bulk defined nuclides that are typically used when modeling TODO: A unit test can be used to demonstrate that lumped nuclides can be used and created. .. req:: The nucDirectory package shall allow for elemental nuclides. - :id: REQ_NUCDIR_ELEMENTALS + :id: R_NUCDIR_ELEMENTALS :status: needs implementation, needs test The nuclear data libraries available in versions 2 and 3 of MCC do not always allow for nuclide input, and some materials are grouped into elemental nuclides. Iron is an example of this in MCC version 2. Consequently, ARMI needs to be able to model elemental nuclides which represent the entire element, as well as the individual nuclides. .. req:: The nucDirectory package shall allow for dummy nuclides. - :id: REQ_NUCDIR_DUMMY + :id: R_NUCDIR_DUMMY :status: needs implementation, needs test Dummy nuclides, typically written in all capitals as "DUMMY", are used to truncate the burn chain in order to reduce the problem size without compromising the results. .. req:: The nucDirectory package shall allow for indexing of nuclide information. - :id: REQ_NUCDIR_INDEX + :id: R_NUCDIR_INDEX :status: needs implementation, needs test The nuclear data files created by physics codes such as MCC and DIF3D may not necessary correspond to the name used within ARMI, it will be necessary to load nuclide information based on a non-ARMI name. The software shall provide lookup mechanisms for nuclide objects based on: @@ -290,7 +290,7 @@ The nuclear data files created by physics codes such as MCC and DIF3D may not ne - MCC versions 2 and 3 IDs .. req:: The nucDirectory package shall contain decay chain data. - :id: REQ_NUCDIR_DECAY_CHAIN + :id: R_NUCDIR_DECAY_CHAIN :status: needs implementation, needs test The decay chain is an important step in performing burn-up calculations. The nucDirectory shall contain necessary decay mechanisms: @@ -306,7 +306,7 @@ The nucDirectory shall contain the half-life, decay mode(s) with corresponding b TODO: A unit test can be generated to test that the correct decay chain is present, and that the data matches other resources. .. req:: The nucDirectory package shall contain transmutation data. - :id: REQ_NUCDIR_TRANSMUTE + :id: R_NUCDIR_TRANSMUTE :status: needs implementation, needs test In addition to the decay chain, nuclides may transmute through interactions into other nuclides. The nucDirectory shall contain transmutations including: @@ -323,7 +323,7 @@ The nucDirectory shall contain the transmutation mechanism, branch ratio, and pr TODO: A unit test can be generated to test that the correct transmutations are present, and corresponding data matches other resources. .. req:: The nucDirectory package shall warn the user if there are potential burn-chain faults. - :id: REQ_NUCDIR_BURN_CHAIN + :id: R_NUCDIR_BURN_CHAIN :status: needs implementation, needs test The user supplies the nuclides to be modeled in the simulation; therefore, it is possible that the user may inadvertently describe a burn-chain that is not complete. The software shall be capable of detecting erroneous user input and terminate the program. @@ -331,7 +331,7 @@ The user supplies the nuclides to be modeled in the simulation; therefore, it is TODO: A unit test can be generated with faulty decay chains to determine that they do not work. .. req:: The nuclearDataIO package shall read and write ISOTXS files. - :id: REQ_NUCDATA_ISOTXS + :id: R_NUCDATA_ISOTXS :status: needs implementation, needs test ISOTXS files contain the multi-group microscopic cross sections, and other nuclear data, for each nuclide being modeled. The multi-group cross sections are used throughout ARMI. @@ -341,7 +341,7 @@ The software shall be capable of reading an ISOTXS file into memory, and writing TODO: A unit test can be created with reads an ISOTXS file generated by MCC, and then writes out the file to another name. The two files can then be compared using a binary file comparison to demonstrate that the contents of the files are identical. .. req:: The nuclearDataIO package shall read and write GAMISO files. - :id: REQ_NUCDATA_GAMISO + :id: R_NUCDATA_GAMISO :status: needs implementation, needs test GAMISO files are generated by MCC-v3, and are the same format as an ISOTXS file. The file contains photon interaction cross sections instead of neutron cross sections. @@ -351,7 +351,7 @@ The software shall be capable of reading a GAMISO file into memory, and writing TODO: This can be covered in a unit test; the unit test can be the same as described for ISOTXS files. .. req:: The nuclearDataIO package shall read and write PMATRX files. - :id: REQ_NUCDATA_PMATRX + :id: R_NUCDATA_PMATRX :status: needs implementation, needs test PMATRX files contain the gamma production matrix resulting from fission or capture events. Given a neutron flux distribution, and a PMATRX file, the gamma source can be computed and then used to determine gamma transport and heating. @@ -368,7 +368,7 @@ The software shall be capable of reading a DLAYXS file into memory, and writing TODO: This can be covered in a unit test; the unit test can be the same as described for ISOTXS files. .. req:: The nuclearDataIO package shall merge files of the same type. - :id: REQ_NUCDATA_MERGE + :id: R_NUCDATA_MERGE :status: needs implementation, needs test The software shall be capable merging multiple files of the same type (ISOTXS, PMATRX, etc.) into a single file meeting the specifications. The software shall fail with a descriptive error message if any two nuclides have the same name. @@ -380,13 +380,13 @@ This can be covered in a unit test which runs 3 MCC-v3 cases. The third MCC-v3 case will produce a merged ISOTXS file which can be compared to an ISOTXS file generated by merging the output ISOTXS from cases 1 and 2. .. req:: The nuclearDataIO package shall make the data programmatically available. - :id: REQ_NUCDATA_AVAIL + :id: R_NUCDATA_AVAIL :status: needs implementation, needs test The software shall make the nuclear data provided in ISOTXS, GAMISO, PMATRX and DLAYXS available in the form of Python objects, such that it can be used elsewhere in the code, such as in the depletion, nuclear uncertainty quantification, and `\beta` calculations. .. req:: The nuclearDataIO package shall key nuclear data based on nuclide label and xsID. - :id: REQ_NUCDATA_AVAIL_LABEL + :id: R_NUCDATA_AVAIL_LABEL :status: needs implementation, needs test When nuclear data files are read, they should be made available in a container object, such as a dictionary, and keyed on the nuclide label (a unique four character nuclide identifier) and the cross section ID, a two character identifier for block type and burnup group. @@ -394,7 +394,7 @@ When nuclear data files are read, they should be made available in a container o TODO: This can be covered by a unit test which reads an ISOTXS into a container object, and then obtaining cross sections by using the nuclide label and xsID. .. req:: The nuclearDataIO package shall be able to remove nuclides from specifc nuclear data files. - :id: REQ_NUCDATA_AVAIL_FILES + :id: R_NUCDATA_AVAIL_FILES :status: needs implementation, needs test ARMI has a concept of "lumped fission products" that result in more nuclides being in ISOTXS, GAMISO, and PMATRX files than are needed for subsequent calculations. The software shall be capable of removing the unused nuclides from ISOTXS, GAMISO, and PMATRX files. This generally does not apply to DLAYXS files, because they typically only contain nuclides that fission. @@ -402,7 +402,7 @@ ARMI has a concept of "lumped fission products" that result in more nuclides bei TODO: This can be covered by a unit test where a file is read in, a nuclide removed, and then rewritten and reread. The reread file should not contain the removed nuclides. .. req:: The nuclearDataIO package shall be able to modify the nuclear data. - :id: REQ_NUCDATA_AVAIL_MODIFY + :id: R_NUCDATA_AVAIL_MODIFY :status: needs implementation, needs test In order to calculate the uncertainties of our methodology introduced by nuclear data uncertainty, it is necessary to be able to perturb (i.e. modify) specific values within the nuclear data files. @@ -414,26 +414,26 @@ Performance Requirements ------------------------ .. req:: The database representation on disk shall be smaller than the the in-memory Python representation - :id: REQ_DB_PERFORMANCE + :id: R_DB_PERFORMANCE :status: needs implementation, needs test The database implementation shall use lossless compression to reduce the database size. .. req:: The report package shall present no burden. - :id: REQ_REPORT_PERFORMANCE + :id: R_REPORT_PERFORMANCE :status: needs implementation, needs test As the report package is a lightweight interface to write data out to a text based format, and render a few images, the performance costs are entirely negligible and should not burden the run, nor the user's computer in both memory and processor time. .. req:: The reactor package shall allow rapid synchronization of state across the network to parallel processors. - :id: REQ_REACTOR_PARALLEL + :id: R_REACTOR_PARALLEL :status: needs implementation, needs test For performance, many physics calculations are done in parallel. The reactor must be able to synchronize the state on multiple processors efficiently. .. req:: The nucDirectory package shall try to prevent data duplication to limit the memory footprint of this information. - :id: REQ_NUCDIR_DUPLICATION + :id: R_NUCDIR_DUPLICATION :status: needs implementation, needs test TODO: Is this testable? @@ -444,15 +444,15 @@ Software Attributes ------------------- .. req:: ARMI shall generally support at least one modern Windows and one modern CentOS operating system version. - :id: REQ_OS + :id: R_OS :status: needs implementation, needs test .. req:: The database produced shall be easily accessible in a variety of programming environments beyond Python. - :id: REQ_DB_LANGUAGE + :id: R_DB_LANGUAGE :status: needs implementation, needs test .. req:: The settings package shall use human-readable, plain-text files as input. - :id: REQ_SETTINGS_READABLE + :id: R_SETTINGS_READABLE :status: implemented, needs more tests The user must be able to read and edit their settings file as plain text in broadly any typical text editor. @@ -463,25 +463,25 @@ Software Design Constraints --------------------------- .. req:: The report package shall not burden new developers with grasping a complex system. - :id: REQ_REPORT_TECH + :id: R_REPORT_TECH :status: needs implementation, needs test Given the functional requirements of the report package, new developers should be able to understand how to contribute to a report nigh instantly. No new technologies should be introduced to the system as HTML and ASCII are both purely text-based. .. req:: The reactor package shall not exhibit any stochastic behavior. - :id: REQ_REACTOR_STOCHASTIC + :id: R_REACTOR_STOCHASTIC :status: needs implementation, needs test Any two ARMI runs with the same input file must produce the same results. .. req:: The nucDirectory package shall use nuclear data that is contained within the ARMI code base. - :id: REQ_NUCDIR_DATA + :id: R_NUCDIR_DATA :status: needs implementation, needs test The nucDirectory package shall not use data data retrieved from online sources. The intent here is to prevent inadvertent security risks. .. req:: The nucDirectory package shall follow a particular naming convention. - :id: REQ_NUCDIR_NAMING + :id: R_NUCDIR_NAMING :status: needs implementation, needs test Other physics codes use the name Am-242 for the metastable state of Am-242, and use Am-242g for the ground state. @@ -495,7 +495,7 @@ Interface I/O Requirements TODO: blueprints need some interface and I/O reqs .. req:: The setting system shall render a view of every defined setting as well as the key attributes associated with it - :id: REQ_SETTINGS_REPORT + :id: R_SETTINGS_REPORT :status: implemented, needs more tests Utilizing the documentation of the ARMI project the settings system shall contribute a page containing a table summary of the settings included in the system. @@ -503,15 +503,15 @@ Utilizing the documentation of the ARMI project the settings system shall contri TODO: This is completed by the :doc:`Settings Report `. .. req:: The latticePhysics package will write input files for the desired code for each representative block to be modeled. - :id: REQ_LATTICE_INPUTS + :id: R_LATTICE_INPUTS :status: needs implementation, needs test .. req:: The latticePhysics package will use the output(s) to create a reactor library, ``ISOTXS`` or ``COMPXS``, used in the global flux solution. - :id: REQ_LATTICE_OUTPUTS + :id: R_LATTICE_OUTPUTS :status: needs implementation, needs test .. req:: The reactor package shall check input for basic correctness. - :id: REQ_REACTOR_CORRECTNESS + :id: R_REACTOR_CORRECTNESS :status: needs implementation, needs test The reactor package shall check its inputs for certain obvious errors including unphysical quantities. At a deep level, the reactor package will not attempt to fully validate subtle engineering aspects of the reactor; that is more generally the reason users will want to fully simulate a reactor and cannot be done at input time. From 4e4451ba7bdd00696ed1419d2c3acf24504482ca Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 10 Oct 2023 14:24:59 -0700 Subject: [PATCH 016/176] Version-pinning the h5py library (#1429) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 80b548419..c84985d7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ authors = [ dependencies = [ "configparser", "coverage", - "h5py>=3.0", + "h5py>=3.0,<=3.9", "htmltree", "matplotlib", "numpy>=1.21,<=1.23.5", From 17ece5db8760eb92720644e1d6ade6b49afbf58a Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 10 Oct 2023 15:42:05 -0700 Subject: [PATCH 017/176] Removing sphinx-needs (#1430) --- doc/conf.py | 4 ---- doc/requirements/srsd.rst | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 952fa4989..7b370cd42 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -113,9 +113,6 @@ def setup(app): # -- General configuration ----------------------------------------------------- -# If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = "4.4.0" - # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ @@ -137,7 +134,6 @@ def setup(app): "sphinx_gallery.gen_gallery", "sphinx.ext.imgconverter", # to convert GH Actions badge SVGs to PNG for LaTeX "sphinxcontrib.plantuml", - "sphinx_needs", "sphinx_rtd_theme", # needed here for loading jquery in sphinx 6 "sphinxcontrib.jquery", # see https://github.com/readthedocs/sphinx_rtd_theme/issues/1452 ] diff --git a/doc/requirements/srsd.rst b/doc/requirements/srsd.rst index 0d1fbc31d..e86adacb2 100644 --- a/doc/requirements/srsd.rst +++ b/doc/requirements/srsd.rst @@ -444,7 +444,7 @@ Software Attributes ------------------- .. req:: ARMI shall generally support at least one modern Windows and one modern CentOS operating system version. - :id: R_OS + :id: R_OSS :status: needs implementation, needs test .. req:: The database produced shall be easily accessible in a variety of programming environments beyond Python. From 1b5e7492c3b0a4ceb75387eda5121147057816f4 Mon Sep 17 00:00:00 2001 From: Chris Keckler Date: Tue, 10 Oct 2023 18:13:44 -0500 Subject: [PATCH 018/176] Allow for different file formats in plotBlockDiagram (#1422) --- armi/utils/plotting.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/armi/utils/plotting.py b/armi/utils/plotting.py index 64696ec84..cdfad1219 100644 --- a/armi/utils/plotting.py +++ b/armi/utils/plotting.py @@ -1372,7 +1372,9 @@ def _makeComponentPatch(component, position, cold): return [blockPatch] -def plotBlockDiagram(block, fName, cold, cmapName="RdYlBu", materialList=None): +def plotBlockDiagram( + block, fName, cold, cmapName="RdYlBu", materialList=None, fileFormat="svg" +): """Given a Block with a spatial Grid, plot the diagram of it with all of its components. (wire, duct, coolant, etc...). @@ -1380,14 +1382,16 @@ def plotBlockDiagram(block, fName, cold, cmapName="RdYlBu", materialList=None): ---------- block : block object fName : String - name of the file to save to + Name of the file to save to cold : boolean - true is for cold temps, hot is false. + True is for cold temps, False is hot cmapName : String name of a colorMap to use for block colors - materialList: List - a list of material names across all blocks to be plotted - so that same material on all diagrams will have the same color. + materialList : List + A list of material names across all blocks to be plotted + so that same material on all diagrams will have the same color + fileFormat : str + The format to save the picture as, e.g. svg, png, jpg, etc. """ _, ax = plt.subplots(figsize=(20, 20), dpi=200) @@ -1440,7 +1444,7 @@ def plotBlockDiagram(block, fName, cold, cmapName="RdYlBu", materialList=None): ax.spines["left"].set_visible(False) ax.spines["bottom"].set_visible(False) ax.margins(0) - plt.savefig(fName, format="svg", **pltKwargs) + plt.savefig(fName, format=fileFormat, **pltKwargs) plt.close() return os.path.abspath(fName) From 8b5485c4ac4f988a680af7444c985698cdc9cbd4 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Wed, 11 Oct 2023 09:55:47 -0700 Subject: [PATCH 019/176] Removing unused Database3.load() argument (#1431) --- armi/bookkeeping/db/database3.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/armi/bookkeeping/db/database3.py b/armi/bookkeeping/db/database3.py index e3f7ccb44..5e073e297 100644 --- a/armi/bookkeeping/db/database3.py +++ b/armi/bookkeeping/db/database3.py @@ -651,7 +651,6 @@ def load( statePointName=None, allowMissing=False, updateGlobalAssemNum=True, - updateMasterCs=True, ): """Load a new reactor from (cycle, node). @@ -679,8 +678,6 @@ def load( with undefined parameters. Default False. updateGlobalAssemNum : bool, optional DeprecationWarning: This is unused. - updateMasterCs : bool, optional - TODO: Deprecated. Slated for removal. Returns ------- From 29652f3de3a6af97b5becd20f1c8418f7521f152 Mon Sep 17 00:00:00 2001 From: Tony Alberti Date: Wed, 11 Oct 2023 10:16:48 -0700 Subject: [PATCH 020/176] Add test coverage to Blocks (#1426) --- armi/reactor/tests/test_blocks.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py index 381bce355..3d1a0135d 100644 --- a/armi/reactor/tests/test_blocks.py +++ b/armi/reactor/tests/test_blocks.py @@ -16,11 +16,14 @@ import math import os import unittest +import io import numpy from numpy.testing import assert_allclose from armi import materials, runLog, settings, tests +from armi.reactor import blueprints +from armi.reactor.blueprints.tests.test_blockBlueprints import FULL_BP from armi.reactor.components import basicShapes, complexShapes from armi.nucDirectory import nucDir, nuclideBases from armi.nuclearDataIO.cccc import isotxs @@ -302,6 +305,30 @@ def getComponentData(component): return component, density, volume, mass +class TestDetailedNDensUpdate(unittest.TestCase): + def setUp(self): + cs = settings.Settings() + with io.StringIO(FULL_BP) as stream: + bps = blueprints.Blueprints.load(stream) + bps._prepConstruction(cs) + self.r = tests.getEmptyHexReactor() + self.r.blueprints = bps + a = makeTestAssembly(numBlocks=1, assemNum=0) + a.add(buildSimpleFuelBlock()) + self.r.core.add(a) + + def test_updateDetailedNdens(self): + # get first block in assembly with 'fuel' key + block = self.r.core[0][0] + # get nuclides in first component in block + adjList = block[0].getNuclides() + block.p.detailedNDens = numpy.array([1.0]) + block.p.pdensDecay = 1.0 + block._updateDetailedNdens(frac=0.5, adjustList=adjList) + self.assertEqual(block.p.pdensDecay, 0.5) + self.assertEqual(block.p.detailedNDens, numpy.array([0.5])) + + class Block_TestCase(unittest.TestCase): def setUp(self): self.block = loadTestBlock() @@ -1089,7 +1116,6 @@ def test_completeInitialLoading(self): self.assertAlmostEqual(cur, ref, places=places) def test_add(self): - numComps = len(self.block.getComponents()) fuelDims = {"Tinput": 25.0, "Thot": 600, "od": 0.76, "id": 0.00, "mult": 127.0} @@ -1109,7 +1135,6 @@ def test_hasComponents(self): ) def test_getComponentNames(self): - cur = self.block.getComponentNames() ref = set( [ @@ -1393,7 +1418,7 @@ def test_106_getAreaFractions(self): fracs[c.getName()] = a / tot places = 6 - for (c, a) in cur: + for c, a in cur: self.assertAlmostEqual(a, fracs[c.getName()], places=places) self.assertAlmostEqual(sum(fracs.values()), sum([a for c, a in cur])) From d3afb25e669b7c2db7f53ea151df1ad31ed3633c Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Mon, 16 Oct 2023 11:04:13 -0700 Subject: [PATCH 021/176] Removing deprecated Database3.load() param (#1434) --- armi/bookkeeping/db/database3.py | 9 --------- armi/bookkeeping/db/databaseInterface.py | 5 +---- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/armi/bookkeeping/db/database3.py b/armi/bookkeeping/db/database3.py index 5e073e297..d7bbf9eba 100644 --- a/armi/bookkeeping/db/database3.py +++ b/armi/bookkeeping/db/database3.py @@ -650,7 +650,6 @@ def load( bp=None, statePointName=None, allowMissing=False, - updateGlobalAssemNum=True, ): """Load a new reactor from (cycle, node). @@ -676,8 +675,6 @@ def load( allowMissing : bool, optional Whether to emit a warning, rather than crash if reading a database with undefined parameters. Default False. - updateGlobalAssemNum : bool, optional - DeprecationWarning: This is unused. Returns ------- @@ -719,12 +716,6 @@ def load( ) root = comps[0][0] - if updateGlobalAssemNum: - runLog.warning( - "The method input `updateGlobalAssemNum` is no longer used.", - single=True, - ) - # return a Reactor object if cs[CONF_SORT_REACTOR]: root.sort() diff --git a/armi/bookkeeping/db/databaseInterface.py b/armi/bookkeeping/db/databaseInterface.py index d49f04aec..eddfd129a 100644 --- a/armi/bookkeeping/db/databaseInterface.py +++ b/armi/bookkeeping/db/databaseInterface.py @@ -302,9 +302,7 @@ def _getLoadDB(self, fileName): if os.path.exists(self.cs["reloadDBName"]): yield Database3(self.cs["reloadDBName"], "r") - def loadState( - self, cycle, timeNode, timeStepName="", fileName=None, updateGlobalAssemNum=True - ): + def loadState(self, cycle, timeNode, timeStepName="", fileName=None): """ Loads a fresh reactor and applies it to the Operator. @@ -329,7 +327,6 @@ def loadState( statePointName=timeStepName, cs=self.cs, allowMissing=True, - updateGlobalAssemNum=updateGlobalAssemNum, ) self.o.reattach(newR, self.cs) break From 68f2ede890e4e2f2d764b8da04fc14739655dc1e Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Wed, 18 Oct 2023 09:05:50 -0700 Subject: [PATCH 022/176] Removed requiements, so the work can be started fresh (#1438) --- armi/operators/operator.py | 4 - armi/reactor/blueprints/__init__.py | 8 +- .../blueprints/tests/test_blueprints.py | 14 +- armi/reactor/components/basicShapes.py | 7 - armi/reactor/components/complexShapes.py | 11 +- armi/reactor/components/component.py | 4 - armi/reactor/components/volumetricShapes.py | 9 +- armi/reactor/grids/cartesian.py | 4 - armi/reactor/grids/hexagonal.py | 4 - armi/reactor/grids/structuredgrid.py | 4 - armi/reactor/grids/tests/test_grids.py | 21 +- armi/reactor/grids/thetarz.py | 4 - armi/reactor/reactors.py | 7 - armi/reactor/tests/test_components.py | 77 +-- armi/reactor/tests/test_reactors.py | 7 +- armi/utils/tests/test_utils.py | 10 +- doc/release/0.2.rst | 1 + doc/requirements/index.rst | 25 - doc/requirements/sdid.rst | 223 -------- doc/requirements/srsd.rst | 541 ------------------ doc/requirements/str.rst | 133 ----- 21 files changed, 22 insertions(+), 1096 deletions(-) delete mode 100644 doc/requirements/index.rst delete mode 100644 doc/requirements/sdid.rst delete mode 100644 doc/requirements/srsd.rst delete mode 100644 doc/requirements/str.rst diff --git a/armi/operators/operator.py b/armi/operators/operator.py index f86a49e0f..4d1888b07 100644 --- a/armi/operators/operator.py +++ b/armi/operators/operator.py @@ -21,10 +21,6 @@ This is analogous to a real reactor operating over some period of time, often from initial startup, through the various cycles, and out to the end of plant life. - -.. impl:: ARMI controls the time flow of the reactor, by running a sequence of Interfaces at each time step. - :id: I_EVOLVING_STATE_0 - :links: R_EVOLVING_STATE """ import collections import os diff --git a/armi/reactor/blueprints/__init__.py b/armi/reactor/blueprints/__init__.py index 00930d788..179c664a2 100644 --- a/armi/reactor/blueprints/__init__.py +++ b/armi/reactor/blueprints/__init__.py @@ -171,13 +171,7 @@ def __new__(mcs, name, bases, attrs): class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector): - """ - Base Blueprintsobject representing all the subsections in the input file. - - .. impl:: ARMI represents a user-specified reactor by providing a "Blueprint" YAML interface. - :id: I_REACTOR_0 - :links: R_REACTOR - """ + """Base Blueprintsobject representing all the subsections in the input file.""" nuclideFlags = yamlize.Attribute( key="nuclide flags", type=isotopicOptions.NuclideFlags, default=None diff --git a/armi/reactor/blueprints/tests/test_blueprints.py b/armi/reactor/blueprints/tests/test_blueprints.py index 57ca1e943..118e54e99 100644 --- a/armi/reactor/blueprints/tests/test_blueprints.py +++ b/armi/reactor/blueprints/tests/test_blueprints.py @@ -65,12 +65,7 @@ def tearDownClass(cls): cls.directoryChanger.close() def test_nuclides(self): - """Tests the available sets of nuclides work as expected. - - .. test:: Tests that users can define their nuclides of interest. - :id: T_REACTOR_0 - :links: R_REACTOR - """ + """Tests the available sets of nuclides work as expected.""" actives = set(self.blueprints.activeNuclides) inerts = set(self.blueprints.inertNuclides) self.assertEqual( @@ -92,12 +87,7 @@ def test_specialIsotopicVectors(self): self.assertAlmostEqual(mox["PU239"], 0.00286038) def test_componentDimensions(self): - """Tests that the user can specifiy the dimensions of a component with arbitray fidelity. - - .. test:: Tests that the user can specify the dimensions of a component with arbitrary fidelity. - :id: T_REACTOR_1 - :links: R_REACTOR - """ + """Tests that the user can specifiy the dimensions of a component with arbitray fidelity.""" fuelAssem = self.blueprints.constructAssem(self.cs, name="igniter fuel") fuel = fuelAssem.getComponents(Flags.FUEL)[0] self.assertAlmostEqual(fuel.getDimension("od", cold=True), 0.86602) diff --git a/armi/reactor/components/basicShapes.py b/armi/reactor/components/basicShapes.py index 8abc58f97..c91ed9109 100644 --- a/armi/reactor/components/basicShapes.py +++ b/armi/reactor/components/basicShapes.py @@ -17,13 +17,6 @@ Many reactor components can be described in 2D by circles, hexagons, rectangles, etc. These are defined in this subpackage. - -.. impl:: ARMI supports a reasonable set of basic shapes. - :id: I_REACTOR_SHAPES_0 - :links: R_REACTOR_SHAPES - - Here ARMI implements its support for: Circles, Hexagons, Rectangles, Solid Rectangles, - Squares, and Triangles. """ import math diff --git a/armi/reactor/components/complexShapes.py b/armi/reactor/components/complexShapes.py index 468a16078..ac456e26f 100644 --- a/armi/reactor/components/complexShapes.py +++ b/armi/reactor/components/complexShapes.py @@ -12,16 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Components represented by complex shapes, and typically less widely used. - -.. impl:: ARMI supports a reasonable set of basic shapes. - :id: I_REACTOR_SHAPES_1 - :links: R_REACTOR_SHAPES - - Here ARMI implements its support for: Holed Hexagons, Holed Rectangles, - Holed Squares, and Helixes. -""" +"""Components represented by complex shapes, and typically less widely used.""" import math diff --git a/armi/reactor/components/component.py b/armi/reactor/components/component.py index d8f77953e..ed667ccc0 100644 --- a/armi/reactor/components/component.py +++ b/armi/reactor/components/component.py @@ -179,10 +179,6 @@ class Component(composites.Composite, metaclass=ComponentType): Temperature in C to which dimensions were thermally-expanded upon input. material : str or material.Material The material object that makes up this component and give it its thermo-mechanical properties. - - .. impl:: ARMI allows for thermal expansion of all components by user-defined custom curves. - :id: I_REACTOR_THERMAL_EXPANSION_0 - :links: R_REACTOR_THERMAL_EXPANSION """ DIMENSION_NAMES = tuple() # will be assigned by ComponentType diff --git a/armi/reactor/components/volumetricShapes.py b/armi/reactor/components/volumetricShapes.py index d2aed5eaa..329c537b1 100644 --- a/armi/reactor/components/volumetricShapes.py +++ b/armi/reactor/components/volumetricShapes.py @@ -12,14 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""3-dimensional shapes. - -.. impl:: ARMI supports a reasonable set of basic shapes. - :id: I_REACTOR_SHAPES_2 - :links: R_REACTOR_SHAPES - - Here ARMI implements its support for: Cubes, Spheres, RadialSegments, and more. -""" +"""Three-dimensional shapes.""" import math diff --git a/armi/reactor/grids/cartesian.py b/armi/reactor/grids/cartesian.py index e72a047a0..73ff01f55 100644 --- a/armi/reactor/grids/cartesian.py +++ b/armi/reactor/grids/cartesian.py @@ -67,10 +67,6 @@ class CartesianGrid(StructuredGrid): Grid example where the axes lie between the "center assemblies" (even-by-even). Note that ring 1 has four locations, and that the center of the (0, 0)-index location is offset from the origin. - - .. impl:: ARMI supports a Cartesian mesh. - :id: I_REACTOR_MESH_1 - :links: R_REACTOR_MESH """ @classmethod diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py index d6bb29ac3..60b0fc241 100644 --- a/armi/reactor/grids/hexagonal.py +++ b/armi/reactor/grids/hexagonal.py @@ -67,10 +67,6 @@ class HexGrid(StructuredGrid): (-1, 1) ( 0, 0) ( 1,-1) (-1, 0) ( 0,-1) - - .. impl:: ARMI supports a Hexagonal mesh. - :id: I_REACTOR_MESH_2 - :links: R_REACTOR_MESH """ @staticmethod diff --git a/armi/reactor/grids/structuredgrid.py b/armi/reactor/grids/structuredgrid.py index 50a4860bc..07ec5f2af 100644 --- a/armi/reactor/grids/structuredgrid.py +++ b/armi/reactor/grids/structuredgrid.py @@ -136,10 +136,6 @@ class StructuredGrid(Grid): * Unit step calculations use dot products and must not be polluted by the bound indices. Thus we reduce the size of the unitSteps tuple accordingly. - - .. impl:: ARMI supports a number of structured mesh options. - :id: I_REACTOR_MESH_0 - :links: R_REACTOR_MESH """ def __init__( diff --git a/armi/reactor/grids/tests/test_grids.py b/armi/reactor/grids/tests/test_grids.py index 099338da6..0cae25308 100644 --- a/armi/reactor/grids/tests/test_grids.py +++ b/armi/reactor/grids/tests/test_grids.py @@ -223,12 +223,7 @@ def test_ringPosFromIndicesIncorrect(self): class TestHexGrid(unittest.TestCase): - """A set of tests for the Hexagonal Grid. - - .. test: Tests of the Hexagonal grid. - :id: T_REACTOR_MESH_0 - :link: R_REACTOR_MESH - """ + """A set of tests for the Hexagonal Grid.""" def test_positions(self): grid = grids.HexGrid.fromPitch(1.0) @@ -490,12 +485,7 @@ def test_getIndexBounds(self): class TestThetaRZGrid(unittest.TestCase): - """A set of tests for the RZTheta Grid. - - .. test: Tests of the RZTheta grid. - :id: T_REACTOR_MESH_1 - :link: R_REACTOR_MESH - """ + """A set of tests for the RZTheta Grid.""" def test_positions(self): grid = grids.ThetaRZGrid( @@ -513,12 +503,7 @@ def test_positions(self): class TestCartesianGrid(unittest.TestCase): - """A set of tests for the Cartesian Grid. - - .. test: Tests of the Cartesian grid. - :id: T_REACTOR_MESH_2 - :link: R_REACTOR_MESH - """ + """A set of tests for the Cartesian Grid.""" def test_ringPosNoSplit(self): grid = grids.CartesianGrid.fromRectangle(1.0, 1.0, isOffset=True) diff --git a/armi/reactor/grids/thetarz.py b/armi/reactor/grids/thetarz.py index e954c291b..ec6ed2774 100644 --- a/armi/reactor/grids/thetarz.py +++ b/armi/reactor/grids/thetarz.py @@ -37,10 +37,6 @@ class ThetaRZGrid(StructuredGrid): rather than directly constructing with ``__init__`` See Figure 2.2 in Derstine 1984, ANL. [DIF3D]_. - - .. impl:: ARMI supports an RZTheta mesh. - :id: I_REACTOR_MESH_3 - :links: R_REACTOR_MESH """ def getSymmetricEquivalents(self, indices: IJType) -> NoReturn: diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index 0a84d9b0f..610d7052e 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -19,13 +19,6 @@ Core is a high-level object in the data model in ARMI. They contain assemblies which in turn contain more refinement in representing the physical reactor. The reactor is the owner of many of the plant-wide state variables such as keff, cycle, and node. - -.. impl:: ARMI represents the Reactor heirarchically. - :id: I_REACTOR_HIERARCHY_0 - :links: R_REACTOR_HIERARCHY - - The Reactor contains a Core, which contains a heirachical collection of Assemblies, which in turn - each contain a collection of Blocks. """ from typing import Optional import collections diff --git a/armi/reactor/tests/test_components.py b/armi/reactor/tests/test_components.py index 64672f2b1..074b6e9cc 100644 --- a/armi/reactor/tests/test_components.py +++ b/armi/reactor/tests/test_components.py @@ -257,12 +257,7 @@ class TestShapedComponent(TestGeneralComponents): """Abstract class for all shaped components.""" def test_preserveMassDuringThermalExpansion(self): - """Test that when we thermally expand any arbitrary shape, mass is conserved. - - .. test:: Test that ARMI can thermally expand any arbitrary shape. - :id: T_REACTOR_THERMAL_EXPANSION_0 - :links: R_REACTOR_THERMAL_EXPANSION - """ + """Test that when we thermally expand any arbitrary shape, mass is conserved.""" if not self.component.THERMAL_EXPANSION_DIMS: return temperatures = [25.0, 30.0, 40.0, 60.0, 80.0, 430.0] @@ -370,12 +365,7 @@ class TestCircle(TestShapedComponent): } def test_getThermalExpansionFactorConservedMassByLinearExpansionPercent(self): - """Test that when ARMI thermally expands a circle, mass is conserved. - - .. test:: Test that ARMI correctly thermally expands objects with circular shape. - :id: T_REACTOR_THERMAL_EXPANSION_1 - :links: R_REACTOR_THERMAL_EXPANSION - """ + """Test that when ARMI thermally expands a circle, mass is conserved.""" hotTemp = 700.0 dLL = self.component.material.linearExpansionFactor( Tc=hotTemp, T0=self._coldTemp @@ -391,12 +381,7 @@ def test_getDimension(self): self.assertAlmostEqual(cur, ref) def test_thermallyExpands(self): - """Test that ARMI can thermally expands a circle. - - .. test:: Test that ARMI can thermally expands a circle - :id: T_REACTOR_THERMAL_EXPANSION_2 - :links: R_REACTOR_THERMAL_EXPANSION - """ + """Test that ARMI can thermally expands a circle.""" self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) def test_getBoundingCircleOuterDiameter(self): @@ -709,12 +694,7 @@ def test_getArea(self): self.assertAlmostEqual(cur, ref) def test_thermallyExpands(self): - """Test that ARMI can thermally expands a triangle. - - .. test:: Test that ARMI can thermally expands a triangle - :id: T_REACTOR_THERMAL_EXPANSION_3 - :links: R_REACTOR_THERMAL_EXPANSION - """ + """Test that ARMI can thermally expands a triangle.""" self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) def test_dimensionThermallyExpands(self): @@ -777,12 +757,7 @@ def test_getArea(self): self.assertAlmostEqual(cur, ref) def test_thermallyExpands(self): - """Test that ARMI can thermally expands a rectangle. - - .. test:: Test that ARMI can thermally expands a rectangle - :id: T_REACTOR_THERMAL_EXPANSION_4 - :links: R_REACTOR_THERMAL_EXPANSION - """ + """Test that ARMI can thermally expands a rectangle.""" self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) def test_dimensionThermallyExpands(self): @@ -823,12 +798,7 @@ def test_getArea(self): self.assertAlmostEqual(cur, ref) def test_thermallyExpands(self): - """Test that ARMI can thermally expands a solid rectangle. - - .. test:: Test that ARMI can thermally expands a solid rectangle - :id: T_REACTOR_THERMAL_EXPANSION_5 - :links: R_REACTOR_THERMAL_EXPANSION - """ + """Test that ARMI can thermally expands a solid rectangle.""" self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) def test_dimensionThermallyExpands(self): @@ -886,12 +856,7 @@ def test_getArea(self): self.assertAlmostEqual(cur, ref) def test_thermallyExpands(self): - """Test that ARMI can thermally expands a square. - - .. test:: Test that ARMI can thermally expands a square - :id: T_REACTOR_THERMAL_EXPANSION_6 - :links: R_REACTOR_THERMAL_EXPANSION - """ + """Test that ARMI can thermally expands a square.""" self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) def test_dimensionThermallyExpands(self): @@ -951,12 +916,7 @@ def test_getVolume(self): self.assertAlmostEqual(cur, ref) def test_thermallyExpands(self): - """Test that ARMI can thermally expands a cube. - - .. test:: Test that ARMI can thermally expands a cube - :id: T_REACTOR_THERMAL_EXPANSION_7 - :links: R_REACTOR_THERMAL_EXPANSION - """ + """Test that ARMI can thermally expands a cube.""" self.assertFalse(self.component.THERMAL_EXPANSION_DIMS) @@ -990,12 +950,7 @@ def test_getArea(self): self.assertAlmostEqual(cur, ref) def test_thermallyExpands(self): - """Test that ARMI can thermally expands a hexagon. - - .. test:: Test that ARMI can thermally expands a hexagon - :id: T_REACTOR_THERMAL_EXPANSION_8 - :links: R_REACTOR_THERMAL_EXPANSION - """ + """Test that ARMI can thermally expands a hexagon.""" self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) def test_dimensionThermallyExpands(self): @@ -1054,12 +1009,7 @@ def test_getArea(self): self.assertAlmostEqual(cur, ref) def test_thermallyExpands(self): - """Test that ARMI can thermally expands a holed hexagon. - - .. test:: Test that ARMI can thermally expands a holed hexagon - :id: T_REACTOR_THERMAL_EXPANSION_9 - :links: R_REACTOR_THERMAL_EXPANSION - """ + """Test that ARMI can thermally expands a holed hexagon.""" self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) def test_dimensionThermallyExpands(self): @@ -1105,12 +1055,7 @@ def test_getArea(self): self.assertAlmostEqual(cur, ref) def test_thermallyExpands(self): - """Test that ARMI can thermally expands a holed hexagon. - - .. test:: Test that ARMI can thermally expands a holed hexagon - :id: T_REACTOR_THERMAL_EXPANSION_10 - :links: R_REACTOR_THERMAL_EXPANSION - """ + """Test that ARMI can thermally expands a holed hexagon.""" self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) def test_dimensionThermallyExpands(self): diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 14437fe71..9cb3a4972 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -371,12 +371,7 @@ def test_setB10VolOnCreation(self): ) def test_countFuelAxialBlocks(self): - """Tests that the users definition of fuel blocks is preserved. - - .. test:: Tests that the users definition of fuel blocks is preserved. - :id: T_REACTOR_2 - :links: R_REACTOR - """ + """Tests that the users definition of fuel blocks is preserved.""" numFuelBlocks = self.r.core.countFuelAxialBlocks() self.assertEqual(numFuelBlocks, 3) diff --git a/armi/utils/tests/test_utils.py b/armi/utils/tests/test_utils.py index ba795f15f..d5aac0314 100644 --- a/armi/utils/tests/test_utils.py +++ b/armi/utils/tests/test_utils.py @@ -137,15 +137,7 @@ def test_plotMatrix(self): utils.plotMatrix(matrix, fname, xticks=xtick, yticks=ytick) def test_classesInHierarchy(self): - """Tests the classesInHierarchy utility. - - .. test:: Tests that the Reactor is stored heirarchically - :id: T_REACTOR_HIERARCHY_0 - :links: R_REACTOR_HIERARCHY - - This test shows that the Blocks and Assemblies are stored - heirarchically inside the Core, which is inside the Reactor object. - """ + """Tests the classesInHierarchy utility.""" # load the test reactor _o, r = loadTestReactor() diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst index 4f8bbd735..3c0a391d0 100644 --- a/doc/release/0.2.rst +++ b/doc/release/0.2.rst @@ -10,6 +10,7 @@ What's new in ARMI ------------------ #. The ``_copyInputsHelper()`` gives relative path and not absolute after copy. (`PR#1416 `_) #. ARMI now mandates ``ruff`` linting. (`PR#1419 `_) +#. Removed all old ARMI requirements, to start the work fresh. (`PR#1438 `_) #. TBD Bug fixes diff --git a/doc/requirements/index.rst b/doc/requirements/index.rst deleted file mode 100644 index a7a85b5c1..000000000 --- a/doc/requirements/index.rst +++ /dev/null @@ -1,25 +0,0 @@ -############ -Requirements -############ - -.. note:: Work In Progress: The ARMI Requirements are thus-far incomplete. - -This section gives a general overview of the requirements of ARMI. These requirements -are meant to satisfy NRC regulations and also give a firm basis for the high-level -goals of ARMI. - -ARMI's requirements are tracked in this documentation and linked to specific -implementations in the source code or configuration. Ideally, each requirement will -be linked to both unit tests and source code within the ARMI repository. - -------------- - -.. toctree:: - :maxdepth: 2 - :numbered: - :glob: - - srsd - sdid - str - * diff --git a/doc/requirements/sdid.rst b/doc/requirements/sdid.rst deleted file mode 100644 index 20b884058..000000000 --- a/doc/requirements/sdid.rst +++ /dev/null @@ -1,223 +0,0 @@ -************************************************** -Software Design and Implementation Document (SDID) -************************************************** - - --------- -Overview --------- - -.. - TODO - -Settings Overview ------------------ - -The settings package is a long-time feature of ARMI. It was made to fill the niche of gathering user input on what should be simulated and what results should be generated. - -The main design principles for the settings system are: - -* Keep the user's experience with settings simple -* Remove setting definitions from code, centralize them, and facilitate definition change -* Don't break backwards compatibility -* Improve safety around settings errors - - -Keep the user's experience with settings simple -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -This is the principle design restraint for the settings package. The settings must be easy to use, human-readable, and not change too often. - - -Reactor Overview ----------------- - -.. - TODO - - ------- -Design ------- - -TODO - -Blueprints Design ------------------ - -The ``blueprints`` package makes extensive use of the ``yamlize`` library, which provides a mechanism for serializing and deserializing blueprints to and from YAML files. - - -Settings Design ---------------- - -Dictionary-like behavior -^^^^^^^^^^^^^^^^^^^^^^^^ -The settings system functions much like a Python dictionary for most of its interactions. - -The two objects to note in this interplay are the ``Settings`` object which is what the vast majority of interactions are directed at. Then there's the more hidden ``Setting`` object which corresponds to a single defined entity in the settings system, housed under a ``Settings.settings`` dictionary. - -When users perform lookups and value assignments to a key on the settings object, it hides a bit of the work behind the scenes of applying setting rules. - -The lookup fashioned as:: - - >>>Settings['mySettingName'] - 1 - -will only return the current value of the setting stored under ``mySettingName``, whereas:: - - >>>Settings.settings['mySettingName'] - - -returns the setting object itself. Only more complex relationships with coupled code tools will be concerned with -how the object itself behaves. A prime example of this is the GUI. - -Improve safety around settings errors -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -While simplicity remains a principle concern, some allotment for error prevention has to be allowed. A basic system that can be misused wildly is inherently more complex than one with a little overhead that works to keep nasty surprises at bay. - -The settings system is written in a defensive fashion, as it's the most frequent location for possible developer and user misuse. Given this, any method from accessing non-existent settings to trying to supply duplicate settings has been written with fail-fast behavior. - -Non-dynamic data typing -^^^^^^^^^^^^^^^^^^^^^^^ -One specific category of safety in the settings system stems from the nature of data variables in Python. - -Because ARMI is written in Python, there's a large issue of dynamic typing polluting setting values. There can easily be aberrant behavior introduced by a new kind of data being input to a value. - -For example it's not uncommon to have code flow based off setting values such as zero. If some developer makes a mistake and changes the datatype of a value zero from an integer to a string, it will break the utility of the code flow evaluation without alerting anyone to the change in behavior.:: - - def myFunction(var): - if var == 0: - doSomething() - else: - doSomethingSubtlyDifferent() - - myVar = 0 - myFunction(myVar) # doSomething() - - # oops! clumsy str() method used somewhere - myVar = "0" - myFunction(myVar) # doSomethingSubtlyDifferent() - -These kinds of hard to track mistakes were resolved by creating a more object oriented division of settings with type protection surrounding the setting's values, so it wouldn't be possible to set the example variable to a string. - -Now there are a finite set of allowable setting types which loosely mirror the Python primitive data types, namely: ``list``, ``float``, ``int``, ``str``, and ``bool``. - -As mutable objects are encompassed in the list of supported data types, it became important to prevent the perversion of their contained values with alterations that would bypass the protective methods on setting objects entirely. Such an example would be when a list is returned from the appropriate list setting object, and something is appended to it. This would bypass all error prevention methodology in place. The easiest fix for this was to return a deep copy of the contained value, and only reassign the contained value on an explicit value assignment statement:: - - # note 'cs' is the common term for 'case settings' - # referring to the settings system main shared object 'Settings'. - myList = cs['myListSetting'] - - myList.append('Some value not allowed by myListSetting!') # bad! - # if we stop the code here, the value in cs['myListSetting'] will not contain - # the bad value appended thanks to myList being a deep-copied value - -The customizability of settings -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Another error prevention tool is the customizability of individual settings. As mentioned previously, a complete understanding of the complex meaning involved in setting values can't be programmed. However, when the user creates a setting, they have the ability to control default values, valid ranges, and other basic sanity checking parameters. - -Each setting is intended to present a way of answering a question to the user from the system. For example, many settings ask questions like what external code engine to utilize for advanced calculations, or what temperature to apply to a particular component. These questions are not open-ended and as such usually have a set of rules surrounding their use like no temperatures below absolute zero, or only code engines specified by the following three strings are valid. - -The setting system is designed to be extensible, so developers may add further setting validation specific to the settings they add. - - -Reactor Design --------------- - -The physical hierarchy typical in a nuclear reactor is reflected in the design of the reactor package. -It uses a `Composite Design Pattern `_ to represent -part-whole hierarchies. In other words, a Reactor is typically made of Assemblies, which are made of Blocks, -which are made of Components, and so on. Requirements regarding the representation of a user-specified reactor -are satisfied by the objects in this hierarchy. - -At each level of the hierarchy, the state can be found as a state variable called a *Parameter*. The parameter -system is designed and implemented to satisfy the requirements related to storing and updating a dynamic state. - - -Spatial Arrangements -^^^^^^^^^^^^^^^^^^^^ - -The :py:mod:`grids module ` define where objects currently are in a regular, structured -grid. In particular, *Assemblies* sit in the 2-D grid on the reactor and *Blocks* sit in 1-D grids on Assemblies. - -Setting and getting state variables -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The *state* is stored in *parameters* at the ``block`` level and higher. The design and implementation of this subpackage is fully described in :py:mod:`armi.reactor.parameters`. - - -Averaging over children -^^^^^^^^^^^^^^^^^^^^^^^ - -Member objects of the ``reactor`` hierarchy have several capacities to average over their children. -This is useful for collecting information at levels necessary for meaningful analysis, for example -figuring out a core-averaged temperature or for homogenizing regions in preparation of neutronics models. - - -Computing Homogenized Number Densities -"""""""""""""""""""""""""""""""""""""" -Objects can compute homogenized number densities of each nuclide as required in many nuclear simulations (e.g. DIF3D). -The components contained in each block have heterogeneous compositions and dimensions that must be smeared into -a homogeneous block, as shown in Figure 1. - -To homogenize number densities, conservation of atoms is applied. Consider a a collection of :math:`I` components, each with -heterogeneous number density :math:`N_i` and volumes :math:`V_i`. The number of atoms in -component :math:`i` is :math:`N_i V_i`. Thus, to conserve this number of atoms over a -total volume :math:`V_b = \sum_i V_i`, we calculate the homogenized number density :math:`\bar{N_i}` -of component :math:`i` as - -.. math:: - :label: conserveAtoms - N_i V_i = \bar{N_i} V_b \\ - \bar{N_i} = \frac{N_i V_i}{V_b} -Thus, homogenized number densities are equal to heterogeneous number densities multiplied by the component volume -fraction. - -This calculation is performed in :py:meth:`armi.reactor.composites.ArmiObject.getNumberDensity`. - -.. figure:: /.static/block_homogenization.png - :align: center - - **Figure 1.** Homogenizing pins, duct, wire, cladding, and coolant into a uniform block - -Similarly, :py:meth:`~armi.reactor.composites.ArmiObject.getMass` can get the mass of some or all -nuclides in a structure and :py:meth:`~armi.reactor.composites.ArmiObject.getNumberOfAtoms` can get the number -of atoms. - -Calculation Of Volume Fractions -""""""""""""""""""""""""""""""" -To support the homogenization responsibility, the ``reactor`` package is responsible for computing the volume fractions -:math:`v_i` of each component. Generally, ``components`` are responsible for computing their own volume :math:`V_i`, and -other levels of the hierarchy simply have to evaluate the simple formula, - -.. math:: - :label: areaFraction - v_i = \frac{V_i}{\sum_j V_j} -.. WARNING:: - Often, components only compute their area and their height is inherited as the height of the - containing block. There are exceptions for more complex geometries. - -For user convenience, the dimensions of one component may be left undefined in input. If one and only one -component has undefined area, then the block will compute the area automatically. This is useful, for example, -when a complex shape exists for the coolant material between all pins. In this scenario, the maximum block -area is computed using the largest pitch :math:`p_{max}` (generally the interstitial gap). For hex geometry, the missing area :math:`A_{missing}` -is computed as: - -.. math:: - :label: missingArea - A_{missing} = p_{max}^2 \frac{\sqrt{3}}{2} - \sum_{i \neq missing}{A_i} - -Hot and input dimensions -^^^^^^^^^^^^^^^^^^^^^^^^ -ARMI treats dimensions and material properties as functions of temperature. However, a pure physical analogy is challenging for several reasons. These reasons and the implementation details are explained here. - -For a typical ``component``, users may define most dimensions at any temperature they desire (the *Input temperature*), as explained in :doc:`/user/inputs/blueprints`. These dimensions will be thermally-expanded up to the *Hot temperature* as input. For most shapes and components, this works as expected. However, in Hex geometries the outer hexagonal boundary is currently limited to be consistent across all assemblies in a core. This stems from some physics solver requirements of structured meshes. Users should set the hot dimension on input. Models that change pitch as functions of grid-plate and load pad temperatures may be developed in the future. - -**Component** dimensions are stored as *parameters* at the input temperature and thermally expanded to the current temperature of the component upon access. To run a case at a specific temperature, the user should set the hot and input temperatures to the same value. This can be used to study isothermal conditions during outages and startup. - -------------------- -Requirements Review -------------------- - -.. - TODO diff --git a/doc/requirements/srsd.rst b/doc/requirements/srsd.rst deleted file mode 100644 index e86adacb2..000000000 --- a/doc/requirements/srsd.rst +++ /dev/null @@ -1,541 +0,0 @@ -************************************************** -Software Requirement Specification Document (SRSD) -************************************************** - - ---------------- -Business Impact ---------------- - -#. The ``database`` package is used to restart runs, analyze results, and determine when changes are introduced to otherwise identical cases in ARMI. The ``database`` package is considered high risk. -#. The ``report`` package is one of many tools available for viewing case details and is intended purely for developer or analyst feedback on run specifications and results, not as a means of altering the run. Thus the ``report`` package is low risk. -#. The ``blueprints`` package interprets user input into an ARMI reactor model. If done incorrectly, ARMI simulations would be unreliable and inaccurate. ARMI is used for informing core designs, engineering calculations, and safety bases; therefore, the ``bluperints`` package is considered high risk. -#. The ``settings`` package contains a substantive amount of the run's definition. Problems in the settings system can invalidate runs. Fortunately, the errors are easily traced and replicated as the system itself is not complicated. Therefore, the ``settings`` package is considered high risk. -#. The ``operator`` package is paramount to every facility the code offers, affecting every aspect of design. Thus, the ``operator`` packages is high risk. -#. The ``fissionProductModel`` package has substantial impact on the results of the neutronic calculations that affect the plant design. Thus, it falls under the **high risk** impact level. -#. The ``reactor`` package contains the majority of state information throughout a case. Issues in it -could propagate and invalidate results derived from ARMI to perform design and analysis. Therefore, the ``reactor`` package is considered high risk. -#. The ``nucDirectory`` package contains nuclide-level information including names, weights, symbols, decay-chain, and transmutation information. This information is used for converting mass fractions to number densities, and identifying specific nuclides to be used in used in performing flux and burn-up calculations. Therefore, the ``nucDirectory`` package is considered high risk. -#. The ``nuclearDataIO`` package is used to read and write nuclear data files which are used as input to global flux solvers, and eventually input into safety calculations via reactivity coefficients. Therefore, the ``nuclearDataIO`` package is considered high risk. - - --------------------- -Applicable Documents --------------------- - -.. - TODO: Do this by topic - - ------------------------ -Functional Requirements ------------------------ - -.. req:: The database shall maintain fidelity of data. - :id: R_DB_FIDELITY - :status: needs implementation, needs test - -The database shall faithfully represent the possessed information and not alter its contents, retrieving the data exactly as input. - -.. req:: The database shall allow case restarts. - :id: R_DB_RESTARTS - :status: needs implementation, needs test - -The state information representing as near as possible the entirety of the run when the database write was executed, shall be retrievable to restore the previous case to a particular point in time for further use by analysts. - -.. req:: The database shall accept all pythonic primitive data types. - :id: R_DB_PRIMITIVES - :status: needs implementation, needs test - -Given the ubiquity of Python's ``None`` the database shall support its inclusion as a valid entry for data. There will be no support for any abstract data type beyond None. - -.. req:: The report package shall maintain data fidelity. - :id: R_REPORT_FIDELITY - :status: implemented, needs more tests - -The report package shall not modify or subvert data integrity as it reports the information out to the user. - - -.. - TODO: blueprints need some interface and I/O reqs - -.. req:: The settings package shall not accept ambiguous setting definitions. - :id: R_SETTINGS_UNAMBIGUOUS - :status: implemented, needs more tests - -Settings defined in the system must have both the intended data type and default value defined, or it is considered incomplete and therefore invalid. Additionally the system shall not accept multiple definitions of the same name. - -TODO: This may be tested by a unit test loading in duplicate setting definitions or cases where a definition does not provide adequate details. - - .. req:: Settings shall have unique, case-insensitive names. - :id: R_SETTINGS_UNAMBIGUOUS_NAME - :status: implemented, needs more tests - - No two settings may share names. - - TODO: This may be tested by a unit test loading two similar names - - .. req:: Settings shall not allow dynamic typing. - :id: R_SETTINGS_UNAMBIGUOUS_TYPE - :status: implemented, needs more tests - - Settings shall exist exclusively as a well-defined data type, as chosen by the setting definition. - - TODO: This may be tested by unit tests attempting to subvert the contained data type. - - .. req:: The settings package shall contain a default state of all settings. - :id: R_SETTINGS_DEFAULTS - :status: implemented, needs more tests - - Many of the settings will not be altered by the user of a run, and there will likely be too many for a user to deal with on an individual basis. Therefore, most settings will need to function sensibly with their default value. This default value shall always be accessible throughout the runs life cycle. - - TODO: This may be tested by unit tests loading and checking values on each setting. - -.. req:: Settings shall support more complex rule association to further customize each setting's behavior. - :id: R_SETTINGS_RULES - :status: implemented, needs more tests - -It shall be possible to support a valid list or range of values for any given setting. - -TODO: This may be tested by a unit test attempting to set a value outside a given min/max range. - -.. req:: Setting addition, renaming, and removal shall be supported.setting's behavior. - :id: R_SETTINGS_CHANGES - :status: implemented, needs more tests - -The setting package shall accomodate the introduction of new settings, renaming of old settings, and support the complex deprecation behaviors of settings. - -TODO: This may be tested by a unit test containing removed settings references in both input and code references, as well as an additional definition load and use - -.. req:: The settings package shall support version tracking. - :id: R_SETTINGS_VERSION - :status: implemented, needs more tests - -Each settings file is only genuinely valid with the version of ARMI that generated the file, as settings might change between versions. As a safegaurd of this, the settings system shall alert the user if the version of the settings file does not match the version of ARMI in-memory. - -TODO: This may be tested by unit tests with out of date or omitted version information - -.. req:: The settings system shall raise an error if the same setting is created twice. - :id: R_SETTINGS_DUPLICATES - :status: implemented, needs more tests - -When a user creates a setting twice, it shall be detected as an error which is raised to the user. - -TODO: This may be tested by unit tests loading and checking settings that have a setting created twice, and failing. - -.. req:: ARMI shall be able to represent a user-specified reactor. - :id: R_REACTOR - :status: implemented, needs more tests - - Given user input describing a reactor, ARMI shall construct with equivalent fidelity a software model of the reactor. In particular, ARMI shall appropriately represent the shape, arrangement, connectivity, dimensions, materials (including thermo-mechanical properties), isotopic composition, and temperatures of the reactor. - - .. req:: ARMI shall represent the reactor hierarchically. - :id: R_REACTOR_HIERARCHY - :status: completed - - To maintain consistency with the physical reactor being modeled, ARMI shall maintain a hierarchical definition of its components. For example, all the fuel pins in a single fuel assembly in a solid-fuel reactor shall be collected such that they can be queried or modified as a unit as well as individuals. - - .. req:: ARMI shall automatically handle thermal expansion. - :id: R_REACTOR_THERMAL_EXPANSION - :status: completed - - ARMI shall automatically compute and applied thermal expansion and contraction - of materials. - - .. req:: ARMI shall support a reasonable set of basic shapes. - :id: R_REACTOR_SHAPES - :status: implemented, needs more tests - - ARMI shall support the following basic shapes: Hexagonal prism (ducts in fast reactors), rectangular prism (ducts in thermal reactors), cylindrical prism (fuel pins, cladding, etc.), and helix (wire wrap). - - .. req:: ARMI shall support a number of structured mesh options. - :id: R_REACTOR_MESH - :status: completed - - ARMI shall support regular, repeating meshes in hexagonal, radial-zeta-theta (RZT), and Cartesian structures. - - .. req:: ARMI shall support the specification of symmetry options and boundary conditions. - :id: R_REACTOR_4 - :status: implemented, need impl/test - - ARMI shall support symmetric models including 1/4, 1/8 core models for Cartesian meshes and 1/3 and full core for Hex meshes. For Cartesian 1/8 core symmetry, the core axial symmetry plane (midplane) will be located at the top of the reactor. - - .. req:: ARMI shall check for basic correctness. - :id: R_REACTOR_5 - :status: implemented, need impl/test - - ARMI shall check its input for certain obvious errors including unphysical densities and proper fit. - - .. req:: ARMI shall allow for the definition of limited one-dimensional translation paths. - :id: R_REACTOR_6 - :status: implemented, need impl/test - - ARMI shall allow the user specification of translation pathways for certain objects to - follow, to support moving control mechanisms. - - .. req:: ARMI shall allow the definition of fuel management operations (i.e. shuffling) - :id: R_REACTOR_7 - :status: implemented, need impl/test - - ARMI shall allow for the modeling of a reactor over multiple cycles. - -.. req:: ARMI shall represent and reflect the evolving state of a reactor. - :id: R_EVOLVE - :status: needs implementation, needs test - - The reactor state shale be made available to users and plugins, which may in turn modify the state. ARMI shall fully define how all aspects of state may be accessed and modified and shall reflect any new state after it is applied. - - The reactor state shall be represented as evolving either through time (i.e. in a typical cycle-by-cycle analysis) or through a series of control configurations. - -.. req:: The operator package shall provide a means by which to communicate inputs and results between analysis plugins. - :id: R_OPERATOR_IO - :status: needs implementation, needs test - -The operator package shall receive output from calculation Plugins and store the results on a well-defined central model. A composite pattern shall be used, with a Reactor containing Assemblies containing Blocks, etc. - -.. req:: The operator package shall provide a means to perform computations in parallel on a high performance computer. - :id: R_OPERATOR_PARALLEL - :status: needs implementation, needs test - -Many analysis tasks require high performance computing (HPC), and the operator package shall contain utilities and routines to communicate with an HPC and to facilitate execution of simulations in parallel. - -.. req:: The operator package shall allow physics coupling between analysis plugins. - :id: R_OPERATOR_COUPLING - :status: implemented, needs more tests - -For coupled physics (e.g. neutronics depends on thermal hydraulics depends on neutronics), the operator package shall allow loose and/or tight coupling. Loose coupling is using the values from the previous timestep to update the next timestep. Tight is an operator-splitting iteration until convergence between one or more plugins. - -.. req:: The operator package shall allow analysis plugins to be replaced without affecting interfaces in other plugins. - :id: R_OPERATOR_ANALYSIS - :status: needs implementation, needs test - -Often, a plugin is replaced with a new plugin fulfilling some new requirement. When this happens, the operator package shall isolate required changes to the new plugin. For example, if a fuel performance plugin needs temperatures but the temperature-computing plugin is replaced, the fuel performance plugin should require no changes to work with the drop-in replacement. This requires modular design and standardization in state names. - -.. req:: The operator package shall coordinate calls to the various plugins. - :id: R_OPERATOR_COORD - :status: needs implementation, needs test - -Based on user settings, the ordering, initialization, and calls to other plugins shall be coordinated by the operator package. The operator package must therefore be aware of dependencies of each plugin. - -.. req:: The latticePhysics package will execute the lattice physics code in a parallel or serial fashion depending on the mode. - :id: R_LATTICE_EXE - :status: needs implementation, needs test - -.. req:: The nucDirectory package shall contain basic nuclide information for a wide range of nuclides. - :id: R_NUCDIR_INFO - :status: needs implementation, needs test - -The nucDirectory package shall contain the following general information for each nuclide: - -- name -- symbol -- natural isotopic abundance of elements -- atomic number (Z) -- mass number (A) -- atomic weight -- meta stable state - -.. req:: The nucDirectory package shall store data separately from code. - :id: R_NUCDIR_FILES - :status: needs implementation, needs test - -The software shall be made flexible such that the definition of specific nuclides available (i.e. those used in a version of MCC), can be updated without modifying the code. - -TODO: This can be tested by inspecting the logic of the code to retrieve data from a resource file, or by modifying the resource file to create an expected outcome. - -.. req:: The nucDirectory package shall enforce unique nuclide names. - :id: R_NUCDIR_UNIQUE - :status: needs implementation, needs test - -The nuclides names shall be unique, and consist of the nuclide's symbol, mass number, and an indication if it is in a meta-stable state. Elemental nuclides shall omit the mass number, since they represent more than a single mass number. Lumped nuclides shall also have unqiue, user-data identified names. - -TODO: A unit test can be used to demonstrate that all nuclide names are unique. - -.. req:: The nucDirectory package shall be capable of generating unique 4-character labels. - :id: R_NUCDIR_LABELS - :status: needs implementation, needs test - -Versions 2 and 3 of MCC allow for unique 6 character labels to be used to reference nuclides. Two characters need to be used to describe the different cross section sets used by the problem. Therefore, every nuclide in ARMI needs to have a unique 4 character representation to use in MCC and the downstream global flux solver. - -TODO: A unit test can be used to demonstrate that all nuclides have unique 4-character labels. - -.. req:: The nucDirectory package shall allow for use of lumped nuclides. - :id: R_NUCDIR_LUMPED - :status: needs implementation, needs test - -Lumped nuclides are bulk defined nuclides that are typically used when modeling fission products. Lumping the nuclides during burnup calculations lowers the problem size without having a significant impact on the results. Consequently, they do not always need to be modeled individually, but can be grouped. - -TODO: A unit test can be used to demonstrate that lumped nuclides can be used and created. - -.. req:: The nucDirectory package shall allow for elemental nuclides. - :id: R_NUCDIR_ELEMENTALS - :status: needs implementation, needs test - -The nuclear data libraries available in versions 2 and 3 of MCC do not always allow for nuclide input, and some materials are grouped into elemental nuclides. Iron is an example of this in MCC version 2. Consequently, ARMI needs to be able to model elemental nuclides which represent the entire element, as well as the individual nuclides. - -.. req:: The nucDirectory package shall allow for dummy nuclides. - :id: R_NUCDIR_DUMMY - :status: needs implementation, needs test - -Dummy nuclides, typically written in all capitals as "DUMMY", are used to truncate the burn chain in order to reduce the problem size without compromising the results. - -.. req:: The nucDirectory package shall allow for indexing of nuclide information. - :id: R_NUCDIR_INDEX - :status: needs implementation, needs test - -The nuclear data files created by physics codes such as MCC and DIF3D may not necessary correspond to the name used within ARMI, it will be necessary to load nuclide information based on a non-ARMI name. The software shall provide lookup mechanisms for nuclide objects based on: - -- Name -- 4-character label -- MCC versions 2 and 3 IDs - -.. req:: The nucDirectory package shall contain decay chain data. - :id: R_NUCDIR_DECAY_CHAIN - :status: needs implementation, needs test - -The decay chain is an important step in performing burn-up calculations. The nucDirectory shall contain necessary decay mechanisms: - -- `\beta^-` -- `\beta^+` -- `\alpha` -- Electron capture -- Spontaneous fission - -The nucDirectory shall contain the half-life, decay mode(s) with corresponding branch ratio(s) and daughter nuclide(s) of each decay mode being modeled. Since it is possible for the user to define specific nuclides to be modeled, the nucDirectory shall allow for use of different daughter nuclides. - -TODO: A unit test can be generated to test that the correct decay chain is present, and that the data matches other resources. - -.. req:: The nucDirectory package shall contain transmutation data. - :id: R_NUCDIR_TRANSMUTE - :status: needs implementation, needs test - -In addition to the decay chain, nuclides may transmute through interactions into other nuclides. The nucDirectory shall contain transmutations including: - -- `n,2n` -- `n,p` -- `n,t` -- `n,\fission` -- `n,\gamma` -- `n,\alpha` - -The nucDirectory shall contain the transmutation mechanism, branch ratio, and product nuclides of each transmutation being modeled. The nucDirectory shall not contain the cross sections, as these are calculated using lattice physics codes, such as MCC. Since it is possible for the user to define specific nuclides to be modeled, the nucDirectory shall allow optional daughter nuclides. - -TODO: A unit test can be generated to test that the correct transmutations are present, and corresponding data matches other resources. - -.. req:: The nucDirectory package shall warn the user if there are potential burn-chain faults. - :id: R_NUCDIR_BURN_CHAIN - :status: needs implementation, needs test - -The user supplies the nuclides to be modeled in the simulation; therefore, it is possible that the user may inadvertently describe a burn-chain that is not complete. The software shall be capable of detecting erroneous user input and terminate the program. - -TODO: A unit test can be generated with faulty decay chains to determine that they do not work. - -.. req:: The nuclearDataIO package shall read and write ISOTXS files. - :id: R_NUCDATA_ISOTXS - :status: needs implementation, needs test - -ISOTXS files contain the multi-group microscopic cross sections, and other nuclear data, for each nuclide being modeled. The multi-group cross sections are used throughout ARMI. - -The software shall be capable of reading an ISOTXS file into memory, and writing it out to a file that is exactly the same as the original. - -TODO: A unit test can be created with reads an ISOTXS file generated by MCC, and then writes out the file to another name. The two files can then be compared using a binary file comparison to demonstrate that the contents of the files are identical. - -.. req:: The nuclearDataIO package shall read and write GAMISO files. - :id: R_NUCDATA_GAMISO - :status: needs implementation, needs test - -GAMISO files are generated by MCC-v3, and are the same format as an ISOTXS file. The file contains photon interaction cross sections instead of neutron cross sections. - -The software shall be capable of reading a GAMISO file into memory, and writing it out into a file that is exactly the same as the original. - -TODO: This can be covered in a unit test; the unit test can be the same as described for ISOTXS files. - -.. req:: The nuclearDataIO package shall read and write PMATRX files. - :id: R_NUCDATA_PMATRX - :status: needs implementation, needs test - -PMATRX files contain the gamma production matrix resulting from fission or capture events. Given a neutron flux distribution, and a PMATRX file, the gamma source can be computed and then used to determine gamma transport and heating. - -.. req:: The nuclearDataIO package shall be capable of reading a PMATRX file into memory, and writing it out into a file that is exactly the same as the original. - -TODO: This can be covered in a unit test; the unit test can be the same as described for ISOTXS files. - -.. req:: The nuclearDataIO package shall read and write DLAYXS files -DLAYXS files contain delayed neutron data, such as precursor decay constants and number of neutrons emitted, :math:`\nu_{\mathrm{delay}}`. The DLAYXS data is used to calculate :math:`\beta_{\rm{eff}}`, which is used to calculate reactivity coefficients, and consequently in AOO and accident simulations. - -The software shall be capable of reading a DLAYXS file into memory, and writing it out into a file that is exactly the same as the original. - -TODO: This can be covered in a unit test; the unit test can be the same as described for ISOTXS files. - -.. req:: The nuclearDataIO package shall merge files of the same type. - :id: R_NUCDATA_MERGE - :status: needs implementation, needs test - -The software shall be capable merging multiple files of the same type (ISOTXS, PMATRX, etc.) into a single file meeting the specifications. The software shall fail with a descriptive error message if any two nuclides have the same name. - -This can be covered in a unit test which runs 3 MCC-v3 cases. - -1. Generate cross sections for a set of nuclides with the xsID=AA 1. Generate cross sections for a set of nuclides with the xsID=AB 1. Generate cross sections with two regions using an input file containing the nuclides of the above two cases. - -The third MCC-v3 case will produce a merged ISOTXS file which can be compared to an ISOTXS file generated by merging the output ISOTXS from cases 1 and 2. - -.. req:: The nuclearDataIO package shall make the data programmatically available. - :id: R_NUCDATA_AVAIL - :status: needs implementation, needs test - -The software shall make the nuclear data provided in ISOTXS, GAMISO, PMATRX and DLAYXS available in the form of Python objects, such that it can be used elsewhere in the code, such as in the depletion, nuclear uncertainty quantification, and `\beta` calculations. - - .. req:: The nuclearDataIO package shall key nuclear data based on nuclide label and xsID. - :id: R_NUCDATA_AVAIL_LABEL - :status: needs implementation, needs test - -When nuclear data files are read, they should be made available in a container object, such as a dictionary, and keyed on the nuclide label (a unique four character nuclide identifier) and the cross section ID, a two character identifier for block type and burnup group. - -TODO: This can be covered by a unit test which reads an ISOTXS into a container object, and then obtaining cross sections by using the nuclide label and xsID. - - .. req:: The nuclearDataIO package shall be able to remove nuclides from specifc nuclear data files. - :id: R_NUCDATA_AVAIL_FILES - :status: needs implementation, needs test - -ARMI has a concept of "lumped fission products" that result in more nuclides being in ISOTXS, GAMISO, and PMATRX files than are needed for subsequent calculations. The software shall be capable of removing the unused nuclides from ISOTXS, GAMISO, and PMATRX files. This generally does not apply to DLAYXS files, because they typically only contain nuclides that fission. - -TODO: This can be covered by a unit test where a file is read in, a nuclide removed, and then rewritten and reread. The reread file should not contain the removed nuclides. - - .. req:: The nuclearDataIO package shall be able to modify the nuclear data. - :id: R_NUCDATA_AVAIL_MODIFY - :status: needs implementation, needs test - -In order to calculate the uncertainties of our methodology introduced by nuclear data uncertainty, it is necessary to be able to perturb (i.e. modify) specific values within the nuclear data files. - -TODO: This can be covered by a unit test where a file is read in, a cross section or relevant piece of data modified, and then rewritten and reread. The reread file should contain the modified data. - ------------------------- -Performance Requirements ------------------------- - -.. req:: The database representation on disk shall be smaller than the the in-memory Python representation - :id: R_DB_PERFORMANCE - :status: needs implementation, needs test - -The database implementation shall use lossless compression to reduce the database size. - -.. req:: The report package shall present no burden. - :id: R_REPORT_PERFORMANCE - :status: needs implementation, needs test - -As the report package is a lightweight interface to write data out to a text based format, and render a few images, the performance costs are entirely negligible and should not burden the run, nor the user's computer in both memory and processor time. - -.. req:: The reactor package shall allow rapid synchronization of state across the network to parallel processors. - :id: R_REACTOR_PARALLEL - :status: needs implementation, needs test - -For performance, many physics calculations are done in parallel. The reactor must be able to synchronize -the state on multiple processors efficiently. - -.. req:: The nucDirectory package shall try to prevent data duplication to limit the memory footprint of this information. - :id: R_NUCDIR_DUPLICATION - :status: needs implementation, needs test - -TODO: Is this testable? - - -------------------- -Software Attributes -------------------- - -.. req:: ARMI shall generally support at least one modern Windows and one modern CentOS operating system version. - :id: R_OSS - :status: needs implementation, needs test - -.. req:: The database produced shall be easily accessible in a variety of programming environments beyond Python. - :id: R_DB_LANGUAGE - :status: needs implementation, needs test - -.. req:: The settings package shall use human-readable, plain-text files as input. - :id: R_SETTINGS_READABLE - :status: implemented, needs more tests - -The user must be able to read and edit their settings file as plain text in broadly any typical text editor. - - ---------------------------- -Software Design Constraints ---------------------------- - -.. req:: The report package shall not burden new developers with grasping a complex system. - :id: R_REPORT_TECH - :status: needs implementation, needs test - -Given the functional requirements of the report package, new developers should be able to understand how to contribute to a report nigh instantly. No new technologies should be introduced to the system as HTML and ASCII are both purely text-based. - -.. req:: The reactor package shall not exhibit any stochastic behavior. - :id: R_REACTOR_STOCHASTIC - :status: needs implementation, needs test - -Any two ARMI runs with the same input file must produce the same results. - -.. req:: The nucDirectory package shall use nuclear data that is contained within the ARMI code base. - :id: R_NUCDIR_DATA - :status: needs implementation, needs test - -The nucDirectory package shall not use data data retrieved from online sources. The intent here is to prevent inadvertent security risks. - -.. req:: The nucDirectory package shall follow a particular naming convention. - :id: R_NUCDIR_NAMING - :status: needs implementation, needs test - -Other physics codes use the name Am-242 for the metastable state of Am-242, and use Am-242g for the ground state. - - --------------------------- -Interface I/O Requirements --------------------------- - -.. - TODO: blueprints need some interface and I/O reqs - -.. req:: The setting system shall render a view of every defined setting as well as the key attributes associated with it - :id: R_SETTINGS_REPORT - :status: implemented, needs more tests - -Utilizing the documentation of the ARMI project the settings system shall contribute a page containing a table summary of the settings included in the system. - -TODO: This is completed by the :doc:`Settings Report `. - -.. req:: The latticePhysics package will write input files for the desired code for each representative block to be modeled. - :id: R_LATTICE_INPUTS - :status: needs implementation, needs test - -.. req:: The latticePhysics package will use the output(s) to create a reactor library, ``ISOTXS`` or ``COMPXS``, used in the global flux solution. - :id: R_LATTICE_OUTPUTS - :status: needs implementation, needs test - -.. req:: The reactor package shall check input for basic correctness. - :id: R_REACTOR_CORRECTNESS - :status: needs implementation, needs test - -The reactor package shall check its inputs for certain obvious errors including unphysical quantities. At a deep level, the reactor package will not attempt to fully validate subtle engineering aspects of the reactor; that is more generally the reason users will want to fully simulate a reactor and cannot be done at input time. - - --------------------- -Testing Requirements --------------------- - -.. - TODO: Do this by topic - - --------------------------- -Open-Items and Assumptions --------------------------- - -.. - TODO: Do this by topic - - ----------- -Appendices ----------- - -.. - TODO diff --git a/doc/requirements/str.rst b/doc/requirements/str.rst deleted file mode 100644 index 61b8a896f..000000000 --- a/doc/requirements/str.rst +++ /dev/null @@ -1,133 +0,0 @@ -************************** -Software Test Report (STR) -************************** - - ------------- -Introduction ------------- - -.. - TODO - - ----------------------------------- -Overview, Assumptions, and Results ----------------------------------- - -.. - TODO - - -ARMI Version tested -------------------- - -.. - TODO - - -Computer/platform hardware --------------------------- - -.. - TODO - - -Test equipment and calibration ------------------------------- - -.. - TODO - - -Runtime Environment -------------------- - -.. - TODO - - -Date of the test ----------------- - -.. - TODO - - -Tester ------- - -.. - TODO - - -Input and Output Data ---------------------- - -.. - TODO - - -Simulation model used ---------------------- - -.. - TODO - - -Tests, Requirements, and Implementation ---------------------------------------- - -.. - TODO - - -Results and applicablility --------------------------- - -.. - TODO - - -Action taken in connection with any deviations noted ----------------------------------------------------- - -.. - TODO - - -Acceptabiltiy -------------- - -.. - TODO - - --------------------------------- -Verification and Validation Plan --------------------------------- - -.. - TODO - - -Verification Plan ------------------ - -.. - TODO - - -Validation Plan ---------------- - -.. - TODO - - ---------------- -Test Automation ---------------- - -.. - TODO \ No newline at end of file From 85d4f6e8af38f1901c1a20306d6b1d6b0c891ba6 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 19 Oct 2023 08:07:26 -0700 Subject: [PATCH 023/176] Cleaning up material modification table (#1441) --- doc/user/inputs/blueprints.rst | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/doc/user/inputs/blueprints.rst b/doc/user/inputs/blueprints.rst index 779df8bd7..810682c36 100644 --- a/doc/user/inputs/blueprints.rst +++ b/doc/user/inputs/blueprints.rst @@ -395,22 +395,35 @@ material modifications from tabulate import tabulate data = [] - for m in Material.__subclasses__(): numArgs = m.applyInputParams.__code__.co_argcount if numArgs > 1: modNames = m.applyInputParams.__code__.co_varnames[1:numArgs] - data.append((m.__name__, ', '.join(modNames))) + data.append((m.__name__, ", ".join(modNames))) for subM in m.__subclasses__(): num = subM.applyInputParams.__code__.co_argcount if num > 1: mods = subM.applyInputParams.__code__.co_varnames[1:num] - data.append((subM.__name__, ', '.join(mods))) - + if numArgs > 1: + mods += modNames + data.append((subM.__name__, ", ".join(mods))) + + d = {} + for k, v in data: + if k not in d: + d[k] = v + else: + d[k] = d[k].split(",") + v.split(",") + d[k] = sorted(set([vv.strip() for vv in d[k]])) + d[k] = ", ".join(d[k]) + data = [(k, v) for k, v in d.items()] data.sort(key=lambda t: t[0]) - return tabulate(headers=('Material Name', 'Available Modifications'), - tabular_data=data, tablefmt='rst') + return tabulate( + headers=("Material Name", "Available Modifications"), + tabular_data=data, + tablefmt="rst", + ) The class 1/class 2 modifications in fuel materials are used to identify mixtures of custom isotopics labels for input scenarios where a varying blend of a high-reactivity From d3af5deb01eb8f05ebe0129aea50121e4b2cfe3e Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 19 Oct 2023 08:59:14 -0700 Subject: [PATCH 024/176] Cleanup of DB migration scripts (#1433) - I removed two scripts: crossSectionBlueprintsToSettings.py and m0_1_0_newDbFormat.py. - I also moved the directory armi/scripts/migration/ to armi/migration/, - I added some code coverage. --- armi/cli/migrateInputs.py | 2 +- armi/{scripts => }/migration/__init__.py | 10 +- armi/{scripts => }/migration/base.py | 15 +- armi/{scripts => }/migration/m0_1_3.py | 4 +- .../m0_1_6.py} | 2 +- .../{scripts => }/migration/tests/__init__.py | 0 .../tests/test_m0_1_6.py} | 4 +- armi/migration/tests/test_migration_base.py | 42 ++++ armi/physics/constants.py | 37 +--- armi/physics/neutronics/settings.py | 17 -- .../neutronics/tests/test_neutronicsPlugin.py | 2 +- armi/reactor/blueprints/__init__.py | 9 +- armi/scripts/__init__.py | 15 -- .../crossSectionBlueprintsToSettings.py | 171 ----------------- armi/scripts/migration/m0_1_0_newDbFormat.py | 179 ------------------ 15 files changed, 58 insertions(+), 451 deletions(-) rename armi/{scripts => }/migration/__init__.py (84%) rename armi/{scripts => }/migration/base.py (89%) rename armi/{scripts => }/migration/m0_1_3.py (97%) rename armi/{scripts/migration/m0_1_6_locationLabels.py => migration/m0_1_6.py} (98%) rename armi/{scripts => }/migration/tests/__init__.py (100%) rename armi/{scripts/migration/tests/test_m0_1_6_locationLabels.py => migration/tests/test_m0_1_6.py} (93%) create mode 100644 armi/migration/tests/test_migration_base.py delete mode 100644 armi/scripts/__init__.py delete mode 100644 armi/scripts/migration/crossSectionBlueprintsToSettings.py delete mode 100644 armi/scripts/migration/m0_1_0_newDbFormat.py diff --git a/armi/cli/migrateInputs.py b/armi/cli/migrateInputs.py index 9e0651c1e..6355f41ae 100644 --- a/armi/cli/migrateInputs.py +++ b/armi/cli/migrateInputs.py @@ -17,7 +17,7 @@ import os from armi.cli.entryPoint import EntryPoint -from armi.scripts.migration import ACTIVE_MIGRATIONS, base +from armi.migration import ACTIVE_MIGRATIONS, base from armi.utils import directoryChangers diff --git a/armi/scripts/migration/__init__.py b/armi/migration/__init__.py similarity index 84% rename from armi/scripts/migration/__init__.py rename to armi/migration/__init__.py index 9ec4fa366..003cdfb88 100644 --- a/armi/scripts/migration/__init__.py +++ b/armi/migration/__init__.py @@ -32,17 +32,13 @@ like happens in mainstream applications like word processors and spreadsheets. """ -from armi.scripts.migration import ( +from armi.migration import ( m0_1_3, - m0_1_0_newDbFormat, - crossSectionBlueprintsToSettings, - m0_1_6_locationLabels, + m0_1_6, ) ACTIVE_MIGRATIONS = [ - m0_1_0_newDbFormat.ConvertDB2toDB3, m0_1_3.RemoveCentersFromBlueprints, m0_1_3.UpdateElementalNuclides, - crossSectionBlueprintsToSettings.MoveCrossSectionsFromBlueprints, - m0_1_6_locationLabels.ConvertAlphanumLocationSettingsToNum, + m0_1_6.ConvertAlphanumLocationSettingsToNum, ] diff --git a/armi/scripts/migration/base.py b/armi/migration/base.py similarity index 89% rename from armi/scripts/migration/base.py rename to armi/migration/base.py index 7568f4514..a62cb94a6 100644 --- a/armi/scripts/migration/base.py +++ b/armi/migration/base.py @@ -72,7 +72,7 @@ def _loadStreamFromPath(self): The operative subclasses implementing this method are below. """ if not os.path.exists(self.path): - raise ValueError("File {} does not exist".format(self.path)) + raise ValueError(f"File {self.path} does not exist") def _applyToStream(self): """Add actual migration code here in a subclass.""" @@ -88,8 +88,7 @@ def _writeNewFile(self, newStream): while os.path.exists(self.path): # don't overwrite files (could be blueprints) name, ext = os.path.splitext(self.path) - name += f"{i}" - self.path = name + ext + self.path = name + f"{i}" + ext i += 1 with open(self.path, "w") as f: @@ -116,16 +115,6 @@ def _loadStreamFromPath(self): self.stream = open(self.path) -class GeomMigration(Migration): - """Migration for non-blueprints geometry input.""" - - def _loadStreamFromPath(self): - Migration._loadStreamFromPath(self) - cs = caseSettings.Settings(fName=self.path) - self.path = cs["geomFile"] - self.stream = open(self.path) - - class DatabaseMigration(Migration): """Migration for db output.""" diff --git a/armi/scripts/migration/m0_1_3.py b/armi/migration/m0_1_3.py similarity index 97% rename from armi/scripts/migration/m0_1_3.py rename to armi/migration/m0_1_3.py index 575265b63..4e7f4ed49 100644 --- a/armi/scripts/migration/m0_1_3.py +++ b/armi/migration/m0_1_3.py @@ -12,10 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. """Cleans up blueprints.""" -import re import io +import re -from armi.scripts.migration.base import BlueprintsMigration +from armi.migration.base import BlueprintsMigration from armi import runLog diff --git a/armi/scripts/migration/m0_1_6_locationLabels.py b/armi/migration/m0_1_6.py similarity index 98% rename from armi/scripts/migration/m0_1_6_locationLabels.py rename to armi/migration/m0_1_6.py index 55d143d53..371dd2d65 100644 --- a/armi/scripts/migration/m0_1_6_locationLabels.py +++ b/armi/migration/m0_1_6.py @@ -18,7 +18,7 @@ from armi import runLog from armi.settings import caseSettings -from armi.scripts.migration.base import SettingsMigration +from armi.migration.base import SettingsMigration from armi.settings import settingsIO from armi.utils.units import ASCII_LETTER_A, ASCII_ZERO diff --git a/armi/scripts/migration/tests/__init__.py b/armi/migration/tests/__init__.py similarity index 100% rename from armi/scripts/migration/tests/__init__.py rename to armi/migration/tests/__init__.py diff --git a/armi/scripts/migration/tests/test_m0_1_6_locationLabels.py b/armi/migration/tests/test_m0_1_6.py similarity index 93% rename from armi/scripts/migration/tests/test_m0_1_6_locationLabels.py rename to armi/migration/tests/test_m0_1_6.py index cd7518fe0..bb0931fdd 100644 --- a/armi/scripts/migration/tests/test_m0_1_6_locationLabels.py +++ b/armi/migration/tests/test_m0_1_6.py @@ -15,10 +15,8 @@ import io import unittest +from armi.migration.m0_1_6 import ConvertAlphanumLocationSettingsToNum from armi.settings import caseSettings -from armi.scripts.migration.m0_1_6_locationLabels import ( - ConvertAlphanumLocationSettingsToNum, -) from armi.settings.settingsIO import SettingsWriter, SettingsReader diff --git a/armi/migration/tests/test_migration_base.py b/armi/migration/tests/test_migration_base.py new file mode 100644 index 000000000..67bffcc81 --- /dev/null +++ b/armi/migration/tests/test_migration_base.py @@ -0,0 +1,42 @@ +# Copyright 2023 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. +"""Test base migration classes.""" +import os +import unittest + +from armi.migration.base import Migration +from armi.migration.base import SettingsMigration +from armi.tests import TEST_ROOT + + +class TestMigrationBases(unittest.TestCase): + def test_basic_validation(self): + with self.assertRaises(RuntimeError): + _m = Migration(None, None) + + with self.assertRaises(RuntimeError): + _m = Migration("fake_stream", "fake_path") + + Migration("fake_stream", None) + m = Migration(None, "fake_path") + with self.assertRaises(ValueError): + m._loadStreamFromPath() + + +class TestSettingsMigration(unittest.TestCase): + def test_loadStreamFromPath(self): + file_path = os.path.join(TEST_ROOT, "armiRun.yaml") + m = SettingsMigration(None, file_path) + m._loadStreamFromPath() + self.assertIsNotNone(m.stream) diff --git a/armi/physics/constants.py b/armi/physics/constants.py index 24972b58f..6e26747ff 100644 --- a/armi/physics/constants.py +++ b/armi/physics/constants.py @@ -19,40 +19,9 @@ Displacements per atom are correlated to material damage. -.. note:: This data structure can be updated by plugins with design-specific dpa data. - -See :py:mod:`armi.scripts.genDpaXs` for details. - -The dpa cross sections are generated by running the following code: - ->>> from armi.scripts import genDpaXs ->>> from armi.nuclearDataIO import SPECTR ->>> from armi.materials import T92, HT9, SS316 ->>> import os ->>> from armi import settings ->>> from armi.physics.neutronics import energyGroups ->>> ANL33 = energyGroups.GROUP_STRUCTURE["ANL33"] ->>> cs = settings.Settings() ->>> spectrum = SPECTR(os.path.join(cs['DPAXSDirectoryPath'], "spectra\\twr_bol.SPECTR")) - ->>> ht9collapser = genDpaXs.DpaCrossSectionCollapser(coarseEnergyUpperBoundseV=ANL33, - detailedSpectrum=spectrum, - endfDataPath=cs['DPAXSDirectoryPath'], - material=HT9()) ->>> ht9collapser.collapse() - ->>> t92collapser = genDpaXs.DpaCrossSectionCollapser(coarseEnergyUpperBoundseV=ANL33, - detailedSpectrum=spectrum, - endfDataPath=cs['DPAXSDirectoryPath'], - material=T92()) ->>> t92collapser.collapse() - ->>> ss316collapser = genDpaXs.DpaCrossSectionCollapser(coarseEnergyUpperBoundseV=ANL33, - detailedSpectrum=spectrum, - endfDataPath=cs['DPAXSDirectoryPath'], - material=SS316()) ->>> ss316collapser.collapse() - +Notes +----- +This data structure can be updated by plugins with design-specific dpa data. """ # The following are multigroup DPA XS for EBR II. They were generated using an ultra hard MCC spectrum diff --git a/armi/physics/neutronics/settings.py b/armi/physics/neutronics/settings.py index 45841188c..46139d961 100644 --- a/armi/physics/neutronics/settings.py +++ b/armi/physics/neutronics/settings.py @@ -19,9 +19,6 @@ from armi.operators import settingsValidation from armi.physics.neutronics.const import NEUTRON from armi.physics.neutronics.energyGroups import GROUP_STRUCTURE -from armi.scripts.migration.crossSectionBlueprintsToSettings import ( - migrateCrossSectionsFromBlueprints, -) from armi.physics.neutronics import LatticePhysicsFrequency from armi.settings import setting from armi.utils import directoryChangers @@ -513,20 +510,6 @@ def migrateDpaGridPlate(): ) ) - queries.append( - settingsValidation.Query( - lambda: inspector.cs[CONF_LOADING_FILE] - and _blueprintsHasOldXSInput(inspector), - "The specified blueprints input file '{0}' contains compound cross section settings. " - "".format(inspector.cs[CONF_LOADING_FILE]), - "Automatically move them to the settings file, {}? WARNING: if multiple settings files point " - "to this blueprints input you must manually update the others.".format( - inspector.cs.path - ), - lambda: migrateCrossSectionsFromBlueprints(inspector.cs), - ) - ) - queries.append( settingsValidation.Query( lambda: inspector.cs[CONF_DETAILED_AXIAL_EXPANSION] diff --git a/armi/physics/neutronics/tests/test_neutronicsPlugin.py b/armi/physics/neutronics/tests/test_neutronicsPlugin.py index dbeac4fcc..dabeb0c2d 100644 --- a/armi/physics/neutronics/tests/test_neutronicsPlugin.py +++ b/armi/physics/neutronics/tests/test_neutronicsPlugin.py @@ -262,7 +262,7 @@ def test_neutronicsSettingsValidators(self): cs = settings.Settings() inspector = settingsValidation.Inspector(cs) sv = getNeutronicsSettingValidators(inspector) - self.assertEqual(len(sv), 9) + self.assertEqual(len(sv), 8) # Test the Query: boundaries are now "Extrapolated", not "Generalized" cs = cs.modified(newSettings={CONF_BOUNDARIES: "Generalized"}) diff --git a/armi/reactor/blueprints/__init__.py b/armi/reactor/blueprints/__init__.py index 179c664a2..7031afab1 100644 --- a/armi/reactor/blueprints/__init__.py +++ b/armi/reactor/blueprints/__init__.py @@ -77,6 +77,7 @@ from armi import context from armi import getPluginManager, getPluginManagerOrFail +from armi import migration from armi import plugins from armi import runLog from armi.nucDirectory import nuclideBases @@ -84,7 +85,6 @@ from armi.reactor import geometry from armi.reactor import systemLayoutInput from armi.reactor.flags import Flags -from armi.scripts import migration from armi.utils.customExceptions import InputError from armi.utils import textProcessors from armi.settings.fwSettings.globalSettings import ( @@ -633,10 +633,5 @@ def migrate(bp: Blueprints, cs): aDesign.radialMeshPoints = radMesh aDesign.azimuthalMeshPoints = aziMesh - # Someday: write out the migrated file. At the moment this messes up the case + # TODO: write out the migrated file. At the moment this messes up the case # title and doesn't yet have the other systems in place so this isn't the right place. - - -# cs.writeToXMLFile(cs.caseTitle + '.migrated.xml') -# with open(os.path.split(cs['loadingFile'])[0] + '.migrated.' + '.yaml', 'w') as loadingFile: -# blueprints.Blueprints.dump(bp, loadingFile) diff --git a/armi/scripts/__init__.py b/armi/scripts/__init__.py deleted file mode 100644 index 68246dea8..000000000 --- a/armi/scripts/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2019 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. - -"""This package contains some migration scripts to help with backward compatibility.""" diff --git a/armi/scripts/migration/crossSectionBlueprintsToSettings.py b/armi/scripts/migration/crossSectionBlueprintsToSettings.py deleted file mode 100644 index 37b69034a..000000000 --- a/armi/scripts/migration/crossSectionBlueprintsToSettings.py +++ /dev/null @@ -1,171 +0,0 @@ -# Copyright 2019 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. - -""" -Migrate compound cross section settings from blueprints file to settings file. - -This setting was originally stored in blueprints just because yaml was more -conducive than XML for a compound setting. After we migrated the settings -system to YAML, it was much more natural to put this setting in the settings -file. Along the way, we renamed some of the input fields as well. - -This deletes the ``cross sections`` section from the blueprints file -and adds a valid one into the settings file. - -It manually reads the blueprints file rather than parsing it to ensure -round-trippiness even with yaml-native links. -""" -# ruff: noqa: F403,F405,F821 -import io -import os -import shutil - -from ruamel.yaml import YAML - -from armi import runLog -from armi.physics.neutronics.const import CONF_CROSS_SECTION -from armi.physics.neutronics.crossSectionSettings import * -from armi.scripts.migration.base import SettingsMigration -from armi.settings import caseSettings -from armi.settings import settingsIO - - -class MoveCrossSectionsFromBlueprints(SettingsMigration): - """ - Move cross sections settings from blueprints to settings. - - This modifies both settings and blueprints. - """ - - fromVersion = "0.0.0" - toVersion = "0.1.0" - - def _applyToStream(self): - cs = caseSettings.Settings() - reader = settingsIO.SettingsReader(cs) - reader.readFromStream(self.stream) - self.stream.close() - cs.path = self.path - - migrateCrossSectionsFromBlueprints(cs) - writer = settingsIO.SettingsWriter(cs) - newStream = io.StringIO() - writer.writeYaml(newStream) - newStream.seek(0) - return newStream - - def _backupOriginal(self): - """Don't actually back up because it's done below.""" - - def _writeNewFile(self, newStream): - """Skip writing new file since it's handled below.""" - - -def migrateCrossSectionsFromBlueprints(settingsObj): - from armi.physics.neutronics.settings import CONF_LOADING_FILE - - settingsPath = settingsObj.path - runLog.info( - "Migrating cross section settings from blueprints file to settings file ({})...".format( - settingsPath - ) - ) - cs = caseSettings.Settings() - cs.loadFromInputFile(settingsPath) - - fullBlueprintsPath = os.path.join(cs.inputDirectory, cs[CONF_LOADING_FILE]) - origXsInputLines = _convertBlueprints(fullBlueprintsPath) - if not origXsInputLines: - runLog.warning( - "No old input found in {}. Aborting migration.".format(fullBlueprintsPath) - ) - return cs - newXsData = _migrateInputData(origXsInputLines) - _writeNewSettingsFile(cs, newXsData) - # cs now has a proper crossSection setting - - _finalize(fullBlueprintsPath, settingsPath) - # update the existing cs with the new setting in memory so the GUI doesn't wipe it out! - settingsObj[CONF_CROSS_SECTION] = cs.get(CONF_CROSS_SECTION).dump() - return cs - - -def _convertBlueprints(bpPath): - origXsInput = [] - active = False - with open(bpPath) as bpIn, open(bpPath + ".new", "w") as bpOut: - for line in bpIn: - if line.startswith("cross sections:"): - active = True - elif active and not line.startswith(" "): - active = False - - if active: - origXsInput.append(line) - else: - bpOut.write(line) - return origXsInput - - -def _migrateInputData(origXsInputLines): - """Take spaces in labels out and return dict.""" - conversions = { - "critical buckling": CONF_BUCKLING, - "block representation": CONF_BLOCK_REPRESENTATION, - "driver id": CONF_DRIVER, - "nuclear reaction driver": CONF_REACTION_DRIVER, - "valid block types": CONF_BLOCKTYPES, - "external driver": CONF_EXTERNAL_DRIVER, - "use homogenized block composition": CONF_HOMOGBLOCK, - "internal rings": CONF_INTERNAL_RINGS, - "external rings": CONF_EXTERNAL_RINGS, - "merge into clad": CONF_MERGE_INTO_CLAD, - "file location": CONF_XS_FILE_LOCATION, - "mesh points per cm": CONF_MESH_PER_CM, - } - yaml = YAML() - oldXsData = yaml.load(io.StringIO("\n".join(origXsInputLines))) - newXsData = {} - for xsID, xsIdData in oldXsData["cross sections"].items(): - newIdData = {} - for label, val in xsIdData.items(): - newIdData[conversions.get(label, label)] = val - newXsData[xsID] = newIdData - return newXsData - - -def _writeNewSettingsFile(cs, newXsInput): - cs[CONF_CROSS_SECTION] = newXsInput - cs.writeToYamlFile(cs.path + ".new") - - -def _finalize(bpPath, csPath): - """ - Actually overwrite the original files if previous steps completed. - - This ensures we don't get partially migrated and then crash. - """ - shutil.move(bpPath, bpPath + "-migrated") - shutil.move(bpPath + ".new", bpPath) - - shutil.move(csPath, csPath + "-migrated") - shutil.move(csPath + ".new", csPath) - - -if __name__ == "__main__": - import sys - - cs = caseSettings.Settings() - cs.loadFromInputFile(sys.argv[1]) - migrateCrossSectionsFromBlueprints(cs) diff --git a/armi/scripts/migration/m0_1_0_newDbFormat.py b/armi/scripts/migration/m0_1_0_newDbFormat.py deleted file mode 100644 index 3197c0b07..000000000 --- a/armi/scripts/migration/m0_1_0_newDbFormat.py +++ /dev/null @@ -1,179 +0,0 @@ -# Copyright 2019 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. -""" -Migrate ARMI databases to newer versions. - -Oftentimes, code changes cause databases from previous versions -to become invalid, yet users still want to be able to load from -the old cases. These scripts assist in converting old -databases to new ones. - -This is expected to be extended with version-dependent chains. -""" -import os -import io - -import h5py - -from armi import __version__ as version -from armi import getApp, runLog, utils -from armi.reactor import systemLayoutInput -from armi.scripts.migration.base import DatabaseMigration - - -class ConvertDB2toDB3(DatabaseMigration): - """Convert ARMI DB version 2 to DB version 3.""" - - def __init__(self, stream=None, path=None): - DatabaseMigration.__init__(self, stream=stream, path=path) - if stream: - raise ValueError("Can only migrate database by path.") - - def apply(self): - _migrateDatabase(self.path, _preCollector, _visit) - - -def _migrateDatabase(databasePath, preCollector, visitor): - """ - Generic database-traversing system to apply custom version-specific migrations. - - Parameters - ---------- - databasePath : str - Path to DB file to be converted - preCollector : callable - Function that acts on oldDB and produces some generic data object - visitor : callable - Function that will be called on each dataset of the old HDF5 database. - This should map information into the new DB. - - Raises - ------ - OSError - When database is not found. - """ - if not os.path.exists(databasePath): - raise OSError("Database file {} does not exist".format(databasePath)) - - runLog.info("Migrating database file: {}".format(databasePath)) - runLog.info("Generating SHA-1 hash for original database: {}".format(databasePath)) - shaHash = utils.getFileSHA1Hash(databasePath) - runLog.info(" Database: {}\n" " SHA-1: {}".format(databasePath, shaHash)) - _remoteFolder, remoteDbName = os.path.split(databasePath) # make new DB locally - root, ext = os.path.splitext(remoteDbName) - newDBName = root + "_migrated" + ext - runLog.info("Copying database from {} to {}".format(databasePath, newDBName)) - with h5py.File(newDBName, "w") as newDB, h5py.File(databasePath, "r") as oldDB: - - preCollection = preCollector(oldDB) - - def closure(name, dataset): - visitor(newDB, preCollection, name, dataset) - - oldDB.visititems(closure) - - # Copy all old database attributes to the new database (h5py AttributeManager has no update method) - for key, val in oldDB.attrs.items(): - newDB.attrs[key] = val - - newDB.attrs["original-armi-version"] = oldDB.attrs["version"] - newDB.attrs["original-db-hash"] = shaHash - newDB.attrs["original-databaseVersion"] = oldDB.attrs["databaseVersion"] - newDB.attrs["version"] = version - - runLog.info("Successfully generated migrated database file: {}".format(newDBName)) - - -def _visit(newDB, preCollection, name, dataset): - - updated = False - # runLog.important(f"Visiting Dataset {name}") - path = name.split("/") - if path[0] == "inputs": - pass - elif len(path) > 1 and path[1] == "layout": - updated = _updateLayout(newDB, preCollection, name, dataset) - elif len(path) == 3: - updated = _updateParams(newDB, preCollection, name, dataset) - - if not updated: - if isinstance(dataset, h5py.Group): - # Skip groups because they come along with copied datasets - msg = "Skipped" - else: - newDB.copy(dataset, dataset.name) - msg = "Copied" - else: - msg = "Updated" - - runLog.important(f"{msg} Dataset {name}") - - -def _preCollector(oldDB): - preCollection = {} - preCollection.update(_collectParamRenames()) - preCollection.update(_collectSymmetry(oldDB)) - return preCollection - - -def _updateLayout(newDB, preCollection, name, dataset): - path = name.split("/") - if len(path) == 4 and path[2] == "grids" and path[3] != "type": - # node/layout/grids/4 - if "symmetry" not in dataset: - newDB.create_dataset(f"{name}/symmetry", data=preCollection["symmetry"]) - if "geomType" not in dataset: - newDB.create_dataset(f"{name}/geomType", data=preCollection["geomType"]) - - # maintain attrs on group if we just made it - gridGroup = newDB[f"{name}"] - for key, val in dataset.attrs.items(): - gridGroup.attrs[key] = val - - return True - return False - - -def _updateParams(newDB, preCollection, name, dataset): - """ - Visit parameters and apply migration transformations. - - Does not affect input or layout. - """ - renames = preCollection["paramRenames"] - updated = _applyRenames(newDB, renames, name, dataset) - return updated - - -def _collectParamRenames(): - return {"paramRenames": getApp().getParamRenames()} - - -def _collectSymmetry(oldDB): - """Read symmetry and geomType off old-style geometry input str in DB.""" - geomPath = "/inputs/geomFile" - if geomPath in oldDB: - geom = systemLayoutInput.SystemLayoutInput() - geom.readGeomFromStream(io.StringIO(oldDB["inputs/geomFile"][()])) - return {"symmetry": geom.symmetry, "geomType": geom.geomType} - - -def _applyRenames(newDB, renames, name, dataset): - node, paramType, paramName = name.split("/") - if paramName in renames: - runLog.important("Renaming `{}` -> `{}`.".format(paramName, renames[paramName])) - paramName = renames[paramName] - newDB.copy(dataset, "{}/{}/{}".format(node, paramType, paramName)) - return True - return False From bdd34d39a74c2b21549630b650ddce90e18f4752 Mon Sep 17 00:00:00 2001 From: Chris Keckler Date: Thu, 19 Oct 2023 14:02:17 -0500 Subject: [PATCH 025/176] Remove stale info from `flags` docstring (#1443) --- armi/utils/flags.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/armi/utils/flags.py b/armi/utils/flags.py index a531cc4a5..7b478ae0c 100644 --- a/armi/utils/flags.py +++ b/armi/utils/flags.py @@ -23,9 +23,6 @@ ``Enum`` classes, but unfortunately does not support extension of ``Flags``. So, we had to make our own. This is a much simplified version of what comes with ``aenum``, but still provides most of the safety and functionality. - -There is an `issue `_ on the ``aenum`` -bitbucket site to track ``Flag`` extension. """ import math From b67c22f17a91f9ca5fc6f397dc3086822787f621 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 19 Oct 2023 15:10:29 -0700 Subject: [PATCH 026/176] Misc cleanup (#1439) - I removed some random code (AVAILABLE_MODIFICATION_NAMES) - I removed an unused armi/physics/safety/settings.py file. - I added requirements to our PR template. - Removing useless getMassFracCopy - Removing unused variable cycleTime --- .github/pull_request_template.md | 12 +++---- armi/bookkeeping/report/tests/test_report.py | 7 ++-- armi/bookkeeping/visualization/utils.py | 2 +- armi/materials/__init__.py | 11 ------- armi/materials/material.py | 4 --- armi/materials/tests/test_materials.py | 4 +-- armi/nuclearDataIO/tests/test_xsLibraries.py | 33 ++++++++++--------- .../physics/fuelCycle/fuelHandlerInterface.py | 5 --- armi/physics/safety/__init__.py | 4 +-- armi/physics/safety/settings.py | 18 ---------- armi/reactor/tests/test_reactors.py | 9 +++++ armi/utils/tests/test_plotting.py | 9 +++++ doc/developer/tooling.rst | 21 ++++++++++++ 13 files changed, 67 insertions(+), 72 deletions(-) delete mode 100644 armi/physics/safety/settings.py diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 77e424a45..c13378ea6 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -7,8 +7,6 @@ - - --- ## Checklist @@ -16,12 +14,11 @@ -- [ ] This PR has only one purpose or idea. -- [ ] Tests have been added/updated to verify that the new/changed code works. +- [ ] This PR has only [one purpose or idea](https://terrapower.github.io/armi/developer/tooling.html#one-idea-one-pr). +- [ ] [Tests](https://terrapower.github.io/armi/developer/tooling.html#test-it) have been added/updated to verify that the new/changed code works. @@ -31,5 +28,6 @@ - [ ] The [release notes](https://terrapower.github.io/armi/release/index.html) (location `doc/release/0.X.rst`) are up-to-date with any important changes. -- [ ] The documentation is still up-to-date in the `doc` folder. +- [ ] The [documentation](https://terrapower.github.io/armi/developer/tooling.html#document-it) is still up-to-date in the `doc` folder. +- [ ] No [requirements](https://terrapower.github.io/armi/developer/tooling.html#watch-for-requirements) were altered. - [ ] The dependencies are still up-to-date in `pyproject.toml`. diff --git a/armi/bookkeeping/report/tests/test_report.py b/armi/bookkeeping/report/tests/test_report.py index d1dc80c68..577b97276 100644 --- a/armi/bookkeeping/report/tests/test_report.py +++ b/armi/bookkeeping/report/tests/test_report.py @@ -136,11 +136,8 @@ def test_reactorSpecificReporting(self): def test_writeWelcomeHeaders(self): o, r = loadTestReactor() - # grab a random file (that exist in the working dir) - files = os.listdir(os.getcwd()) - files = [f for f in files if f.endswith(".py") or f.endswith(".txt")] - self.assertGreater(len(files), 0) - randoFile = files[0] + # grab this file path + randoFile = os.path.abspath(__file__) # pass that random file into the settings o.cs["crossSectionControl"]["DA"].xsFileLocation = randoFile diff --git a/armi/bookkeeping/visualization/utils.py b/armi/bookkeeping/visualization/utils.py index 8bef3bd91..a27a0a3e0 100644 --- a/armi/bookkeeping/visualization/utils.py +++ b/armi/bookkeeping/visualization/utils.py @@ -255,7 +255,7 @@ def _createCartesianBlockMesh(b: blocks.CartesianBlock) -> VtkMesh: def _createTRZBlockMesh(b: blocks.ThRZBlock) -> VtkMesh: - # There's no sugar-coating this one. It sucks. + # This could be improved. rIn = b.radialInner() rOut = b.radialOuter() thIn = b.thetaInner() diff --git a/armi/materials/__init__.py b/armi/materials/__init__.py index 5482ede60..02eaa38cb 100644 --- a/armi/materials/__init__.py +++ b/armi/materials/__init__.py @@ -38,7 +38,6 @@ import inspect from armi.materials.material import Material -from armi.utils import dynamicImporter # this will frequently be updated by the CONF_MATERIAL_NAMESPACE_ORDER setting # during reactor construction (see armi.reactor.reactors.factory) @@ -94,16 +93,6 @@ def importMaterialsIntoModuleNamespace(path, name, namespace, updateSource=None) importMaterialsIntoModuleNamespace(__path__, __name__, globals()) -# the co_varnames attribute contains arguments and then locals so we must restrict it to just the arguments. -AVAILABLE_MODIFICATION_NAMES = { - name - for subclass in dynamicImporter.getEntireFamilyTree(Material) - for name in subclass.applyInputParams.__code__.co_varnames[ - : subclass.applyInputParams.__code__.co_argcount - ] -} -AVAILABLE_MODIFICATION_NAMES.remove("self") - def iterAllMaterialClassesInNamespace(namespace): """ diff --git a/armi/materials/material.py b/armi/materials/material.py index e7d3d09d0..c5592db4d 100644 --- a/armi/materials/material.py +++ b/armi/materials/material.py @@ -17,7 +17,6 @@ Most temperatures may be specified in either K or C and the functions will convert for you. """ -import copy import warnings from scipy.optimize import fsolve @@ -574,9 +573,6 @@ def removeNucMassFrac(self, nuc: str) -> None: # the nuc isn't in the mass Frac vector pass - def getMassFracCopy(self): - return copy.deepcopy(self.massFrac) - def checkPropertyTempRange(self, label, val): """Checks if the given property / value combination fall between the min and max valid temperatures provided in the propertyValidTemperature object. diff --git a/armi/materials/tests/test_materials.py b/armi/materials/tests/test_materials.py index 258e0282d..e60d692be 100644 --- a/armi/materials/tests/test_materials.py +++ b/armi/materials/tests/test_materials.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Tests materials.py.""" - +from copy import deepcopy import math import pickle import unittest @@ -710,7 +710,7 @@ def test_duplicate(self): for key in self.mat.massFrac: self.assertEqual(duplicateU.massFrac[key], self.mat.massFrac[key]) - duplicateMassFrac = self.mat.getMassFracCopy() + duplicateMassFrac = deepcopy(self.mat.massFrac) for key in self.mat.massFrac.keys(): self.assertEqual(duplicateMassFrac[key], self.mat.massFrac[key]) diff --git a/armi/nuclearDataIO/tests/test_xsLibraries.py b/armi/nuclearDataIO/tests/test_xsLibraries.py index 692e7e93a..bbe29afe2 100644 --- a/armi/nuclearDataIO/tests/test_xsLibraries.py +++ b/armi/nuclearDataIO/tests/test_xsLibraries.py @@ -127,23 +127,24 @@ def test_mergeFailsWithNonIsotxsFiles(self): finally: os.remove(dummyFileName) - dummyFileName = "ISOtopics.txt" - with open(dummyFileName, "w") as file: - file.write( - "This is a file that starts with the letters 'ISO' but will" - " break the regular expression search." - ) - - try: - with mockRunLogs.BufferLog() as log: - lib = xsLibraries.IsotxsLibrary() - xsLibraries.mergeXSLibrariesInWorkingDirectory(lib) - self.assertIn( - f"{dummyFileName} in the merging of ISOXX files", - log.getStdout(), + with TemporaryDirectoryChanger(): + dummyFileName = "ISOtopics.txt" + with open(dummyFileName, "w") as file: + file.write( + "This is a file that starts with the letters 'ISO' but will" + " break the regular expression search." ) - finally: - pass + + try: + with mockRunLogs.BufferLog() as log: + lib = xsLibraries.IsotxsLibrary() + xsLibraries.mergeXSLibrariesInWorkingDirectory(lib) + self.assertIn( + f"{dummyFileName} in the merging of ISOXX files", + log.getStdout(), + ) + finally: + pass def _xsLibraryAttributeHelper( self, diff --git a/armi/physics/fuelCycle/fuelHandlerInterface.py b/armi/physics/fuelCycle/fuelHandlerInterface.py index 31732123f..aa7098bbd 100644 --- a/armi/physics/fuelCycle/fuelHandlerInterface.py +++ b/armi/physics/fuelCycle/fuelHandlerInterface.py @@ -42,8 +42,6 @@ def __init__(self, r, cs): # need order due to nature of moves but with fast membership tests self.moved = [] self.cycle = 0 - # filled during summary of EOC time in years of each cycle (time at which shuffling occurs) - self.cycleTime = {} @staticmethod def specifyInputs(cs): @@ -86,9 +84,6 @@ def interactBOC(self, cycle=None): self.manageFuel(cycle) def interactEOC(self, cycle=None): - timeYears = self.r.p.time - # keep track of the EOC time in years. - self.cycleTime[cycle] = timeYears if self.r.sfp is not None: runLog.extra( f"There are {len(self.r.sfp)} assemblies in the Spent Fuel Pool" diff --git a/armi/physics/safety/__init__.py b/armi/physics/safety/__init__.py index adaf6b2b4..82f82e49f 100644 --- a/armi/physics/safety/__init__.py +++ b/armi/physics/safety/__init__.py @@ -15,11 +15,9 @@ """Safety package for generic safety-related code.""" from armi import plugins -from armi.physics.safety import settings - class SafetyPlugin(plugins.ArmiPlugin): @staticmethod @plugins.HOOKIMPL def defineSettings(): - return settings.defineSettings() + return [] diff --git a/armi/physics/safety/settings.py b/armi/physics/safety/settings.py deleted file mode 100644 index 5571a74e9..000000000 --- a/armi/physics/safety/settings.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright 2019 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. - - -def defineSettings(): - settings = [] - return settings diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 9cb3a4972..7e3fa9533 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -16,6 +16,7 @@ import logging import os import unittest +from unittest.mock import patch from numpy.testing import assert_allclose, assert_equal from six.moves import cPickle @@ -279,6 +280,9 @@ def test_getTotalParam(self): val2 = self.r.core.getTotalBlockParam("power", addSymmetricPositions=True) self.assertEqual(val2 / self.r.core.powerMultiplier, val) + with self.assertRaises(ValueError): + self.r.core.getTotalBlockParam(generationNum=1) + def test_geomType(self): self.assertEqual(self.r.core.geomType, geometry.GeomType.HEX) @@ -622,6 +626,11 @@ def test_getNumAssembliesWithAllRingsFilledOut(self): nAssmWithBlanks = self.r.core.getNumAssembliesWithAllRingsFilledOut(nRings) self.assertEqual(77, nAssmWithBlanks) + @patch("armi.reactor.reactors.Core.powerMultiplier", 1) + def test_getNumAssembliesWithAllRingsFilledOutBipass(self): + nAssems = self.r.core.getNumAssembliesWithAllRingsFilledOut(3) + self.assertEqual(19, nAssems) + def test_getNumEnergyGroups(self): # this Core doesn't have a loaded ISOTXS library, so this test is minimally useful with self.assertRaises(AttributeError): diff --git a/armi/utils/tests/test_plotting.py b/armi/utils/tests/test_plotting.py index 802f01ae3..ec7ab50f8 100644 --- a/armi/utils/tests/test_plotting.py +++ b/armi/utils/tests/test_plotting.py @@ -55,6 +55,9 @@ def test_plotAssemblyTypes(self): plotting.plotAssemblyTypes(self.r.core.parent.blueprints, plotPath) self._checkExists(plotPath) + if os.path.exists(plotPath): + os.remove(plotPath) + plotPath = "coreAssemblyTypes2.png" plotting.plotAssemblyTypes( self.r.core.parent.blueprints, @@ -64,9 +67,15 @@ def test_plotAssemblyTypes(self): ) self._checkExists(plotPath) + if os.path.exists(plotPath): + os.remove(plotPath) + with self.assertRaises(ValueError): plotting.plotAssemblyTypes(None, plotPath, None) + if os.path.exists(plotPath): + os.remove(plotPath) + def test_plotBlockFlux(self): with TemporaryDirectoryChanger(): xslib = isotxs.readBinary(ISOAA_PATH) diff --git a/doc/developer/tooling.rst b/doc/developer/tooling.rst index 37b366771..d9dfb091c 100644 --- a/doc/developer/tooling.rst +++ b/doc/developer/tooling.rst @@ -77,6 +77,27 @@ nitty-gritty detail to fix a bug without you. Think about variable names, commen Also consider (if you are making a major change) that you might be making something in the docs out-of-date. +Watch for Requirements +^^^^^^^^^^^^^^^^^^^^^^ +When you are touching code in ARMI, watch out for the docstrings in the methods, classes, or +modules you are editing. These docstrings might have bread crumbs that link back to requirements. +Such breadcrumbs will look like: + +.. code-block:: + + """ + .. req: This is a requirement breadcrumb. + + .. test: This is a test breadcrumb. + + .. impl: This is an implementation breadcrumb. + + """ + +If you touch any code that has such a docstring, even in a file, you are going to be +responsible for not breaking that code/functionality. And you will be required to explicitly +call out that you touch such a code in your PR. + Packaging and dependency management ----------------------------------- The process of packaging Python projects and managing their dependencies is somewhat From ddb740ce419e0b1e8aaf90177692ff45cad59013 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Fri, 20 Oct 2023 08:22:11 -0700 Subject: [PATCH 027/176] Let's not run GH Unit Tests on doc-only PRs (#1445) --- .github/workflows/coverage.yaml | 4 ++++ .github/workflows/unittests.yaml | 8 +++++++- .github/workflows/wintests.yaml | 8 +++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 0e85a6400..114f5acd5 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -4,7 +4,11 @@ on: push: branches: - main + paths-ignore: + - 'doc/**' pull_request: + paths-ignore: + - 'doc/**' jobs: build: diff --git a/.github/workflows/unittests.yaml b/.github/workflows/unittests.yaml index 475d78017..6e1d950c3 100644 --- a/.github/workflows/unittests.yaml +++ b/.github/workflows/unittests.yaml @@ -1,6 +1,12 @@ name: ARMI unit tests -on: [push, pull_request] +on: + push: + paths-ignore: + - 'doc/**' + pull_request: + paths-ignore: + - 'doc/**' jobs: build: diff --git a/.github/workflows/wintests.yaml b/.github/workflows/wintests.yaml index 4d1dd435d..354ae0813 100644 --- a/.github/workflows/wintests.yaml +++ b/.github/workflows/wintests.yaml @@ -1,6 +1,12 @@ name: ARMI Windows tests -on: [push, pull_request] +on: + push: + paths-ignore: + - 'doc/**' + pull_request: + paths-ignore: + - 'doc/**' jobs: build: From 7f5b80a6f73929f15a0640b5f1dc1fbd647ee0c4 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Fri, 20 Oct 2023 09:01:49 -0700 Subject: [PATCH 028/176] Changing draft PR policy (#1444) --- doc/developer/tooling.rst | 9 +++++---- doc/release/0.2.rst | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/doc/developer/tooling.rst b/doc/developer/tooling.rst index d9dfb091c..1cc8127f0 100644 --- a/doc/developer/tooling.rst +++ b/doc/developer/tooling.rst @@ -46,10 +46,11 @@ Don't open until it is ready Your PR isn't complete when the code works, it is complete when the code is polished and all the tests are written and working. The idea here is: as soon as you open a PR, people will start -spending their time looking at it. And their time is valuable. An exception to this rule is that -GitHub allows you to `open a Draft PR `_ -which is a nice option if you need to open your PR early for some reason (usually testing). You -can also convert any open PR to Draft if you decide it needs more work. +spending their time looking at it. And their time is valuable. Even though GitHub allows you to +`open a Draft PR `_, this is +not the default option in ARMI. It should not be your workflow to open a Draft PR by default. We +prefer to keep the PR list as short as possible. A good rule of thumb is: don't open a PR until +you think it is ready for final review. Test It ^^^^^^^ diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst index 3c0a391d0..62bed0fc6 100644 --- a/doc/release/0.2.rst +++ b/doc/release/0.2.rst @@ -11,6 +11,7 @@ What's new in ARMI #. The ``_copyInputsHelper()`` gives relative path and not absolute after copy. (`PR#1416 `_) #. ARMI now mandates ``ruff`` linting. (`PR#1419 `_) #. Removed all old ARMI requirements, to start the work fresh. (`PR#1438 `_) +#. Downgrading Draft PRs as policy. (`PR#1444 `_) #. TBD Bug fixes From 170e391e75f19853df67b409b4cedf3d4bc6725d Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Fri, 20 Oct 2023 09:13:29 -0700 Subject: [PATCH 029/176] Cleaning up bootstrap warning (#1446) --- armi/_bootstrap.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/armi/_bootstrap.py b/armi/_bootstrap.py index 2afb2fa85..7d522cd3b 100644 --- a/armi/_bootstrap.py +++ b/armi/_bootstrap.py @@ -12,25 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -""" -Collection of code that needs to be executed before most ARMI components are safe to -import. -""" +"""Code that needs to be executed before most ARMI components are safe to import.""" import sys import tabulate -# This needs to happen pretty darn early, as one of it's purposes is to provide a better -# python version warning than "invalid syntax". Maybe this is enough of a crutch that we -# should get rid of it... +# This is a courtesy, to help people who accidently run ARMI with an old version of Python. if ( sys.version_info.major < 3 or sys.version_info.major == 3 - and sys.version_info.minor < 6 + and sys.version_info.minor < 7 ): raise RuntimeError( - "ARMI highly recommends using Python 3.7. Are you sure you are using the correct " - "interpreter?\nUsing: {}".format(sys.executable) + "ARMI highly recommends using Python 3.9 or 3.11. Are you sure you are using the " + f"correct interpreter?\nYou are using: {sys.executable}" ) From f580e3fe2120f93a0fbb0b5daa8424c9394c4698 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Fri, 20 Oct 2023 11:23:04 -0700 Subject: [PATCH 030/176] Cleaning out defunct shuffle logic validator (#1447) --- armi/physics/fuelCycle/settings.py | 78 +----------------------------- 1 file changed, 1 insertion(+), 77 deletions(-) diff --git a/armi/physics/fuelCycle/settings.py b/armi/physics/fuelCycle/settings.py index 8edbdc860..045f08ce4 100644 --- a/armi/physics/fuelCycle/settings.py +++ b/armi/physics/fuelCycle/settings.py @@ -12,11 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. """Settings for generic fuel cycle code.""" -import re -import os - -from armi.settings import setting from armi.operators import settingsValidation +from armi.settings import setting CONF_ASSEM_ROTATION_STATIONARY = "assemblyRotationStationary" CONF_ASSEMBLY_ROTATION_ALG = "assemblyRotationAlgorithm" @@ -131,66 +128,6 @@ def getFuelCycleSettingValidators(inspector): ) ) - # Check for code fixes for input code on the fuel shuffling outside the version control of ARMI - # These are basically auto-migrations for untracked code using - # the ARMI API. (This may make sense at a higher level) - regex_solutions = [ - ( - r"(#{0,20}?)[^\s#]*output\s*?\((.*?)(,\s*[1-3]{1}\s*)\)", - r"\1runLog.important(\2)", - ), - ( - r"(#{0,20}?)[^\s#]*output\s*?\((.*?)(,\s*[4-5]{1,2}\s*)\)", - r"\1runLog.info(\2)", - ), - ( - r"(#{0,20}?)[^\s#]*output\s*?\((.*?)(,\s*[6-8]{1,2}\s*)\)", - r"\1runLog.extra(\2)", - ), - ( - r"(#{0,20}?)[^\s#]*output\s*?\((.*?)(,\s*\d{1,2}\s*)\)", - r"\1runLog.debug(\2)", - ), - (r"(#{0,20}?)[^\s#]*output\s*?\((.*?)\)", r"\1runLog.important(\2)"), - (r"output = self.cs.output", r""), - (r"cs\.getSetting\(\s*([^\)]+)\s*\)", r"cs[\1]"), - (r"cs\.setSetting\(\s*([^\)]+)\s*,\s*([^\)]+)\s*\)", r"cs[\1] = \2"), - ( - r"import\s*armi\.components\s*as\s*components", - r"from armi.reactor import components", - ), - (r"\[['\"]caseTitle['\"]\]", r".caseTitle"), - ( - r"self.r.core.bolAssems\['(.*?)'\]", - r"self.r.blueprints.assemblies['\1']", - ), - (r"copyAssembly", r"duplicate"), - ] - - def _locateRegexOccurences(): - with open(inspector._csRelativePath(inspector.cs[CONF_SHUFFLE_LOGIC])) as src: - src = src.read() - matches = [] - for pattern, _sub in regex_solutions: - matches += re.findall(pattern, src) - return matches - - def _applyRegexSolutions(): - srcFile = inspector._csRelativePath(inspector.cs[CONF_SHUFFLE_LOGIC]) - destFile = os.path.splitext(srcFile)[0] + "migrated.py" - with open(srcFile) as src, open(destFile, "w") as dest: - srcContent = src.read() # get the buffer content - regexContent = srcContent # keep the before and after changes separate - - for pattern, sub in regex_solutions: - regexContent = re.sub(pattern, sub, regexContent) - - if regexContent != srcContent: - dest.write("from armi import runLog\n") - dest.write(regexContent) - - inspector.cs = inspector.cs.modified(newSettings={CONF_SHUFFLE_LOGIC: destFile}) - queries.append( settingsValidation.Query( lambda: " " in inspector.cs[CONF_SHUFFLE_LOGIC], @@ -216,17 +153,4 @@ def _clearShufflingInput(): ) ) - queries.append( - settingsValidation.Query( - lambda: inspector.cs[CONF_SHUFFLE_LOGIC] - and inspector._csRelativePathExists(inspector.cs[CONF_SHUFFLE_LOGIC]) - and _locateRegexOccurences(), - "The shuffle logic file {} uses deprecated code." - " It will not work unless you permit some automated changes to occur." - " The logic file will be backed up to the current directory under a timestamped name" - "".format(inspector.cs[CONF_SHUFFLE_LOGIC]), - "Proceed?", - _applyRegexSolutions, - ) - ) return queries From b35fd6ed5678786d18fd7fb4f453b6a8a717fa18 Mon Sep 17 00:00:00 2001 From: Michael Jarrett Date: Wed, 25 Oct 2023 08:27:16 -0700 Subject: [PATCH 031/176] Representative block component temperature fix (#1412) Set representative block densities by component if requested by user. Only possible if all blocks in the collection have similar components. If not, block averaging will revert to the default behavior. Co-authored-by: John Stilley <1831479+john-science@users.noreply.github.com> --- .../neutronics/crossSectionGroupManager.py | 134 ++++++++-- .../neutronics/crossSectionSettings.py | 9 + .../tests/test_crossSectionManager.py | 232 +++++++++++++++++- doc/release/0.2.rst | 2 +- 4 files changed, 353 insertions(+), 24 deletions(-) diff --git a/armi/physics/neutronics/crossSectionGroupManager.py b/armi/physics/neutronics/crossSectionGroupManager.py index 3c8162177..11a7fb535 100644 --- a/armi/physics/neutronics/crossSectionGroupManager.py +++ b/armi/physics/neutronics/crossSectionGroupManager.py @@ -120,10 +120,13 @@ class BlockCollection(list): This is a list with special methods. """ - def __init__(self, allNuclidesInProblem, validBlockTypes=None): + def __init__( + self, allNuclidesInProblem, validBlockTypes=None, averageByComponent=False + ): list.__init__(self) self.allNuclidesInProblem = allNuclidesInProblem self.weightingParam = None + self.averageByComponent = averageByComponent # allowed to be independent of fuel component temperatures b/c Doppler self.avgNucTemperatures = {} @@ -319,7 +322,17 @@ def _makeRepresentativeBlock(self): newBlock = self._getNewBlock() lfpCollection = self._getAverageFuelLFP() newBlock.setLumpedFissionProducts(lfpCollection) - newBlock.setNumberDensities(self._getAverageNumberDensities()) + # check if components are similar + if self._performAverageByComponent(): + # set number densities and temperatures on a component basis + for compIndex, c in enumerate(sorted(newBlock.getComponents())): + c.setNumberDensities( + self._getAverageComponentNumberDensities(compIndex) + ) + c.temperatureInC = self._getAverageComponentTemperature(compIndex) + else: + newBlock.setNumberDensities(self._getAverageNumberDensities()) + newBlock.p.percentBu = self._calcWeightedBurnup() self.calcAvgNuclideTemperatures() return newBlock @@ -359,6 +372,96 @@ def _getNucTempHelper(self): nv += nvBlock * wt return nvt, nv + def _getAverageComponentNumberDensities(self, compIndex): + """ + Get weighted average number densities of a component in the collection. + + Returns + ------- + numberDensities : dict + nucName, ndens data (atoms/bn-cm) + """ + nuclides = self.allNuclidesInProblem + blocks = self.getCandidateBlocks() + weights = numpy.array([self.getWeight(b) for b in blocks]) + weights /= weights.sum() # normalize by total weight + components = [sorted(b.getComponents())[compIndex] for b in blocks] + ndens = weights.dot([c.getNuclideNumberDensities(nuclides) for c in components]) + return dict(zip(nuclides, ndens)) + + def _getAverageComponentTemperature(self, compIndex): + """ + Get weighted average component temperature for the collection + + Notes + ----- + Weighting is both by the block weight within the collection and the relative mass of the component. + The block weight is already scaled by the block volume, so we need to pull that out of the block + weighting because it would effectively be double-counted in the component mass. b.getHeight() + is proportional to block volume, so it is used here as a computationally cheaper proxy for scaling + by block volume. + + Returns + ------- + numberDensities : dict + nucName, ndens data (atoms/bn-cm) + """ + blocks = self.getCandidateBlocks() + weights = numpy.array([self.getWeight(b) / b.getHeight() for b in blocks]) + weights /= weights.sum() # normalize by total weight + components = [sorted(b.getComponents())[compIndex] for b in blocks] + weightedAvgComponentMass = sum( + w * c.getMass() for w, c in zip(weights, components) + ) + if weightedAvgComponentMass == 0.0: + # if there is no component mass (e.g., gap), do a regular average + return numpy.mean(numpy.array([c.temperatureInC for c in components])) + else: + return ( + weights.dot( + numpy.array([c.temperatureInC * c.getMass() for c in components]) + ) + / weightedAvgComponentMass + ) + + def _performAverageByComponent(self): + """ + Check if block collection averaging can/should be performed by component + + If the components of blocks in the collection are similar and the user + has requested component-level averaging, return True. + Otherwise, return False. + """ + if not self.averageByComponent: + return False + else: + return self._checkBlockSimilarity() + + def _checkBlockSimilarity(self): + """ + Check if blocks in the collection have similar components + + If the components of blocks in the collection are similar and the user + has requested component-level averaging, return True. + Otherwise, return False. + """ + cFlags = dict() + for b in self.getCandidateBlocks(): + cFlags[b] = [c.p.flags for c in sorted(b.getComponents())] + refB = b + refFlags = cFlags[refB] + for b, compFlags in cFlags.items(): + for c, refC in zip(compFlags, refFlags): + if c != refC: + runLog.warning( + "Non-matching block in AverageBlockCollection!\n" + f"{refC} component flags in {refB} does not match {c} in {b}.\n" + f"Number densities will be smeared in representative block." + ) + return False + else: + return True + def getBlockNuclideTemperatureAvgTerms(block, allNucNames): """ @@ -370,22 +473,22 @@ def getBlockNuclideTemperatureAvgTerms(block, allNucNames): as trace values at the proper component temperatures. """ - def getNumberDensityWithTrace(component, nucName): - # needed to make sure temperature of 0-density nuclides in fuel get fuel temperature - try: - dens = component.p.numberDensities[nucName] or TRACE_NUMBER_DENSITY - except KeyError: - dens = 0.0 - return dens + def getNumberDensitiesWithTrace(component, allNucNames): + """ + Needed to make sure temperature of 0-density nuclides in fuel get fuel temperature + """ + return [ + component.p.numberDensities[nucName] or TRACE_NUMBER_DENSITY + if nucName in component.p.numberDensities + else 0.0 + for nucName in allNucNames + ] vol = block.getVolume() components, volFracs = zip(*block.getVolumeFractions()) # D = CxN matrix of number densities ndens = numpy.array( - [ - [getNumberDensityWithTrace(c, nucName) for nucName in allNucNames] - for c in components - ] + [getNumberDensitiesWithTrace(c, allNucNames) for c in components] ) temperatures = numpy.array( [c.temperatureInC for c in components] @@ -1314,6 +1417,9 @@ def blockCollectionFactory(xsSettings, allNuclidesInProblem): """Build a block collection based on user settings and input.""" blockRepresentation = xsSettings.blockRepresentation validBlockTypes = xsSettings.validBlockTypes + averageByComponent = xsSettings.averageByComponent return BLOCK_COLLECTIONS[blockRepresentation]( - allNuclidesInProblem, validBlockTypes=validBlockTypes + allNuclidesInProblem, + validBlockTypes=validBlockTypes, + averageByComponent=averageByComponent, ) diff --git a/armi/physics/neutronics/crossSectionSettings.py b/armi/physics/neutronics/crossSectionSettings.py index 92c58019d..5cdbdf55b 100644 --- a/armi/physics/neutronics/crossSectionSettings.py +++ b/armi/physics/neutronics/crossSectionSettings.py @@ -54,6 +54,7 @@ CONF_XSID = "xsID" CONF_XS_EXECUTE_EXCLUSIVE = "xsExecuteExclusive" CONF_XS_PRIORITY = "xsPriority" +CONF_COMPONENT_AVERAGING = "averageByComponent" CONF_XS_MAX_ATOM_NUMBER = "xsMaxAtomNumber" CONF_MIN_DRIVER_DENSITY = "minDriverDensity" @@ -113,6 +114,7 @@ def getStr(cls, typeSpec: Enum): CONF_BLOCKTYPES, CONF_BLOCK_REPRESENTATION, CONF_EXTERNAL_FLUX_FILE_LOCATION, + CONF_COMPONENT_AVERAGING, CONF_XS_EXECUTE_EXCLUSIVE, CONF_XS_PRIORITY, CONF_XS_MAX_ATOM_NUMBER, @@ -124,6 +126,7 @@ def getStr(cls, typeSpec: Enum): CONF_BLOCKTYPES, CONF_BLOCK_REPRESENTATION, CONF_EXTERNAL_FLUX_FILE_LOCATION, + CONF_COMPONENT_AVERAGING, CONF_XS_EXECUTE_EXCLUSIVE, CONF_XS_PRIORITY, CONF_XS_MAX_ATOM_NUMBER, @@ -141,6 +144,7 @@ def getStr(cls, typeSpec: Enum): CONF_BLOCKTYPES, CONF_BLOCK_REPRESENTATION, CONF_EXTERNAL_FLUX_FILE_LOCATION, + CONF_COMPONENT_AVERAGING, CONF_XS_EXECUTE_EXCLUSIVE, CONF_XS_PRIORITY, CONF_XS_MAX_ATOM_NUMBER, @@ -156,6 +160,7 @@ def getStr(cls, typeSpec: Enum): CONF_EXTERNAL_RINGS, CONF_BLOCK_REPRESENTATION, CONF_EXTERNAL_FLUX_FILE_LOCATION, + CONF_COMPONENT_AVERAGING, CONF_XS_EXECUTE_EXCLUSIVE, CONF_XS_PRIORITY, CONF_XS_MAX_ATOM_NUMBER, @@ -188,6 +193,7 @@ def getStr(cls, typeSpec: Enum): vol.Optional(CONF_XS_PRIORITY): vol.Coerce(float), vol.Optional(CONF_XS_MAX_ATOM_NUMBER): vol.Coerce(int), vol.Optional(CONF_MIN_DRIVER_DENSITY): vol.Coerce(float), + vol.Optional(CONF_COMPONENT_AVERAGING): bool, } ) @@ -456,6 +462,7 @@ def __init__( xsExecuteExclusive=None, xsPriority=None, xsMaxAtomNumber=None, + averageByComponent=False, minDriverDensity=0.0, ): self.xsID = xsID @@ -478,6 +485,7 @@ def __init__( self.meshSubdivisionsPerCm = meshSubdivisionsPerCm self.xsMaxAtomNumber = xsMaxAtomNumber self.minDriverDensity = minDriverDensity + self.averageByComponent = averageByComponent # these are related to execution self.xsExecuteExclusive = xsExecuteExclusive self.xsPriority = xsPriority @@ -688,6 +696,7 @@ def setDefaults(self, blockRepresentation, validBlockTypes): defaults[CONF_XS_EXECUTE_EXCLUSIVE] = False defaults[CONF_XS_PRIORITY] = 5 + defaults[CONF_COMPONENT_AVERAGING] = False for attrName, defaultValue in defaults.items(): currentValue = getattr(self, attrName) diff --git a/armi/physics/neutronics/tests/test_crossSectionManager.py b/armi/physics/neutronics/tests/test_crossSectionManager.py index 2282651c3..f6c1a0404 100644 --- a/armi/physics/neutronics/tests/test_crossSectionManager.py +++ b/armi/physics/neutronics/tests/test_crossSectionManager.py @@ -21,6 +21,7 @@ import copy import os import unittest +from unittest.mock import MagicMock from six.moves import cPickle @@ -43,8 +44,9 @@ ) from armi.reactor.blocks import HexBlock from armi.reactor.flags import Flags -from armi.reactor.tests import test_reactors +from armi.reactor.tests import test_reactors, test_blocks from armi.tests import TEST_ROOT +from armi.tests import mockRunLogs from armi.utils import units from armi.utils.directoryChangers import TemporaryDirectoryChanger @@ -97,28 +99,238 @@ def test_createRepresentativeBlock(self): class TestBlockCollectionAverage(unittest.TestCase): - def setUp(self): + @classmethod + def setUpClass(cls): fpFactory = test_lumpedFissionProduct.getDummyLFPFile() - self.blockList = makeBlocks(5) - for bi, b in enumerate(self.blockList): + cls.blockList = makeBlocks(5) + for bi, b in enumerate(cls.blockList): b.setType("fuel") b.p.percentBu = bi / 4.0 * 100 b.setLumpedFissionProducts(fpFactory.createLFPsFromFile()) + # put some trace Fe-56 and Na-23 into the fuel + # zero out all fuel nuclides except U-235 (for mass-weighting of component temperature) + fuelComp = b.getComponent(Flags.FUEL) + for nuc in fuelComp.getNuclides(): + b.setNumberDensity(nuc, 0.0) b.setNumberDensity("U235", bi) + fuelComp.setNumberDensity("FE56", 1e-15) + fuelComp.setNumberDensity("NA23", 1e-15) b.p.gasReleaseFraction = bi * 2 / 8.0 + for c in b: + if c.hasFlags(Flags.FUEL): + c.temperatureInC = 600.0 + bi + elif c.hasFlags([Flags.CLAD, Flags.DUCT, Flags.WIRE]): + c.temperatureInC = 500.0 + bi + elif c.hasFlags([Flags.BOND, Flags.COOLANT, Flags.INTERCOOLANT]): + c.temperatureInC = 400.0 + bi + + def setUp(self): self.bc = AverageBlockCollection( self.blockList[0].r.blueprints.allNuclidesInProblem ) self.bc.extend(self.blockList) + self.bc.averageByComponent = True + + def test_performAverageByComponent(self): + """ + Check the averageByComponent attribute + """ + self.bc._checkBlockSimilarity = MagicMock(return_value=True) + self.assertTrue(self.bc._performAverageByComponent()) + self.bc.averageByComponent = False + self.assertFalse(self.bc._performAverageByComponent()) + + def test_checkBlockSimilarity(self): + """ + Check the block similarity test + """ + self.assertTrue(self.bc._checkBlockSimilarity()) + self.bc.append(test_blocks.loadTestBlock()) + self.assertFalse(self.bc._checkBlockSimilarity()) def test_createRepresentativeBlock(self): + """ + Test creation of a representative block + """ avgB = self.bc.createRepresentativeBlock() self.assertNotIn(avgB, self.bc) - # 0 + 1/4 + 2/4 + 3/4 + 4/4 = - # (0 + 1 + 2 + 3 + 4 ) / 5 = 10/5 = 2.0 + # (0 + 1 + 2 + 3 + 4) / 5 = 10/5 = 2.0 self.assertAlmostEqual(avgB.getNumberDensity("U235"), 2.0) + # (0 + 1/4 + 2/4 + 3/4 + 4/4) / 5 * 100.0 = 50.0 self.assertEqual(avgB.p.percentBu, 50.0) + # check that a new block collection of the representative block has right temperatures + # this is required for Doppler coefficient calculations + newBc = AverageBlockCollection( + self.blockList[0].r.blueprints.allNuclidesInProblem + ) + newBc.append(avgB) + newBc.calcAvgNuclideTemperatures() + self.assertAlmostEqual(newBc.avgNucTemperatures["U235"], 603.0) + self.assertAlmostEqual(newBc.avgNucTemperatures["FE56"], 502.0) + self.assertAlmostEqual(newBc.avgNucTemperatures["NA23"], 402.0) + + def test_createRepresentativeBlockDissimilar(self): + """ + Test creation of a representative block from a collection with dissimilar blocks + """ + + uniqueBlock = test_blocks.loadTestBlock() + uniqueBlock.p.percentBu = 50.0 + fpFactory = test_lumpedFissionProduct.getDummyLFPFile() + uniqueBlock.setLumpedFissionProducts(fpFactory.createLFPsFromFile()) + uniqueBlock.setNumberDensity("U235", 2.0) + uniqueBlock.p.gasReleaseFraction = 1.0 + for c in uniqueBlock: + if c.hasFlags(Flags.FUEL): + c.temperatureInC = 600.0 + elif c.hasFlags([Flags.CLAD, Flags.DUCT, Flags.WIRE]): + c.temperatureInC = 500.0 + elif c.hasFlags([Flags.BOND, Flags.COOLANT, Flags.INTERCOOLANT]): + c.temperatureInC = 400.0 + self.bc.append(uniqueBlock) + + with mockRunLogs.BufferLog() as mock: + avgB = self.bc.createRepresentativeBlock() + self.assertIn( + "Non-matching block in AverageBlockCollection", mock.getStdout() + ) + + self.assertNotIn(avgB, self.bc) + # (0 + 1 + 2 + 3 + 4 + 2) / 6.0 = 12/6 = 2.0 + self.assertAlmostEqual(avgB.getNumberDensity("U235"), 2.0) + # (0 + 1/4 + 2/4 + 3/4 + 4/4) / 5 * 100.0 = 50.0 + self.assertAlmostEqual(avgB.p.percentBu, 50.0) + + # U35 has different average temperature because blocks have different U235 content + newBc = AverageBlockCollection( + self.blockList[0].r.blueprints.allNuclidesInProblem + ) + newBc.append(avgB) + newBc.calcAvgNuclideTemperatures() + # temps expected to be proportional to volume-fraction weighted temperature + # this is a non-physical result, but it demonstrates a problem that exists in the code + # when dissimilar blocks are put together in a BlockCollection + structureVolume = sum( + c.getVolume() + for c in avgB.getComponents([Flags.CLAD, Flags.DUCT, Flags.WIRE]) + ) + fuelVolume = avgB.getComponent(Flags.FUEL).getVolume() + coolantVolume = sum( + c.getVolume() + for c in avgB.getComponents([Flags.BOND, Flags.COOLANT, Flags.INTERCOOLANT]) + ) + expectedIronTemp = (structureVolume * 500.0 + fuelVolume * 600.0) / ( + structureVolume + fuelVolume + ) + expectedSodiumTemp = (coolantVolume * 400.0 + fuelVolume * 600.0) / ( + coolantVolume + fuelVolume + ) + self.assertAlmostEqual(newBc.avgNucTemperatures["U235"], 600.0) + self.assertAlmostEqual(newBc.avgNucTemperatures["FE56"], expectedIronTemp) + self.assertAlmostEqual(newBc.avgNucTemperatures["NA23"], expectedSodiumTemp) + + +class TestComponentAveraging(unittest.TestCase): + @classmethod + def setUpClass(cls): + fpFactory = test_lumpedFissionProduct.getDummyLFPFile() + cls.blockList = makeBlocks(3) + for bi, b in enumerate(cls.blockList): + b.setType("fuel") + b.setLumpedFissionProducts(fpFactory.createLFPsFromFile()) + # put some trace Fe-56 and Na-23 into the fuel + # zero out all fuel nuclides except U-235 (for mass-weighting of component temperature) + for nuc in b.getNuclides(): + b.setNumberDensity(nuc, 0.0) + b.setNumberDensity("U235", bi) + b.setNumberDensity("FE56", bi / 2.0) + b.setNumberDensity("NA23", bi / 3.0) + for c in b: + if c.hasFlags(Flags.FUEL): + c.temperatureInC = 600.0 + bi + elif c.hasFlags([Flags.CLAD, Flags.DUCT, Flags.WIRE]): + c.temperatureInC = 500.0 + bi + elif c.hasFlags([Flags.BOND, Flags.COOLANT, Flags.INTERCOOLANT]): + c.temperatureInC = 400.0 + bi + + def setUp(self): + self.bc = AverageBlockCollection( + self.blockList[0].r.blueprints.allNuclidesInProblem + ) + blockCopies = [copy.deepcopy(b) for b in self.blockList] + self.bc.extend(blockCopies) + + def test_getAverageComponentNumberDensities(self): + """ + Test component number density averaging + """ + # becaue of the way densities are set up, the middle block (index 1 of 0-2) component densities are equivalent to the average + b = self.bc[1] + for compIndex, c in enumerate(b.getComponents()): + avgDensities = self.bc._getAverageComponentNumberDensities(compIndex) + compDensities = c.getNumberDensities() + for nuc in c.getNuclides(): + self.assertAlmostEqual( + compDensities[nuc], + avgDensities[nuc], + msg=f"{nuc} density {compDensities[nuc]} not equal to {avgDensities[nuc]}!", + ) + + def test_getAverageComponentTemperature(self): + """ + Test mass-weighted component temperature averaging + """ + b = self.bc[0] + massWeightedIncrease = 5.0 / 3.0 + baseTemps = [600, 400, 500, 500, 400, 500, 400] + expectedTemps = [t + massWeightedIncrease for t in baseTemps] + for compIndex, c in enumerate(b.getComponents()): + avgTemp = self.bc._getAverageComponentTemperature(compIndex) + self.assertAlmostEqual( + expectedTemps[compIndex], + avgTemp, + msg=f"{c} avg temperature {avgTemp} not equal to expected {expectedTemps[compIndex]}!", + ) + + def test_getAverageComponentTemperatureVariedWeights(self): + """ + Test mass-weighted component temperature averaging with variable weights + """ + # make up a fake weighting with power param + self.bc.weightingParam = "power" + for i, b in enumerate(self.bc): + b.p.power = i + weightedIncrease = 1.8 + baseTemps = [600, 400, 500, 500, 400, 500, 400] + expectedTemps = [t + weightedIncrease for t in baseTemps] + for compIndex, c in enumerate(b.getComponents()): + avgTemp = self.bc._getAverageComponentTemperature(compIndex) + self.assertAlmostEqual( + expectedTemps[compIndex], + avgTemp, + msg=f"{c} avg temperature {avgTemp} not equal to expected {expectedTemps[compIndex]}!", + ) + + def test_getAverageComponentTemperatureNoMass(self): + """ + Test component temperature averaging when the components have no mass + """ + for b in self.bc: + for nuc in b.getNuclides(): + b.setNumberDensity(nuc, 0.0) + + unweightedIncrease = 1.0 + baseTemps = [600, 400, 500, 500, 400, 500, 400] + expectedTemps = [t + unweightedIncrease for t in baseTemps] + for compIndex, c in enumerate(b.getComponents()): + avgTemp = self.bc._getAverageComponentTemperature(compIndex) + self.assertAlmostEqual( + expectedTemps[compIndex], + avgTemp, + msg=f"{c} avg temperature {avgTemp} not equal to expected {expectedTemps[compIndex]}!", + ) + class TestBlockCollectionComponentAverage(unittest.TestCase): r"""tests for ZPPR 1D XS gen cases.""" @@ -442,10 +654,11 @@ def _makeComponents(self, multiplicity, densities): class TestBlockCollectionFluxWeightedAverage(unittest.TestCase): - def setUp(self): + @classmethod + def setUpClass(cls): fpFactory = test_lumpedFissionProduct.getDummyLFPFile() - self.blockList = makeBlocks(5) - for bi, b in enumerate(self.blockList): + cls.blockList = makeBlocks(5) + for bi, b in enumerate(cls.blockList): b.setType("fuel") b.p.percentBu = bi / 4.0 * 100 b.setLumpedFissionProducts(fpFactory.createLFPsFromFile()) @@ -453,6 +666,7 @@ def setUp(self): b.p.gasReleaseFraction = bi * 2 / 8.0 b.p.flux = bi + 1 + def setUp(self): self.bc = FluxWeightedAverageBlockCollection( self.blockList[0].r.blueprints.allNuclidesInProblem ) diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst index 62bed0fc6..10dabfde9 100644 --- a/doc/release/0.2.rst +++ b/doc/release/0.2.rst @@ -12,6 +12,7 @@ What's new in ARMI #. ARMI now mandates ``ruff`` linting. (`PR#1419 `_) #. Removed all old ARMI requirements, to start the work fresh. (`PR#1438 `_) #. Downgrading Draft PRs as policy. (`PR#1444 `_) +#. Attempt to set representative block number densities by component if possible. (`PR#1412 `_) #. TBD Bug fixes @@ -308,4 +309,3 @@ Bug fixes #. Fixed order of operations issue in rotatePins #. Fixed incorrect multiplicity for non-grid block components #. Many additional bugfixes and cleanups (see PR list) - From 753660d71f3777680d541ff5d042d2d71100f4a3 Mon Sep 17 00:00:00 2001 From: Michael Jarrett Date: Wed, 25 Oct 2023 08:52:54 -0700 Subject: [PATCH 032/176] Add nucTempHelper to CylindricalComponentsAverageBlockCollection (#1363) --- .../neutronics/crossSectionGroupManager.py | 13 +++++++++++++ .../neutronics/tests/test_crossSectionManager.py | 16 ++++++++++++++++ doc/release/0.2.rst | 7 +++++++ 3 files changed, 36 insertions(+) diff --git a/armi/physics/neutronics/crossSectionGroupManager.py b/armi/physics/neutronics/crossSectionGroupManager.py index 11a7fb535..b9ee666a5 100644 --- a/armi/physics/neutronics/crossSectionGroupManager.py +++ b/armi/physics/neutronics/crossSectionGroupManager.py @@ -614,6 +614,19 @@ def _orderComponentsInGroup(self, repBlock): componentLists = [list(sorted(b)) for b in self.getCandidateBlocks()] return [list(comps) for comps in zip(*componentLists)] + def _getNucTempHelper(self): + """All candidate blocks are used in the average.""" + nvt = numpy.zeros(len(self.allNuclidesInProblem)) + nv = numpy.zeros(len(self.allNuclidesInProblem)) + for block in self.getCandidateBlocks(): + wt = self.getWeight(block) + nvtBlock, nvBlock = getBlockNuclideTemperatureAvgTerms( + block, self.allNuclidesInProblem + ) + nvt += nvtBlock * wt + nv += nvBlock * wt + return nvt, nv + class SlabComponentsAverageBlockCollection(BlockCollection): """ diff --git a/armi/physics/neutronics/tests/test_crossSectionManager.py b/armi/physics/neutronics/tests/test_crossSectionManager.py index f6c1a0404..995100896 100644 --- a/armi/physics/neutronics/tests/test_crossSectionManager.py +++ b/armi/physics/neutronics/tests/test_crossSectionManager.py @@ -513,12 +513,16 @@ def test_ComponentAverage1DCylinder(self): self.assertEqual(xsOpt.blockRepresentation, "ComponentAverage1DCylinder") xsgm.createRepresentativeBlocks() + xsgm.updateNuclideTemperatures() + representativeBlockList = list(xsgm.representativeBlocks.values()) representativeBlockList.sort(key=lambda repB: repB.getMass() / repB.getVolume()) reprBlock = xsgm.representativeBlocks["ZA"] self.assertEqual(reprBlock.name, "1D_CYL_AVG_ZA") self.assertEqual(reprBlock.p.percentBu, 0.0) + refTemps = {"fuel": 600.0, "coolant": 450.0, "structure": 462.4565} + for c, compDensity, compArea in zip( reprBlock, self.expectedComponentDensities, self.expectedComponentAreas ): @@ -528,6 +532,18 @@ def test_ComponentAverage1DCylinder(self): self.assertAlmostEqual( c.getNumberDensity(nuc), compDensity.get(nuc, 0.0) ) + if "fuel" in c.getType(): + compTemp = refTemps["fuel"] + elif any(sodium in c.getType() for sodium in ["bond", "coolant"]): + compTemp = refTemps["coolant"] + else: + compTemp = refTemps["structure"] + self.assertAlmostEqual( + compTemp, + xsgm.avgNucTemperatures["ZA"][nuc], + 2, + f"{nuc} temperature does not match expected value of {compTemp}", + ) def test_checkComponentConsistency(self): diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst index 10dabfde9..883418687 100644 --- a/doc/release/0.2.rst +++ b/doc/release/0.2.rst @@ -38,8 +38,15 @@ What's new in ARMI Build changes ------------- #. Moved from ``setup.py`` to ``pyproject.toml``. (`PR#1409 `_) +#. Add python 3.11 to ARMI's CI testing GH actions! (`PR#1341 `_) +#. Put back ``avgFuelTemp`` block parameter. (`PR#1362 `_) +#. Make cylindrical component block collection less strict about pre-homogenization checks. (`PR#1347 `_) +#. Updated some parameter definitions and defaults. (`PR#1355 `_) +#. Make the SFP a child of the reactor so it is stored in database. (`PR#1349 `_) +#. Update black to version 22.6 (`PR#1396 `_) #. Added Python 3.11 to ARMI's CI on GH actions. (`PR#1341 `_) #. Updated ``black`` to version 22.6. (`PR#1396 `_) +#. Add a _getNucTempHelper method for CylindricalComponentsAverageBlockCollection. (`PR#1363 `_) Bug fixes --------- From eec8dbd36e097d239e39d51edb2d73773101e684 Mon Sep 17 00:00:00 2001 From: bdlafleur Date: Tue, 31 Oct 2023 13:30:00 -0400 Subject: [PATCH 033/176] Adding tests to the Parameters (#1454) --- .gitignore | 1 + .../parameters/parameterDefinitions.py | 6 +- armi/reactor/tests/test_parameters.py | 69 +++++++++++++++++++ 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 83bb002f7..2d6becc0d 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ pytestdebug.log dump-temp-* dump-tests* .vim-bookmarks +armi-venv/* venv*/ .mypy_cache/ **/__pycache__ diff --git a/armi/reactor/parameters/parameterDefinitions.py b/armi/reactor/parameters/parameterDefinitions.py index 458a849fc..51d298d10 100644 --- a/armi/reactor/parameters/parameterDefinitions.py +++ b/armi/reactor/parameters/parameterDefinitions.py @@ -147,8 +147,8 @@ class Serializer: Defining a Serializer for a Parameter in part defines the underlying representation of the data within a database file; the data stored in a database are sensitive to the code that wrote them. Changing the method that a Serializer - uses to pack or unpack data may break compatibility with old databse files. - Therefore, Serializers should be dilligent about signalling changes by updating + uses to pack or unpack data may break compatibility with old database files. + Therefore, Serializers should be diligent about signalling changes by updating their version. It is also good practice, whenever possible, to support reading old versions so that database files written by old versions can still be read. @@ -170,7 +170,7 @@ def pack(data: Sequence[any]) -> Tuple[numpy.ndarray, Dict[str, any]]: Given unpacked data, return packed data and a dictionary of attributes needed to unpack it. - The should perform the fundamental packing operation, returning the packed data + This should perform the fundamental packing operation, returning the packed data and any metadata ("attributes") that would be necessary to unpack the data. The class's version is always stored, so no need to provide it as an attribute. diff --git a/armi/reactor/tests/test_parameters.py b/armi/reactor/tests/test_parameters.py index 3c862bf1b..4449f3a0d 100644 --- a/armi/reactor/tests/test_parameters.py +++ b/armi/reactor/tests/test_parameters.py @@ -66,6 +66,75 @@ class Mock(parameters.ParameterCollection): with self.assertRaises(AssertionError): fail = pDefs.createBuilder(default={}) + def test_writeSomeParamsToDB(self): + """ + This test tests the ability to specify which parameters should be + written to the database. It assumes that the list returned by + ParameterDefinitionCollection.toWriteToDB() is used to filter for which + parameters to include in the database. + + .. test:: Test to restrict some parameters from being written to the database. + :id: T_ARMI_RESTRICT_DB_WRITE + :links: R_ARMI_RESTRICT_DB_WRITE + """ + + pDefs = parameters.ParameterDefinitionCollection() + with pDefs.createBuilder() as pb: + pb.defParam("write_me", "units", "description", "location", default=42) + pb.defParam("and_me", "units", "description", "location", default=42) + pb.defParam( + "dont_write_me", + "units", + "description", + "location", + default=42, + saveToDB=False, + ) + db_params = pDefs.toWriteToDB(32) + self.assertListEqual(["write_me", "and_me"], [p.name for p in db_params]) + + def test_serializer_pack_unpack(self): + """ + This tests the ability to add a serializer to a parameter instantiation line. + It assumes that if this parameter is not None, that the pack and unpack methods + will be called during storage to and reading from the database. See + database3._writeParams for an example use of this functionality. + + .. test:: Tests for ability to serialize data to database in a custom manner. + :id: T_ARMI_PARAM_SERIALIZE + :links: R_ARMI_PARAM_SERIALIZER + """ + + class TestSerializer(parameters.Serializer): + @staticmethod + def pack(data): + array = [d + 1 for d in data] + return array + + @staticmethod + def unpack(data): + array = [d - 1 for d in data] + return array + + param = parameters.Parameter( + name="myparam", + units="kg", + description="a param", + location=None, + saveToDB=True, + default=[1], + setter=None, + categories=None, + serializer=TestSerializer(), + ) + param.assigned = [1] + + packed = param.serializer.pack(param.assigned) + unpacked = param.serializer.unpack(packed) + + self.assertEqual(packed, [2]) + self.assertEqual(unpacked, [1]) + def test_paramPropertyDoesNotConflict(self): class Mock(parameters.ParameterCollection): pDefs = parameters.ParameterDefinitionCollection() From 9da65879499bda3b9f1e9829e3d9f935275eeaa7 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 31 Oct 2023 13:12:27 -0700 Subject: [PATCH 034/176] Cleaning up docstrings and formatting (#1455) --- armi/apps.py | 3 +-- armi/bookkeeping/snapshotInterface.py | 2 +- armi/cases/suite.py | 2 +- armi/cases/suiteBuilder.py | 1 - armi/operators/operator.py | 1 - armi/physics/executers.py | 5 ++--- armi/plugins.py | 1 - armi/reactor/assemblies.py | 1 - armi/reactor/blocks.py | 3 +-- armi/reactor/blueprints/__init__.py | 28 ++++++++++++--------------- armi/reactor/composites.py | 4 ---- armi/settings/caseSettings.py | 21 ++++++++------------ armi/utils/hexagon.py | 3 +-- armi/utils/textProcessors.py | 8 ++++---- armi/utils/units.py | 2 +- doc/developer/parallel_coding.rst | 8 +++++--- 16 files changed, 37 insertions(+), 56 deletions(-) diff --git a/armi/apps.py b/armi/apps.py index bce6cd63f..cdd83be0e 100644 --- a/armi/apps.py +++ b/armi/apps.py @@ -25,8 +25,7 @@ otherwise be global state. The ARMI Framework has historically made heavy use of global state (e.g., :py:mod:`armi.nucDirectory.nuclideBases`), and it will take quite a bit of effort to refactor the code to access such things through an App - object. We are planning to do this, but for now this App class is somewhat - rudimentary. + object. """ # ruff: noqa: E402 from typing import Dict, Optional, Tuple, List diff --git a/armi/bookkeeping/snapshotInterface.py b/armi/bookkeeping/snapshotInterface.py index 694df080d..e93353e11 100644 --- a/armi/bookkeeping/snapshotInterface.py +++ b/armi/bookkeeping/snapshotInterface.py @@ -28,8 +28,8 @@ Snapshots can be requested through the settings: ``dumpSnapshot`` and/or ``defaultSnapshots``. """ from armi import interfaces -from armi import runLog from armi import operators +from armi import runLog from armi.utils import getStepLengths diff --git a/armi/cases/suite.py b/armi/cases/suite.py index 9320cd359..f6aeba914 100644 --- a/armi/cases/suite.py +++ b/armi/cases/suite.py @@ -22,7 +22,7 @@ executed cases for post-analysis. ``CaseSuite``\ s should allow ``Cases`` to be added from totally separate directories. -This is useful for plugin-informed in-use testing as well as other things. +This is useful for plugin-informed testing as well as other things. See Also -------- diff --git a/armi/cases/suiteBuilder.py b/armi/cases/suiteBuilder.py index 9fff6ee58..e2ce1de57 100644 --- a/armi/cases/suiteBuilder.py +++ b/armi/cases/suiteBuilder.py @@ -246,7 +246,6 @@ def __init__(self, baseCase, noiseFraction): self.noiseFraction = noiseFraction def addDegreeOfFreedom(self, inputModifiers): - new = [] for newMod in inputModifiers: for existingModSet in self.modifierSets: diff --git a/armi/operators/operator.py b/armi/operators/operator.py index 4d1888b07..48a83e19e 100644 --- a/armi/operators/operator.py +++ b/armi/operators/operator.py @@ -605,7 +605,6 @@ def interactAllEveryNode(self, cycle, tn, excludedInterfaceNames=None): The time node that is currently being run (0 for BOC, etc.) excludedInterfaceNames : list, optional Names of interface names that will not be interacted with. - """ excludedInterfaceNames = excludedInterfaceNames or () activeInterfaces = [ diff --git a/armi/physics/executers.py b/armi/physics/executers.py index b4e8db636..e9e755c74 100644 --- a/armi/physics/executers.py +++ b/armi/physics/executers.py @@ -17,13 +17,12 @@ They may involve external codes (with inputs/execution/output) or in-memory data pathways. """ - -import os import hashlib +import os -from armi.utils import directoryChangers, pathTools from armi import runLog from armi.context import getFastPath, MPI_RANK +from armi.utils import directoryChangers, pathTools class ExecutionOptions: diff --git a/armi/plugins.py b/armi/plugins.py index 5bdc51560..d128bcddb 100644 --- a/armi/plugins.py +++ b/armi/plugins.py @@ -59,7 +59,6 @@ Warning ------- - The plugin system was developed to support improved collaboration. It is new and should be considered under development. The API is subject to change as the version of the ARMI framework approaches 1.0. diff --git a/armi/reactor/assemblies.py b/armi/reactor/assemblies.py index a0dbc5a41..529a645ad 100644 --- a/armi/reactor/assemblies.py +++ b/armi/reactor/assemblies.py @@ -785,7 +785,6 @@ def getBlocks(self, typeSpec=None, exact=False): ------- blocks : list List of blocks. - """ if typeSpec is None: return self.getChildren() diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py index a7b80150f..998e64202 100644 --- a/armi/reactor/blocks.py +++ b/armi/reactor/blocks.py @@ -719,10 +719,9 @@ def adjustDensity(self, frac, adjustList, returnMass=False): Returns ------- - mass : float + mass : float Mass difference in grams. If you subtract mass, mass will be negative. If returnMass is False (default), this will always be zero. - """ self._updateDetailedNdens(frac, adjustList) diff --git a/armi/reactor/blueprints/__init__.py b/armi/reactor/blueprints/__init__.py index 7031afab1..c432c4543 100644 --- a/armi/reactor/blueprints/__init__.py +++ b/armi/reactor/blueprints/__init__.py @@ -81,12 +81,19 @@ from armi import plugins from armi import runLog from armi.nucDirectory import nuclideBases +from armi.physics.neutronics.settings import CONF_LOADING_FILE from armi.reactor import assemblies from armi.reactor import geometry from armi.reactor import systemLayoutInput +from armi.reactor.blueprints import isotopicOptions +from armi.reactor.blueprints.assemblyBlueprint import AssemblyKeyedList +from armi.reactor.blueprints.blockBlueprint import BlockKeyedList +from armi.reactor.blueprints.componentBlueprint import ComponentGroups +from armi.reactor.blueprints.componentBlueprint import ComponentKeyedList +from armi.reactor.blueprints.gridBlueprint import Grids, Triplet +from armi.reactor.blueprints.reactorBlueprint import Systems, SystemBlueprint +from armi.reactor.converters import axialExpansionChanger from armi.reactor.flags import Flags -from armi.utils.customExceptions import InputError -from armi.utils import textProcessors from armi.settings.fwSettings.globalSettings import ( CONF_DETAILED_AXIAL_EXPANSION, CONF_ASSEM_FLAGS_SKIP_AXIAL_EXP, @@ -95,26 +102,15 @@ CONF_ACCEPTABLE_BLOCK_AREA_ERROR, CONF_GEOM_FILE, ) -from armi.physics.neutronics.settings import CONF_LOADING_FILE - -# NOTE: using non-ARMI-standard imports because these are all a part of this package, -# and using the module imports would make the attribute definitions extremely long -# without adding detail -from armi.reactor.blueprints import isotopicOptions -from armi.reactor.blueprints.assemblyBlueprint import AssemblyKeyedList -from armi.reactor.blueprints.blockBlueprint import BlockKeyedList -from armi.reactor.blueprints.componentBlueprint import ComponentGroups -from armi.reactor.blueprints.componentBlueprint import ComponentKeyedList -from armi.reactor.blueprints.gridBlueprint import Grids, Triplet -from armi.reactor.blueprints.reactorBlueprint import Systems, SystemBlueprint -from armi.reactor.converters import axialExpansionChanger +from armi.utils import textProcessors +from armi.utils.customExceptions import InputError context.BLUEPRINTS_IMPORTED = True context.BLUEPRINTS_IMPORT_CONTEXT = "".join(traceback.format_stack()) def loadFromCs(cs, roundTrip=False): - r"""Function to load Blueprints based on supplied ``CaseSettings``.""" + """Function to load Blueprints based on supplied ``CaseSettings``.""" from armi.utils import directoryChangers with directoryChangers.DirectoryChanger(cs.inputDirectory, dumpOnException=False): diff --git a/armi/reactor/composites.py b/armi/reactor/composites.py index 92b5ad297..1d3a8092b 100644 --- a/armi/reactor/composites.py +++ b/armi/reactor/composites.py @@ -63,7 +63,6 @@ class FlagSerializer(parameters.Serializer): sequence of enough uint8 elements to represent all flags. These constitute a dimension of a 2-D numpy array containing all Flags for all objects provided to the ``pack()`` function. - """ version = "1" @@ -437,12 +436,9 @@ def __bool__(self): to be considered non-zero even if they don't have any blocks. This is important for parent resolution, etc. If one of these objects exists, it is non-zero, regardless of its contents. - """ return True - __nonzero__ = __bool__ # Python 2 compatibility - def __add__(self, other): """Return a list of all children in this and another object.""" return self.getChildren() + other.getChildren() diff --git a/armi/settings/caseSettings.py b/armi/settings/caseSettings.py index 1ce2dd426..6713eca38 100644 --- a/armi/settings/caseSettings.py +++ b/armi/settings/caseSettings.py @@ -14,15 +14,12 @@ """ This defines a Settings object that acts mostly like a dictionary. It -is meant to be treated mostly like a singleton, where each custom ARMI -object has access to it. It contains global user settings like the core -power level, the input file names, the number of cycles to run, the run type, -the environment setup, and hundreds of other things. +is meant so that each ARMI run has one-and-only-one Settings object. It records +user settings like the core power level, the input file names, the number of cycles to +run, the run type, the environment setup, and hundreds of other things. -A settings object can be saved as or loaded from an YAML file. The ARMI GUI is designed to +A Settings object can be saved as or loaded from an YAML file. The ARMI GUI is designed to create this settings file, which is then loaded by an ARMI process on the cluster. - -A primary case settings is created as ``masterCs``. """ import io import logging @@ -37,8 +34,6 @@ from armi.utils import pathTools from armi.utils.customExceptions import NonexistentSetting -DEP_WARNING = "Deprecation Warning: Settings will not be mutable mid-run: {}" - SIMPLE_CYCLES_INPUTS = { "availabilityFactor", "availabilityFactors", @@ -51,12 +46,12 @@ class Settings: """ - A container for global settings, such as case title, power level, and many run options. + A container for run settings, such as case title, power level, and many more. It is accessible to most ARMI objects through self.cs (for 'Case Settings'). It acts largely as a dictionary, and setting values are accessed by keys. - The settings object has a 1-to-1 correspondence with the ARMI settings input file. + The Settings object has a 1-to-1 correspondence with the ARMI settings input file. This file may be created by hand or by the GUI in submitter.py. Notes @@ -68,7 +63,7 @@ class Settings: def __init__(self, fName=None): """ - Instantiate a settings object. + Instantiate a Settings object. Parameters ---------- @@ -426,7 +421,7 @@ def writeToYamlStream(self, stream, style="short", settingsSetByUser=[]): return writer def updateEnvironmentSettingsFrom(self, otherCs): - r"""Updates the environment settings in this object based on some other cs + """Updates the environment settings in this object based on some other cs (from the GUI, most likely). Parameters diff --git a/armi/utils/hexagon.py b/armi/utils/hexagon.py index bddf8e44d..643b8d670 100644 --- a/armi/utils/hexagon.py +++ b/armi/utils/hexagon.py @@ -19,7 +19,6 @@ .. image:: /.static/hexagon.png :width: 100% - """ import math @@ -83,7 +82,7 @@ def pitch(side): def numRingsToHoldNumCells(numCells): - r""" + """ Determine the number of rings in a hexagonal grid with this many hex cells. If the number of pins don't fit exactly into any ring, returns the ring just large enough to fit them. diff --git a/armi/utils/textProcessors.py b/armi/utils/textProcessors.py index fac768460..de3760627 100644 --- a/armi/utils/textProcessors.py +++ b/armi/utils/textProcessors.py @@ -12,9 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. """Utility classes and functions for manipulating text files.""" +import io import os import re -import io import pathlib from typing import List, Tuple, Union, Optional, TextIO @@ -554,7 +554,7 @@ def __init__(self, fname, highMem=False): self.f = SmartList(f) def reset(self): - r"""Rewinds the file so you can search through it again.""" + """Rewinds the file so you can search through it again.""" self.f.seek(0) def __repr__(self): @@ -567,7 +567,7 @@ def checkErrors(self, line): pass def fsearch(self, pattern, msg=None, killOn=None, textFlag=False): - r""" + """ Searches file f for pattern and displays msg when found. Returns line in which pattern is found or FALSE if no pattern is found. Stops searching if finds killOn first. @@ -619,7 +619,7 @@ def fsearch(self, pattern, msg=None, killOn=None, textFlag=False): class SmartList: - r"""A list that does stuff like files do i.e. remembers where it was, can seek, etc. + """A list that does stuff like files do i.e. remembers where it was, can seek, etc. Actually this is pretty slow. so much for being smart. nice idea though. """ diff --git a/armi/utils/units.py b/armi/utils/units.py index 8e3447ddf..012654588 100644 --- a/armi/utils/units.py +++ b/armi/utils/units.py @@ -316,7 +316,7 @@ def sanitizeAngle(theta): def getXYLineParameters(theta, x=0, y=0): """ - returns parameters A B C D for a plane in the XY direction. + Returns parameters A B C D for a plane in the XY direction. Parameters ---------- diff --git a/doc/developer/parallel_coding.rst b/doc/developer/parallel_coding.rst index b89556684..ad3d5b779 100644 --- a/doc/developer/parallel_coding.rst +++ b/doc/developer/parallel_coding.rst @@ -61,10 +61,12 @@ CPUs, you can either pass the first 10 values out of the list and keep sending g are all sent (multiple sets of transmitions) or you can split the data up into 10 evenly-populated groups (single transmition to each CPU). This is called *load balancing*. -ARMI has utilities that can help called :py:func:`armi.utils.chunks` and :py:func:`armi.iterables.flatten`. -Given an arbitrary list, ``chunks`` breaks it up into a certain number of chunks and ``unchunk`` does the +ARMI has utilities that can help called :py:func:`armi.utils.iterables.chunk` and :py:func:`armi.utils.iterables.flatten`. +Given an arbitrary list, ``chunk`` breaks it up into a certain number of chunks and ``unchunk`` does the opposite to reassemble the original list after processing. Check it out:: + from armi.utils import iterables + if rank == 0: # primary. Make data and send it. workListLoadBalanced = iterables.split(workList, nCpu, padWith=()) @@ -130,7 +132,7 @@ MPI transmit the results, they will not survive on the primary node. For instanc a block parameter (e.g. ``b.p.paramName = 10.0)``, these **will not** be set on the primary! There are a few mechanisms that can help you get the data back to the primary reactor. -.. note:: If you want similar capabilities for objects that are not blocks, take another look at :py:func:`armi.utils.chunks`. +.. note:: If you want similar capabilities for objects that are not blocks, take another look at :py:func:`armi.utils.iterables.chunk`. Example using ``bcast`` From 4acefa8174a0023664afeca3e098fcb87ef76d80 Mon Sep 17 00:00:00 2001 From: bdlafleur Date: Thu, 2 Nov 2023 16:12:44 -0400 Subject: [PATCH 035/176] Adding tests to Parameter package (#1457) --- armi/reactor/composites.py | 4 +++ .../parameters/parameterDefinitions.py | 8 +++++ armi/reactor/tests/test_parameters.py | 30 +++++++++++-------- armi/reactor/tests/test_reactors.py | 30 +++++++++++++++++++ 4 files changed, 60 insertions(+), 12 deletions(-) diff --git a/armi/reactor/composites.py b/armi/reactor/composites.py index 1d3a8092b..58841fde0 100644 --- a/armi/reactor/composites.py +++ b/armi/reactor/composites.py @@ -296,6 +296,10 @@ class ArmiObject(metaclass=CompositeModelType): specialized subclasses (Block, Assembly) in preparation for this next step. As a result, the public API on this method should be considered unstable. + .. impl:: Parameters accessible throughout the armi tree + :id: I_ARMI_PARAM_PART + :implements: R_ARMI_PARAM_PART + Attributes ---------- name : str diff --git a/armi/reactor/parameters/parameterDefinitions.py b/armi/reactor/parameters/parameterDefinitions.py index 51d298d10..e4049f978 100644 --- a/armi/reactor/parameters/parameterDefinitions.py +++ b/armi/reactor/parameters/parameterDefinitions.py @@ -152,6 +152,10 @@ class Serializer: their version. It is also good practice, whenever possible, to support reading old versions so that database files written by old versions can still be read. + .. impl:: Custom parameter serializer + :id: I_ARMI_PARAM_SERIALIZE + :implements: R_ARMI_PARAM_SERIALIZE + See Also -------- armi.bookkeeping.db.database3.packSpecialData @@ -575,6 +579,10 @@ def toWriteToDB(self, assignedMask: Optional[int] = None): """ Get a list of acceptable parameters to store to the database for a level of the data model. + .. impl:: Filter parameters to write to DB + :id: I_ARMI_PARAM_DB + :implements: R_ARMI_PARAM_DB + Parameters ---------- assignedMask : int diff --git a/armi/reactor/tests/test_parameters.py b/armi/reactor/tests/test_parameters.py index 4449f3a0d..c2c01ca8f 100644 --- a/armi/reactor/tests/test_parameters.py +++ b/armi/reactor/tests/test_parameters.py @@ -68,16 +68,15 @@ class Mock(parameters.ParameterCollection): def test_writeSomeParamsToDB(self): """ - This test tests the ability to specify which parameters should be + This tests the ability to specify which parameters should be written to the database. It assumes that the list returned by ParameterDefinitionCollection.toWriteToDB() is used to filter for which parameters to include in the database. - .. test:: Test to restrict some parameters from being written to the database. - :id: T_ARMI_RESTRICT_DB_WRITE - :links: R_ARMI_RESTRICT_DB_WRITE + .. test:: Restrict parameters from DB write + :id: T_ARMI_PARAM_DB + :tests: R_ARMI_PARAM_DB """ - pDefs = parameters.ParameterDefinitionCollection() with pDefs.createBuilder() as pb: pb.defParam("write_me", "units", "description", "location", default=42) @@ -100,9 +99,9 @@ def test_serializer_pack_unpack(self): will be called during storage to and reading from the database. See database3._writeParams for an example use of this functionality. - .. test:: Tests for ability to serialize data to database in a custom manner. + .. test:: Custom parameter serializer :id: T_ARMI_PARAM_SERIALIZE - :links: R_ARMI_PARAM_SERIALIZER + :tests: R_ARMI_PARAM_SERIALIZE """ class TestSerializer(parameters.Serializer): @@ -373,7 +372,8 @@ class MockPCChild(MockPCParent): _ = MockPCChild() - # same name along a different branch from the base ParameterCollection should be fine + # same name along a different branch from the base ParameterCollection should + # be fine class MockPCUncle(parameters.ParameterCollection): pDefs = parameters.ParameterDefinitionCollection() with pDefs.createBuilder() as pb: @@ -445,7 +445,9 @@ class MockPC(parameters.ParameterCollection): self.assertEqual(set(pc.paramDefs.inCategory("bacon")), set([p2, p3])) def test_parameterCollectionsHave__slots__(self): - """Make sure something is implemented to prevent accidental creation of attributes.""" + """Make sure something is implemented to prevent accidental creation of + attributes. + """ self.assertEqual( set(["_hist", "_backup", "assigned", "_p_serialNum", "serialNum"]), set(parameters.ParameterCollection._slots), @@ -511,7 +513,9 @@ def makeComp(name): class SynchronizationTests: - """Some unit tests that must be run with mpirun instead of the standard unittest system.""" + """Some unit tests that must be run with mpirun instead of the standard unittest + system. + """ def setUp(self): self.r = makeComp("reactor") @@ -633,7 +637,8 @@ def mpitest_noConflictsMaintainWithStateRetainer(self): # confirm outside state retainer self.assertEqual(assigned, [c.p.assigned for ci, c in enumerate(self.comps)]) - # this rank's "assigned" components are not assigned on the workers, and so will be updated + # this rank's "assigned" components are not assigned on the workers, and so will + # be updated self.assertEqual(len(self.comps), self.r.syncMpiState()) for ci, comp in enumerate(self.comps): @@ -707,7 +712,8 @@ def do(): param3 = self.r.p.paramDefs["param3"] def do_assert(passNum): - # ensure all assemblies and blocks set values for param2, but param1 is empty + # ensure all assemblies and blocks set values for param2, but param1 is + # empty for rank in range(context.MPI_SIZE): a = self.r.core[passNum * context.MPI_SIZE + rank] assert "param1" not in a.p diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 7e3fa9533..32e85bad5 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -255,6 +255,36 @@ def test_factorySortSetting(self): a1 = [a.name for a in r1.core] self.assertNotEqual(a0, a1) + def test_getSetParameters(self): + """ + This test works through multiple levels of the hierarchy to test ability to + modify parameters at different levels. + + .. test:: Parameters accessible throughout the armi tree + :id: T_ARMI_PARAM_PART + :tests: R_ARMI_PARAM_PART + """ + # Test at core level + core = self.r.core + self.assertGreater(core.p.power, -1) + + core.p.power = 123 + self.assertEqual(core.p.power, 123) + + # Test at assembly level + assembly = core.getFirstAssembly() + self.assertGreater(assembly.p.crRodLength, -1) + + assembly.p.crRodLength = 234 + self.assertEqual(assembly.p.crRodLength, 234) + + # Test at block level + block = core.getFirstBlock() + self.assertGreater(block.p.THTfuelCL, -1) + + block.p.THTfuelCL = 57 + self.assertEqual(block.p.THTfuelCL, 57) + def test_sortChildren(self): self.assertEqual(next(self.r.core.__iter__()), self.r.core[0]) self.assertEqual(self.r.core._children, sorted(self.r.core._children)) From 203f973f6affc9e081f540363792d205dc8976cf Mon Sep 17 00:00:00 2001 From: bdlafleur Date: Mon, 6 Nov 2023 13:09:05 -0500 Subject: [PATCH 036/176] Adding tests to blocks (#1459) --- armi/reactor/blocks.py | 81 +++++++++++++++++++++++++++++-- armi/reactor/tests/test_blocks.py | 78 +++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 4 deletions(-) diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py index 998e64202..a56f105e0 100644 --- a/armi/reactor/blocks.py +++ b/armi/reactor/blocks.py @@ -581,7 +581,12 @@ def adjustUEnrich(self, newEnrich): self.completeInitialLoading() def getLocation(self): - """Return a string representation of the location.""" + """Return a string representation of the location. + + .. impl:: Location of a block is retrievable + :id: I_ARMI_BLOCK_POSI + :implements: R_ARMI_BLOCK_POSI + """ if self.core and self.parent.spatialGrid and self.spatialLocator: return self.core.spatialGrid.getLabel( self.spatialLocator.getCompleteIndices() @@ -590,6 +595,13 @@ def getLocation(self): return "ExCore" def coords(self, rotationDegreesCCW=0.0): + """ + Returns the coordinates of the block. + + .. impl:: Coordinates of a block are queryable + :id: I_ARMI_BLOCK_POSI + :implements: R_ARMI_BLOCK_POSI + """ if rotationDegreesCCW: raise NotImplementedError("Cannot get coordinates with rotation.") return self.spatialLocator.getGlobalCoordinates() @@ -670,6 +682,10 @@ def getVolume(self): """ Return the volume of a block. + .. impl:: Volume of block is retrievable + :id: I_ARMI_BLOCK_DIMS + :implements: R_ARMI_BLOCK_DIMS + Returns ------- volume : float @@ -1231,6 +1247,10 @@ def getPitch(self, returnComp=False): Component that has the max pitch, if returnComp == True. If no component is found to define the pitch, returns None + .. impl:: Pitch of block is retrievable + :id: I_ARMI_BLOCK_DIMS + :implements: R_ARMI_BLOCK_DIMS + Notes ----- The block stores a reference to the component that defines the pitch, making the assumption @@ -1576,6 +1596,13 @@ def getPinCoordinates(self): class HexBlock(Block): + """ + Defines a HexBlock. + + .. impl:: Ability to create hex shaped blocks + :id: I_ARMI_BLOCK_HEX + :implements: R_ARMI_BLOCK_HEX + """ PITCH_COMPONENT_TYPE: ClassVar[_PitchDefiningComponent] = (components.Hexagon,) @@ -1595,6 +1622,10 @@ def _createHomogenizedCopy(self, pinSpatialLocators=False): """ Create a new homogenized copy of a block that is less expensive than a full deepcopy. + .. impl:: Homogenize the compositions of a block + :id: I_ARMI_BLOCK_HOMOG + :implements: R_ARMI_BLOCK_HOMOG + Notes ----- This can be used to improve performance when a new copy of a reactor needs to be @@ -1673,17 +1704,37 @@ def _createHomogenizedCopy(self, pinSpatialLocators=False): return b def getMaxArea(self): - """Compute the max area of this block if it was totally full.""" + """ + Compute the max area of this block if it was totally full. + + .. impl:: Area of block is retrievable + :id: I_ARMI_BLOCK_DIMS + :implements: R_ARMI_BLOCK_DIMS + """ pitch = self.getPitch() if not pitch: return 0.0 return hexagon.area(pitch) def getDuctIP(self): + """ + Returns the duct IP dimension. + + .. impl:: IP dimension is retrievable + :id: I_ARMI_BLOCK_DIMS + :implements: R_ARMI_BLOCK_DIMS + """ duct = self.getComponent(Flags.DUCT, exact=True) return duct.getDimension("ip") def getDuctOP(self): + """ + Returns the duct OP dimension. + + .. impl:: OP dimension is retrievable + :id: I_ARMI_BLOCK_DIMS + :implements: R_ARMI_BLOCK_DIMS + """ duct = self.getComponent(Flags.DUCT, exact=True) return duct.getDimension("op") @@ -1983,6 +2034,10 @@ def getPinToDuctGap(self, cold=False): """ Returns the distance in cm between the outer most pin and the duct in a block. + .. impl:: Pin to duct gap of block is retrievable + :id: I_ARMI_BLOCK_DIMS + :implements: R_ARMI_BLOCK_DIMS + Parameters ---------- cold : boolean @@ -2163,6 +2218,10 @@ def getPinPitch(self, cold=False): Assumes that the pin pitch is defined entirely by contacting cladding tubes and wire wraps. Grid spacers not yet supported. + .. impl:: Pin pitch within block is retrievable + :id: I_ARMI_BLOCK_DIMS + :implements: R_ARMI_BLOCK_DIMS + Parameters ---------- cold : boolean @@ -2194,7 +2253,12 @@ def getPinPitch(self, cold=False): ) def getWettedPerimeter(self): - """Return the total wetted perimeter of the block in cm.""" + """Return the total wetted perimeter of the block in cm. + + .. impl:: Wetted perimeter of block is retrievable + :id: I_ARMI_BLOCK_DIMS + :implements: R_ARMI_BLOCK_DIMS + """ # flags pertaining to hexagon components where the interior of the hexagon is wetted wettedHollowHexagonComponentFlags = ( Flags.DUCT, @@ -2267,7 +2331,12 @@ def getWettedPerimeter(self): ) def getFlowArea(self): - """Return the total flowing coolant area of the block in cm^2.""" + """Return the total flowing coolant area of the block in cm^2. + + .. impl:: Flow area of block is retrievable + :id: I_ARMI_BLOCK_DIMS + :implements: R_ARMI_BLOCK_DIMS + """ return self.getComponent(Flags.COOLANT, exact=True).getArea() def getHydraulicDiameter(self): @@ -2284,6 +2353,10 @@ def getHydraulicDiameter(self): p = sqrt(3)*s l = 6*p/sqrt(3) + + .. impl:: Hydraulic diameter of block is retrievable + :id: I_ARMI_BLOCK_DIMS + :implements: R_ARMI_BLOCK_DIMS """ return 4.0 * self.getFlowArea() / self.getWettedPerimeter() diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py index 3d1a0135d..c2f992c1b 100644 --- a/armi/reactor/tests/test_blocks.py +++ b/armi/reactor/tests/test_blocks.py @@ -473,6 +473,13 @@ def test_duplicate(self): self.assertEqual(self.block.p.flags, Block2.p.flags) def test_homogenizedMixture(self): + """ + Confirms homogenized blocks have correct properties. + + .. test:: Homogenize the compositions of a block + :id: T_ARMI_BLOCK_HOMOG + :tests: R_ARMI_BLOCK_HOMOG + """ args = [False, True] # pinSpatialLocator argument expectedShapes = [ [basicShapes.Hexagon], @@ -843,6 +850,13 @@ def test_adjustUEnrich(self): self.assertAlmostEqual(cur, ref, places=places) def test_setLocation(self): + """ + Retrieve a blocks location + + .. test:: Location of a block is retrievable + :id: T_ARMI_BLOCK_POSI + :tests: R_ARMI_BLOCK_POSI + """ b = self.block # a bit obvious, but location is a property now... i, j = grids.HexGrid.getIndicesFromRingAndPos(2, 3) @@ -1534,6 +1548,30 @@ def test_setPitch(self): moles3 = b.p.molesHmBOL self.assertAlmostEqual(moles2, moles3) + def test_setImportantParams(self): + """ + Confirm that important block parameters can be set and get. + """ + # Test ability to set and get flux + applyDummyData(self.block) + self.assertEqual(self.block.p.mgFlux[0], 161720716762.12997) + self.assertEqual(self.block.p.mgFlux[-1], 601494405.293505) + + # Test ability to set and get number density + fuel = self.block.getComponent(Flags.FUEL) + + u235_dens = fuel.getNumberDensity("U235") + self.assertEqual(u235_dens, 0.003695461770836022) + + fuel.setNumberDensity("U235", 0.5) + u235_dens = fuel.getNumberDensity("U235") + self.assertEqual(u235_dens, 0.5) + + # TH parameter test + self.assertEqual(0, self.block.p.THmassFlowRate) + self.block.p.THmassFlowRate = 10 + self.assertEqual(10, self.block.p.THmassFlowRate) + def test_getMfp(self): """Test mean free path.""" applyDummyData(self.block) @@ -1788,7 +1826,25 @@ def test_getArea(self): places = 6 self.assertAlmostEqual(cur, ref, places=places) + def test_component_type(self): + """ + Test that a hex block has the proper "hexagon" __name__. + + .. test: Ability to create hex shaped blocks + :id: T_ARMI_BLOCK_HEX + :tests: R_ARMI_BLOCK_HEX + """ + pitch_comp_type = self.HexBlock.PITCH_COMPONENT_TYPE[0] + self.assertEqual(pitch_comp_type.__name__, "Hexagon") + def test_coords(self): + """ + Test that coordinates are retrievable from a block. + + .. test: Coordinates of a block are queryable + :id: T_ARMI_BLOCK_POSI + :tests: R_ARMI_BLOCK_POSI + """ r = self.HexBlock.r a = self.HexBlock.parent loc1 = r.core.spatialGrid[0, 1, 0] @@ -1812,6 +1868,28 @@ def test_coords(self): def test_getNumPins(self): self.assertEqual(self.HexBlock.getNumPins(), 169) + def test_block_dims(self): + """ + Tests that the block class can provide basic dimensionality information about + itself. + + .. test: Retrieve important block dimensions + :id: T_ARMI_BLOCK_DIMS + :tests: R_ARMI_BLOCK_DIMS + """ + self.assertAlmostEqual(4316.582, self.HexBlock.getVolume(), 3) + self.assertAlmostEqual(70.6, self.HexBlock.getPitch(), 1) + self.assertAlmostEqual(4316.582, self.HexBlock.getMaxArea(), 3) + + self.assertEqual(70, self.HexBlock.getDuctIP()) + self.assertEqual(70.6, self.HexBlock.getDuctOP()) + + self.assertAlmostEqual(34.273, self.HexBlock.getPinToDuctGap(), 3) + self.assertEqual(0.11, self.HexBlock.getPinPitch()) + self.assertAlmostEqual(300.889, self.HexBlock.getWettedPerimeter(), 3) + self.assertAlmostEqual(4242.184, self.HexBlock.getFlowArea(), 3) + self.assertAlmostEqual(56.395, self.HexBlock.getHydraulicDiameter(), 3) + def test_symmetryFactor(self): # full hex self.HexBlock.spatialLocator = self.HexBlock.r.core.spatialGrid[2, 0, 0] From 96a5edef4a7575a32c21905e3a37bcabf04e3b89 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 7 Nov 2023 08:45:07 -0800 Subject: [PATCH 037/176] Making block impl tags unique (#1461) --- .../nuclearDataIO/tests/test_xsCollections.py | 4 ++ armi/reactor/blocks.py | 39 +++++++++++-------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/armi/nuclearDataIO/tests/test_xsCollections.py b/armi/nuclearDataIO/tests/test_xsCollections.py index 9c997031a..f8b1990f0 100644 --- a/armi/nuclearDataIO/tests/test_xsCollections.py +++ b/armi/nuclearDataIO/tests/test_xsCollections.py @@ -138,16 +138,20 @@ def r(self, r): self._r = r def getVolume(self, *args, **kwargs): + """Return the volume of a block.""" return 1.0 def getNuclideNumberDensities(self, nucNames): + """Return a list of number densities in atoms/barn-cm for the nuc names requested.""" return [self.density.get(nucName, 0.0) for nucName in nucNames] def _getNdensHelper(self): return {nucName: density for nucName, density in self.density.items()} def setNumberDensity(self, key, val, *args, **kwargs): + """Set the number density of this nuclide to this value.""" self.density[key] = val def getNuclides(self): + """Determine which nuclides are present in this armi block.""" return self.density.keys() diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py index a56f105e0..699c03ae5 100644 --- a/armi/reactor/blocks.py +++ b/armi/reactor/blocks.py @@ -584,7 +584,7 @@ def getLocation(self): """Return a string representation of the location. .. impl:: Location of a block is retrievable - :id: I_ARMI_BLOCK_POSI + :id: I_ARMI_BLOCK_POSI0 :implements: R_ARMI_BLOCK_POSI """ if self.core and self.parent.spatialGrid and self.spatialLocator: @@ -599,7 +599,7 @@ def coords(self, rotationDegreesCCW=0.0): Returns the coordinates of the block. .. impl:: Coordinates of a block are queryable - :id: I_ARMI_BLOCK_POSI + :id: I_ARMI_BLOCK_POSI1 :implements: R_ARMI_BLOCK_POSI """ if rotationDegreesCCW: @@ -607,7 +607,7 @@ def coords(self, rotationDegreesCCW=0.0): return self.spatialLocator.getGlobalCoordinates() def setBuLimitInfo(self): - r"""Sets burnup limit based on igniter, feed, etc.""" + """Sets burnup limit based on igniter, feed, etc.""" if self.p.buRate == 0: # might be cycle 1 or a non-burning block self.p.timeToLimit = 0.0 @@ -683,7 +683,7 @@ def getVolume(self): Return the volume of a block. .. impl:: Volume of block is retrievable - :id: I_ARMI_BLOCK_DIMS + :id: I_ARMI_BLOCK_DIMS0 :implements: R_ARMI_BLOCK_DIMS Returns @@ -1248,7 +1248,7 @@ def getPitch(self, returnComp=False): define the pitch, returns None .. impl:: Pitch of block is retrievable - :id: I_ARMI_BLOCK_DIMS + :id: I_ARMI_BLOCK_DIMS1 :implements: R_ARMI_BLOCK_DIMS Notes @@ -1262,7 +1262,6 @@ def getPitch(self, returnComp=False): See Also -------- setPitch : sets pitch - """ c, _p = self._pitchDefiningComponent if c is None: @@ -1549,8 +1548,8 @@ def rotate(self, rad): Parameters ---------- - rad - float - number (in radians) specifying the angle of counter clockwise rotation + rad: float + Number (in radians) specifying the angle of counter clockwise rotation. """ raise NotImplementedError @@ -1610,6 +1609,13 @@ def __init__(self, name, height=1.0): Block.__init__(self, name, height) def coords(self, rotationDegreesCCW=0.0): + """ + Returns the coordinates of the block. + + .. impl:: Coordinates of a block are queryable + :id: I_ARMI_BLOCK_POSI2 + :implements: R_ARMI_BLOCK_POSI + """ x, y, _z = self.spatialLocator.getGlobalCoordinates() x += self.p.displacementX * 100.0 y += self.p.displacementY * 100.0 @@ -1708,7 +1714,7 @@ def getMaxArea(self): Compute the max area of this block if it was totally full. .. impl:: Area of block is retrievable - :id: I_ARMI_BLOCK_DIMS + :id: I_ARMI_BLOCK_DIMS2 :implements: R_ARMI_BLOCK_DIMS """ pitch = self.getPitch() @@ -1721,7 +1727,7 @@ def getDuctIP(self): Returns the duct IP dimension. .. impl:: IP dimension is retrievable - :id: I_ARMI_BLOCK_DIMS + :id: I_ARMI_BLOCK_DIMS3 :implements: R_ARMI_BLOCK_DIMS """ duct = self.getComponent(Flags.DUCT, exact=True) @@ -1732,13 +1738,14 @@ def getDuctOP(self): Returns the duct OP dimension. .. impl:: OP dimension is retrievable - :id: I_ARMI_BLOCK_DIMS + :id: I_ARMI_BLOCK_DIMS4 :implements: R_ARMI_BLOCK_DIMS """ duct = self.getComponent(Flags.DUCT, exact=True) return duct.getDimension("op") def initializePinLocations(self): + """Initialize pin locations.""" nPins = self.getNumPins() self.p.pinLocation = list(range(1, nPins + 1)) @@ -2035,7 +2042,7 @@ def getPinToDuctGap(self, cold=False): Returns the distance in cm between the outer most pin and the duct in a block. .. impl:: Pin to duct gap of block is retrievable - :id: I_ARMI_BLOCK_DIMS + :id: I_ARMI_BLOCK_DIMS5 :implements: R_ARMI_BLOCK_DIMS Parameters @@ -2219,7 +2226,7 @@ def getPinPitch(self, cold=False): and wire wraps. Grid spacers not yet supported. .. impl:: Pin pitch within block is retrievable - :id: I_ARMI_BLOCK_DIMS + :id: I_ARMI_BLOCK_DIMS6 :implements: R_ARMI_BLOCK_DIMS Parameters @@ -2256,7 +2263,7 @@ def getWettedPerimeter(self): """Return the total wetted perimeter of the block in cm. .. impl:: Wetted perimeter of block is retrievable - :id: I_ARMI_BLOCK_DIMS + :id: I_ARMI_BLOCK_DIMS7 :implements: R_ARMI_BLOCK_DIMS """ # flags pertaining to hexagon components where the interior of the hexagon is wetted @@ -2334,7 +2341,7 @@ def getFlowArea(self): """Return the total flowing coolant area of the block in cm^2. .. impl:: Flow area of block is retrievable - :id: I_ARMI_BLOCK_DIMS + :id: I_ARMI_BLOCK_DIMS8 :implements: R_ARMI_BLOCK_DIMS """ return self.getComponent(Flags.COOLANT, exact=True).getArea() @@ -2355,7 +2362,7 @@ def getHydraulicDiameter(self): l = 6*p/sqrt(3) .. impl:: Hydraulic diameter of block is retrievable - :id: I_ARMI_BLOCK_DIMS + :id: I_ARMI_BLOCK_DIMS9 :implements: R_ARMI_BLOCK_DIMS """ return 4.0 * self.getFlowArea() / self.getWettedPerimeter() From 6a546dd1ca3a81128e5dcc469a5d49853db2047c Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 7 Nov 2023 09:08:35 -0800 Subject: [PATCH 038/176] Adding impl and test tags for requirements (#1462) --- armi/interfaces.py | 14 ++++++++++++- armi/materials/__init__.py | 11 ++++++++++ armi/materials/material.py | 20 ++++++++++++++++++- armi/materials/void.py | 7 +++++++ armi/operators/operator.py | 8 ++++++++ .../globalFlux/globalFluxInterface.py | 12 +++++++++++ .../tests/test_globalFluxInterface.py | 8 ++++++++ armi/plugins.py | 8 ++++++++ 8 files changed, 86 insertions(+), 2 deletions(-) diff --git a/armi/interfaces.py b/armi/interfaces.py index 8e96b3ffb..b0d2721dc 100644 --- a/armi/interfaces.py +++ b/armi/interfaces.py @@ -47,6 +47,10 @@ class STACK_ORDER: # noqa: invalid-class-name Each module specifies an ``ORDER`` constant that specifies where in this order it should be placed in the Interface Stack. + .. impl:: Define an ordered list of interfaces. + :id: I_ARMI_OPERATOR_INTERFACES0 + :implements: R_ARMI_OPERATOR_INTERFACES + Notes ----- Originally, the ordering was accomplished with a very large if/else construct in ``createInterfaces``. @@ -84,7 +88,11 @@ class STACK_ORDER: # noqa: invalid-class-name class TightCoupler: """ Data structure that defines tight coupling attributes that are implemented - within an Interface and called upon when ``interactCoupled`` is called. + within an Interface and called upon when ``interactAllCoupled`` is called. + + .. impl:: The TightCoupler defines the convergence criteria for physics coupling. + :id: I_ARMI_OPERATOR_PHYSICS0 + :implements: R_ARMI_OPERATOR_PHYSICS Parameters ---------- @@ -240,6 +248,10 @@ class Interface: Interface instances are gathered into an interface stack in :py:meth:`armi.operators.operator.Operator.createInterfaces`. + + .. impl:: The interface shall allow code execution at important operational points in time. + :id: I_ARMI_INTERFACE + :implements: R_ARMI_INTERFACE """ # list containing interfaceClass diff --git a/armi/materials/__init__.py b/armi/materials/__init__.py index 02eaa38cb..5fb42d25a 100644 --- a/armi/materials/__init__.py +++ b/armi/materials/__init__.py @@ -46,6 +46,13 @@ def setMaterialNamespaceOrder(order): + """ + Set the material namespace order at the Python interpretter, global level. + + .. impl:: Materials can be searched across packages in a defined namespace. + :id: I_ARMI_MAT_NAMESPACE + :implements: R_ARMI_MAT_NAMESPACE + """ global _MATERIAL_NAMESPACE_ORDER _MATERIAL_NAMESPACE_ORDER = order @@ -121,6 +128,10 @@ def resolveMaterialClassByName(name: str, namespaceOrder: List[str] = None): gets used (Framework's UO2 vs. a user plugins UO2 vs. the Kentucky Transportation Cabinet's UO2) is up to the user at runtime. + .. impl:: Material collections are defined with an order of precedence in the case of duplicates. + :id: I_ARMI_MAT_ORDER + :implements: R_ARMI_MAT_ORDER + Parameters ---------- name : str diff --git a/armi/materials/material.py b/armi/materials/material.py index c5592db4d..cdf4af64f 100644 --- a/armi/materials/material.py +++ b/armi/materials/material.py @@ -36,6 +36,14 @@ class Material: """ A material is made up of elements or isotopes. It has bulk properties like mass density. + .. impl:: The abstract material class. + :id: I_ARMI_MAT_PROPERTIES + :implements: R_ARMI_MAT_PROPERTIES + + .. impl:: Materials generate nuclide mass fractions at instantiation. + :id: I_ARMI_MAT_FRACS + :implements: R_ARMI_MAT_FRACS + Attributes ---------- parent : Component @@ -94,7 +102,13 @@ def __repr__(self): @property def name(self): - """Getter for the private name attribute of this Material.""" + """ + 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 + """ return self._name @name.setter @@ -707,6 +721,10 @@ def getThermalExpansionDensityReduction(self, prevTempInC, newTempInC): def linearExpansion(self, Tk=None, Tc=None): """For void, lets just not allow temperature changes to change dimensions since it is a liquid it will fill its space. + + .. impl:: Fluid materials are not thermally expandable. + :id: I_ARMI_MAT_FLUID + :implements: R_ARMI_MAT_FLUID """ return 0.0 diff --git a/armi/materials/void.py b/armi/materials/void.py index 34d3ffec9..eff62c309 100644 --- a/armi/materials/void.py +++ b/armi/materials/void.py @@ -21,6 +21,13 @@ 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 + """ + def pseudoDensity(self, Tk: float = None, Tc: float = None) -> float: return 0.0 diff --git a/armi/operators/operator.py b/armi/operators/operator.py index 48a83e19e..9984c7e06 100644 --- a/armi/operators/operator.py +++ b/armi/operators/operator.py @@ -73,6 +73,10 @@ class Operator: .. note:: The :doc:`/developer/guide` has some additional narrative on this topic. + .. impl:: The operator package shall expose an ordered list of interfaces, and loop over them in order. + :id: I_ARMI_OPERATOR_INTERFACES1 + :implements: R_ARMI_OPERATOR_INTERFACES + Attributes ---------- cs : CaseSettings object @@ -650,6 +654,10 @@ def interactAllCoupled(self, coupledIteration): This is distinct from loose coupling, which would simply uses the temperature values from the previous timestep in the current flux solution. It's also distinct from full coupling where all fields are solved simultaneously. ARMI supports tight and loose coupling. + + .. impl:: Physics coupling is driven from Operator. + :id: I_ARMI_OPERATOR_PHYSICS1 + :implements: R_ARMI_OPERATOR_PHYSICS """ activeInterfaces = [ii for ii in self.interfaces if ii.enabled()] diff --git a/armi/physics/neutronics/globalFlux/globalFluxInterface.py b/armi/physics/neutronics/globalFlux/globalFluxInterface.py index 5c5d20471..3b998b8f6 100644 --- a/armi/physics/neutronics/globalFlux/globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/globalFluxInterface.py @@ -334,6 +334,10 @@ def getLabel(caseTitle, cycle, node, iteration=None): class GlobalFluxOptions(executers.ExecutionOptions): """Data structure representing common options in Global Flux Solvers. + .. impl:: Options for neutronics solvers. + :id: I_ARMI_FLUX_OPTIONS + :implements: R_ARMI_FLUX_OPTIONS + Attributes ---------- adjoint : bool @@ -528,6 +532,10 @@ def _performGeometryTransformations(self, makePlots=False): In both cases, we need to undo the modifications between reading the output and applying the result to the data model. + .. impl:: Ensure the mesh in the reactor model is appropriate for neutronics solver execution. + :id: I_ARMI_FLUX_RX_RATES + :implements: R_ARMI_FLUX_RX_RATES + See Also -------- _undoGeometryTransformations @@ -1244,6 +1252,10 @@ def calcReactionRates(obj, keff, lib): r""" Compute 1-group reaction rates for this object (usually a block). + .. impl:: Return the reaction rates for a given ArmiObject. + :id: I_ARMI_FLUX_RX_RATES + :implements: R_ARMI_FLUX_RX_RATES + Parameters ---------- obj : Block diff --git a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py index ff8957660..5c5b4f131 100644 --- a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py @@ -104,6 +104,14 @@ def getKeff(self): class TestGlobalFluxOptions(unittest.TestCase): + """ + Tests for GlobalFluxOptions. + + .. test:: Tests GlobalFluxOptions + :id: T_ARMI_FLUX_OPTIONS + :tests: R_ARMI_FLUX_OPTIONS + """ + def test_readFromSettings(self): cs = settings.Settings() opts = globalFluxInterface.GlobalFluxOptions("neutronics-run") diff --git a/armi/plugins.py b/armi/plugins.py index d128bcddb..79cb8ba69 100644 --- a/armi/plugins.py +++ b/armi/plugins.py @@ -169,6 +169,10 @@ def defineParameters() -> Dict: """ Function for defining additional parameters. + .. impl:: Plugins can add parameters to the reactor data model. + :id: I_ARMI_PLUGIN_PARAMS + :implements: R_ARMI_PLUGIN_PARAMS + Returns ------- dict @@ -372,6 +376,10 @@ def defineSettings() -> List: neutronics kernel (let's say MCNP), it should also define a new option to tell the settings system that ``"MCNP"`` is a valid option. + .. impl:: Plugins can add settings to the run. + :id: I_ARMI_PLUGIN_SETTINGS + :implements: R_ARMI_PLUGIN_SETTINGS + Returns ------- list From d803a7156a380c800210e633dff71d82adbd18b7 Mon Sep 17 00:00:00 2001 From: Michael Jarrett Date: Wed, 8 Nov 2023 10:32:11 -0800 Subject: [PATCH 039/176] Use functools for codeTiming decorator. (#1466) --- armi/utils/codeTiming.py | 5 ++--- armi/utils/tests/test_utils.py | 14 ++++++++++++++ doc/release/0.2.rst | 1 + 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/armi/utils/codeTiming.py b/armi/utils/codeTiming.py index 543b00132..b205bbe11 100644 --- a/armi/utils/codeTiming.py +++ b/armi/utils/codeTiming.py @@ -16,6 +16,7 @@ import copy import os import time +import functools def timed(*args): @@ -36,9 +37,7 @@ def mymethod2(stuff) """ def time_decorator(func): - time_decorator.__doc__ = func.__doc__ - time_decorator.__name__ = func.__name__ - + @functools.wraps(func) def time_wrapper(*args, **kwargs): generated_name = "::".join( [ diff --git a/armi/utils/tests/test_utils.py b/armi/utils/tests/test_utils.py index d5aac0314..19a221ab5 100644 --- a/armi/utils/tests/test_utils.py +++ b/armi/utils/tests/test_utils.py @@ -36,6 +36,7 @@ getPreviousTimeNode, getCumulativeNodeNum, hasBurnup, + codeTiming, ) @@ -154,6 +155,19 @@ def test_classesInHierarchy(self): self.assertGreater(len(r.core.getAssemblies()), 50) self.assertGreater(len(r.core.getBlocks()), 200) + def test_codeTiming(self): + """ + Test that codeTiming preserves function attributes when it wraps a function + """ + + @codeTiming.timed + def testFunc(): + """Test function docstring""" + pass + + self.assertEqual(getattr(testFunc, "__doc__"), "Test function docstring") + self.assertEqual(getattr(testFunc, "__name__"), "testFunc") + class CyclesSettingsTests(unittest.TestCase): """ diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst index 883418687..631fd05a8 100644 --- a/doc/release/0.2.rst +++ b/doc/release/0.2.rst @@ -13,6 +13,7 @@ What's new in ARMI #. Removed all old ARMI requirements, to start the work fresh. (`PR#1438 `_) #. Downgrading Draft PRs as policy. (`PR#1444 `_) #. Attempt to set representative block number densities by component if possible. (`PR#1412 `_) +#. Use functools to preserve function attributes when wrapping with codeTiming.timed (`PR#1466 `_) #. TBD Bug fixes From 98ce9e504b219e263aff0eef6ea4393513e6bfca Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Wed, 8 Nov 2023 11:30:00 -0800 Subject: [PATCH 040/176] Adding req crumbs for runLog and settings (#1465) --- armi/__init__.py | 5 +- armi/apps.py | 8 +- armi/cases/tests/test_cases.py | 32 +++ armi/operators/settingsValidation.py | 8 +- armi/operators/tests/test_inspectors.py | 4 + armi/physics/fuelCycle/__init__.py | 1 + armi/physics/fuelPerformance/plugin.py | 1 + armi/physics/neutronics/__init__.py | 4 + .../fissionProductModelSettings.py | 1 + armi/physics/safety/__init__.py | 1 + armi/physics/thermalHydraulics/plugin.py | 1 + armi/plugins.py | 2 +- armi/reactor/tests/test_reactors.py | 4 + armi/runLog.py | 12 + armi/settings/caseSettings.py | 4 + armi/settings/fwSettings/databaseSettings.py | 1 + armi/settings/fwSettings/globalSettings.py | 8 +- armi/settings/fwSettings/reportSettings.py | 1 + armi/settings/setting.py | 3 + armi/settings/settingsIO.py | 4 + armi/settings/tests/test_settings.py | 11 +- armi/settings/tests/test_settingsIO.py | 6 + armi/tests/Godiva-blueprints.yaml | 220 ++++-------------- armi/tests/test_runLog.py | 32 ++- armi/tests/test_user_plugins.py | 8 + armi/utils/densityTools.py | 4 +- armi/utils/hexagon.py | 19 +- 27 files changed, 218 insertions(+), 187 deletions(-) diff --git a/armi/__init__.py b/armi/__init__.py index abb40fff0..4c36c7694 100644 --- a/armi/__init__.py +++ b/armi/__init__.py @@ -118,6 +118,10 @@ def init(choice=None, fName=None, cs=None): """ Scan a directory for armi inputs and load one to interact with. + .. impl:: Settings are used to define an ARMI run. + :id: I_ARMI_SETTING1 + :implements: R_ARMI_SETTING + Parameters ---------- choice : int, optional @@ -133,7 +137,6 @@ def init(choice=None, fName=None, cs=None): Examples -------- >>> o = armi.init() - """ from armi import cases from armi import settings diff --git a/armi/apps.py b/armi/apps.py index cdd83be0e..a855840a4 100644 --- a/armi/apps.py +++ b/armi/apps.py @@ -120,7 +120,13 @@ 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.""" + """ + 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 + """ # Start with framework settings settingDefs = { setting.name: setting for setting in fwSettings.getFrameworkSettings() diff --git a/armi/cases/tests/test_cases.py b/armi/cases/tests/test_cases.py index f030c5077..c6e29835c 100644 --- a/armi/cases/tests/test_cases.py +++ b/armi/cases/tests/test_cases.py @@ -447,10 +447,26 @@ def specifyInputs(cs): return {settingName: cs[settingName]} +class TestPluginWithDuplicateSetting(plugins.ArmiPlugin): + @staticmethod + @plugins.HOOKIMPL + def defineSettings(): + """Define a duplicate setting.""" + return [ + settings.setting.Setting( + "power", + default=123, + label="power", + description="duplicate power", + ) + ] + + class TestPluginForCopyInterfacesMultipleFiles(plugins.ArmiPlugin): @staticmethod @plugins.HOOKIMPL def defineSettings(): + """Define settings for the plugin.""" return [ settings.setting.Setting( "multipleFilesSetting", @@ -463,6 +479,7 @@ def defineSettings(): @staticmethod @plugins.HOOKIMPL def exposeInterfaces(cs): + """A plugin is mostly just a vehicle to add Interfaces to an Application.""" return [ interfaces.InterfaceInfo( interfaces.STACK_ORDER.PREPROCESSING, @@ -549,6 +566,21 @@ def test_copyInterfaceInputs_nonFilePath(self): self.assertFalse(os.path.exists(newSettings[testSetting])) 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 + """ + # register the new Plugin + app = getApp() + app.pluginManager.register(TestPluginWithDuplicateSetting) + + with self.assertRaises(ValueError): + cs = settings.Settings(ARMI_RUN_PATH) + def test_copyInterfaceInputs_multipleFiles(self): # register the new Plugin app = getApp() diff --git a/armi/operators/settingsValidation.py b/armi/operators/settingsValidation.py index e9b4ad37e..015621db8 100644 --- a/armi/operators/settingsValidation.py +++ b/armi/operators/settingsValidation.py @@ -42,7 +42,13 @@ class Query: - """An individual query.""" + """ + An individual setting validator. + + .. impl:: Rules to validate and customize a setting's behavior. + :id: I_ARMI_SETTINGS_RULES + :implements: R_ARMI_SETTINGS_RULES + """ def __init__(self, condition, statement, question, correction): """ diff --git a/armi/operators/tests/test_inspectors.py b/armi/operators/tests/test_inspectors.py index 9e6d175b4..3645bf0a1 100644 --- a/armi/operators/tests/test_inspectors.py +++ b/armi/operators/tests/test_inspectors.py @@ -66,6 +66,10 @@ def test_overwriteSettingsCorrectiveQuery(self): """ Tests the case where a corrective query is resolved. Checks to make sure the settings file is overwritten with the resolved setting. + + .. test:: Settings have validation and correction tools. + :id: T_ARMI_SETTINGS_RULES0 + :tests: R_ARMI_SETTINGS_RULES """ # load settings from test settings file self.cs["cycleLength"] = 300.0 diff --git a/armi/physics/fuelCycle/__init__.py b/armi/physics/fuelCycle/__init__.py index 12a55a6b5..95f4e96d8 100644 --- a/armi/physics/fuelCycle/__init__.py +++ b/armi/physics/fuelCycle/__init__.py @@ -71,6 +71,7 @@ def exposeInterfaces(cs): @staticmethod @plugins.HOOKIMPL def defineSettings(): + """Define settings for the plugin.""" return settings.getFuelCycleSettings() @staticmethod diff --git a/armi/physics/fuelPerformance/plugin.py b/armi/physics/fuelPerformance/plugin.py index 4753200e3..9e82f7f50 100644 --- a/armi/physics/fuelPerformance/plugin.py +++ b/armi/physics/fuelPerformance/plugin.py @@ -46,6 +46,7 @@ def defineSettingsValidators(inspector): @staticmethod @plugins.HOOKIMPL def defineParameters(): + """Define parameters for the plugin.""" from armi.physics.fuelPerformance import parameters return parameters.getFuelPerformanceParameterDefinitions() diff --git a/armi/physics/neutronics/__init__.py b/armi/physics/neutronics/__init__.py index bc5ac4e27..463166c1a 100644 --- a/armi/physics/neutronics/__init__.py +++ b/armi/physics/neutronics/__init__.py @@ -60,6 +60,7 @@ def exposeInterfaces(cs): @staticmethod @plugins.HOOKIMPL def defineParameters(): + """Define parameters for the plugin.""" from armi.physics.neutronics import parameters as neutronicsParameters return neutronicsParameters.getNeutronicsParameterDefinitions() @@ -67,6 +68,7 @@ def defineParameters(): @staticmethod @plugins.HOOKIMPL def defineEntryPoints(): + """Define entry points for the plugin.""" from armi.physics.neutronics import diffIsotxs entryPoints = [diffIsotxs.CompareIsotxsLibraries] @@ -76,6 +78,7 @@ def defineEntryPoints(): @staticmethod @plugins.HOOKIMPL def defineSettings(): + """Define settings for the plugin.""" from armi.physics.neutronics import settings as neutronicsSettings from armi.physics.neutronics import crossSectionSettings from armi.physics.neutronics.fissionProductModel import ( @@ -108,6 +111,7 @@ def defineSettingsValidators(inspector): @staticmethod @plugins.HOOKIMPL def onProcessCoreLoading(core, cs, dbLoad): + """Called whenever a Core object is newly built.""" applyEffectiveDelayedNeutronFractionToCore(core, cs) @staticmethod diff --git a/armi/physics/neutronics/fissionProductModel/fissionProductModelSettings.py b/armi/physics/neutronics/fissionProductModel/fissionProductModelSettings.py index 2cdf22c98..f42669afc 100644 --- a/armi/physics/neutronics/fissionProductModel/fissionProductModelSettings.py +++ b/armi/physics/neutronics/fissionProductModel/fissionProductModelSettings.py @@ -24,6 +24,7 @@ def defineSettings(): + """Define settings for the plugin.""" settings = [ setting.Setting( CONF_FP_MODEL, diff --git a/armi/physics/safety/__init__.py b/armi/physics/safety/__init__.py index 82f82e49f..df073d1d2 100644 --- a/armi/physics/safety/__init__.py +++ b/armi/physics/safety/__init__.py @@ -20,4 +20,5 @@ class SafetyPlugin(plugins.ArmiPlugin): @staticmethod @plugins.HOOKIMPL def defineSettings(): + """Define settings for the plugin.""" return [] diff --git a/armi/physics/thermalHydraulics/plugin.py b/armi/physics/thermalHydraulics/plugin.py index 43366977a..0d675a054 100644 --- a/armi/physics/thermalHydraulics/plugin.py +++ b/armi/physics/thermalHydraulics/plugin.py @@ -52,6 +52,7 @@ def defineSettingsValidators(inspector): @staticmethod @plugins.HOOKIMPL def defineParameters(): + """Define additional parameters for the reactor data model.""" from armi.physics.thermalHydraulics import parameters return parameters.getParameterDefinitions() diff --git a/armi/plugins.py b/armi/plugins.py index 79cb8ba69..46064089d 100644 --- a/armi/plugins.py +++ b/armi/plugins.py @@ -167,7 +167,7 @@ def exposeInterfaces(cs) -> List: @HOOKSPEC def defineParameters() -> Dict: """ - Function for defining additional parameters. + Define additional parameters for the reactor data model. .. impl:: Plugins can add parameters to the reactor data model. :id: I_ARMI_PLUGIN_PARAMS diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 32e85bad5..1370bf6bd 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -263,6 +263,10 @@ def test_getSetParameters(self): .. test:: Parameters accessible throughout the armi tree :id: T_ARMI_PARAM_PART :tests: R_ARMI_PARAM_PART + + .. impl:: Prove there is a setting for total core power. + :id: T_ARMI_SETTINGS_POWER + :implements: R_ARMI_SETTINGS_POWER """ # Test at core level core = self.r.core diff --git a/armi/runLog.py b/armi/runLog.py index faa4aa974..176d790f9 100644 --- a/armi/runLog.py +++ b/armi/runLog.py @@ -322,6 +322,10 @@ def concatenateLogs(logDir=None): Concatenate the armi run logs and delete them. Should only ever be called by parent. + + .. impl:: Log files from different processes are combined. + :id: I_ARMI_LOG_MPI + :implements: R_ARMI_LOG_MPI """ if logDir is None: logDir = LOG_DIR @@ -494,6 +498,14 @@ class RunLogger(logging.Logger): 1. Giving users the option to de-duplicate warnings 2. Piping stderr to a log file + + .. impl:: A simulation-wide log, with user-specified verbosity. + :id: I_ARMI_LOG + :implements: R_ARMI_LOG + + .. impl:: Logging is done to the screen and to file. + :id: I_ARMI_LOG_IO + :implements: R_ARMI_LOG_IO """ FMT = "%(levelname)s%(message)s" diff --git a/armi/settings/caseSettings.py b/armi/settings/caseSettings.py index 6713eca38..984055448 100644 --- a/armi/settings/caseSettings.py +++ b/armi/settings/caseSettings.py @@ -54,6 +54,10 @@ class Settings: The Settings object has a 1-to-1 correspondence with the ARMI settings input file. This file may be created by hand or by the GUI in submitter.py. + .. impl:: Settings are used to define an ARMI run. + :id: I_ARMI_SETTING0 + :implements: R_ARMI_SETTING + Notes ----- The actual settings in any instance of this class are immutable. diff --git a/armi/settings/fwSettings/databaseSettings.py b/armi/settings/fwSettings/databaseSettings.py index 79c24a129..7424c7fec 100644 --- a/armi/settings/fwSettings/databaseSettings.py +++ b/armi/settings/fwSettings/databaseSettings.py @@ -30,6 +30,7 @@ def defineSettings(): + """Define settings for the interface.""" settings = [ setting.Setting( CONF_DB, diff --git a/armi/settings/fwSettings/globalSettings.py b/armi/settings/fwSettings/globalSettings.py index 72410a04e..a8f07bdc9 100644 --- a/armi/settings/fwSettings/globalSettings.py +++ b/armi/settings/fwSettings/globalSettings.py @@ -119,7 +119,13 @@ def defineSettings() -> List[setting.Setting]: - """Return a list of global framework settings.""" + """ + Return a list of global framework settings. + + .. impl:: There is a setting for total core power. + :id: I_ARMI_SETTINGS_POWER + :implements: R_ARMI_SETTINGS_POWER + """ settings = [ setting.Setting( CONF_NUM_PROCESSORS, diff --git a/armi/settings/fwSettings/reportSettings.py b/armi/settings/fwSettings/reportSettings.py index c2ed49942..67c5a8322 100644 --- a/armi/settings/fwSettings/reportSettings.py +++ b/armi/settings/fwSettings/reportSettings.py @@ -26,6 +26,7 @@ def defineSettings(): + """Define settings for the interface.""" settings = [ setting.Setting( CONF_GEN_REPORTS, diff --git a/armi/settings/setting.py b/armi/settings/setting.py index 2b63fbc8a..2eb5a9a57 100644 --- a/armi/settings/setting.py +++ b/armi/settings/setting.py @@ -59,6 +59,9 @@ class Setting: the custom object and when you call ``dump``, it will be serialized. Just accessing the value will return the actual object in this case. + .. impl:: The setting default is mandatory. + :id: I_ARMI_SETTINGS_DEFAULTS + :implements: R_ARMI_SETTINGS_DEFAULTS """ def __init__( diff --git a/armi/settings/settingsIO.py b/armi/settings/settingsIO.py index 475a61352..1da66385b 100644 --- a/armi/settings/settingsIO.py +++ b/armi/settings/settingsIO.py @@ -141,6 +141,10 @@ def renameSetting(self, name) -> Tuple[str, bool]: class SettingsReader: """Abstract class for processing settings files. + .. impl:: The setting use a human-readable, plain text file as input. + :id: I_ARMI_SETTINGS_IO_TXT + :implements: R_ARMI_SETTINGS_IO_TXT + Parameters ---------- cs : CaseSettings diff --git a/armi/settings/tests/test_settings.py b/armi/settings/tests/test_settings.py index 343a3c3c0..460f290cd 100644 --- a/armi/settings/tests/test_settings.py +++ b/armi/settings/tests/test_settings.py @@ -45,6 +45,7 @@ class DummyPlugin1(plugins.ArmiPlugin): @staticmethod @plugins.HOOKIMPL def defineSettings(): + """Define settings for the plugin.""" return [ setting.Setting( "extendableOption", @@ -61,6 +62,7 @@ class DummyPlugin2(plugins.ArmiPlugin): @staticmethod @plugins.HOOKIMPL def defineSettings(): + """Define settings for the plugin.""" return [ setting.Option("PLUGIN", "extendableOption"), setting.Default("PLUGIN", "extendableOption"), @@ -71,6 +73,7 @@ class PluginAddsOptions(plugins.ArmiPlugin): @staticmethod @plugins.HOOKIMPL def defineSettings(): + """Define settings for the plugin.""" return [ setting.Option("MCNP", CONF_NEUTRONICS_KERNEL), setting.Option("MCNP_Slab", CONF_NEUTRONICS_KERNEL), @@ -261,7 +264,13 @@ def test_pluginSettings(self): self.assertEqual(cs["extendableOption"], "PLUGIN") def test_default(self): - """Make sure default updating mechanism works.""" + """ + Make sure default updating mechanism works. + + .. test:: The setting default is mandatory. + :id: T_ARMI_SETTINGS_DEFAULTS + :tests: R_ARMI_SETTINGS_DEFAULTS + """ a = setting.Setting("testsetting", 0) newDefault = setting.Default(5, "testsetting") a.changeDefault(newDefault) diff --git a/armi/settings/tests/test_settingsIO.py b/armi/settings/tests/test_settingsIO.py index d9054db31..816eaa88a 100644 --- a/armi/settings/tests/test_settingsIO.py +++ b/armi/settings/tests/test_settingsIO.py @@ -70,6 +70,12 @@ def test_basicSettingsReader(self): self.assertEqual(getattr(reader, "path"), "") def test_readFromFile(self): + """Read settings from a (human-readable) YAML file. + + .. test:: The setting file is in a human-readable plain text file. + :id: T_ARMI_SETTINGS_IO_TXT + :tests: R_ARMI_SETTINGS_IO_TXT + """ with directoryChangers.TemporaryDirectoryChanger(): inPath = os.path.join(TEST_ROOT, "armiRun.yaml") outPath = "test_readFromFile.yaml" diff --git a/armi/tests/Godiva-blueprints.yaml b/armi/tests/Godiva-blueprints.yaml index b24c97d1b..519725dd4 100644 --- a/armi/tests/Godiva-blueprints.yaml +++ b/armi/tests/Godiva-blueprints.yaml @@ -1,180 +1,48 @@ nuclide flags: - PU237: - burn: false - xs: true - expandTo: [] - PU240: - burn: false - xs: true - expandTo: [] - PU241: - burn: false - xs: true - expandTo: [] - AR: - burn: false - xs: true - expandTo: [] - PA233: - burn: false - xs: true - expandTo: [] - NP238: - burn: false - xs: true - expandTo: [] - AR36: - burn: false - xs: true - expandTo: [] - TH230: - burn: false - xs: true - expandTo: [] - AR38: - burn: false - xs: true - expandTo: [] - U238: - burn: false - xs: true - expandTo: [] - U239: - burn: false - xs: true - expandTo: [] - C: - burn: false - xs: true - expandTo: [] - LFP35: - burn: false - xs: true - expandTo: [] - U233: - burn: false - xs: true - expandTo: [] - U234: - burn: false - xs: true - expandTo: [] - U235: - burn: false - xs: true - expandTo: [] - U236: - burn: false - xs: true - expandTo: [] - U237: - burn: false - xs: true - expandTo: [] - PU239: - burn: false - xs: true - expandTo: [] - PU238: - burn: false - xs: true - expandTo: [] - TH234: - burn: false - xs: true - expandTo: [] - TH232: - burn: false - xs: true - expandTo: [] - AR40: - burn: false - xs: true - expandTo: [] - LFP39: - burn: false - xs: true - expandTo: [] - DUMP2: - burn: false - xs: true - expandTo: [] - LFP41: - burn: false - xs: true - expandTo: [] - LFP40: - burn: false - xs: true - expandTo: [] - PU242: - burn: false - xs: true - expandTo: [] - PU236: - burn: false - xs: true - expandTo: [] - U232: - burn: false - xs: true - expandTo: [] - DUMP1: - burn: false - xs: true - expandTo: [] - LFP38: - burn: false - xs: true - expandTo: [] - AM243: - burn: false - xs: true - expandTo: [] - PA231: - burn: false - xs: true - expandTo: [] - CM244: - burn: false - xs: true - expandTo: [] - CM242: - burn: false - xs: true - expandTo: [] - AM242: - burn: false - xs: true - expandTo: [] - CM245: - burn: false - xs: true - expandTo: [] - CM243: - burn: false - xs: true - expandTo: [] - CM246: - burn: false - xs: true - expandTo: [] - CM247: - burn: false - xs: true - expandTo: [] - O: - burn: false - xs: true - expandTo: [O16] - N: - burn: false - xs: true - expandTo: [N14] - ZR: - burn: false - xs: true - expandTo: [] + PU237: {burn: false, xs: true, expandTo: []} + PU240: {burn: false, xs: true, expandTo: []} + PU241: {burn: false, xs: true, expandTo: []} + AR: {burn: false, xs: true, expandTo: []} + PA233: {burn: false, xs: true, expandTo: []} + NP238: {burn: false, xs: true, expandTo: []} + AR36: {burn: false, xs: true, expandTo: []} + TH230: {burn: false, xs: true, expandTo: []} + AR38: {burn: false, xs: true, expandTo: []} + U238: {burn: false, xs: true, expandTo: []} + U239: {burn: false, xs: true, expandTo: []} + C: {burn: false, xs: true, expandTo: []} + LFP35: {burn: false, xs: true, expandTo: []} + U233: {burn: false, xs: true, expandTo: []} + U234: {burn: false, xs: true, expandTo: []} + U235: {burn: false, xs: true, expandTo: []} + U236: {burn: false, xs: true, expandTo: []} + U237: {burn: false, xs: true, expandTo: []} + PU239: {burn: false, xs: true, expandTo: []} + PU238: {burn: false, xs: true, expandTo: []} + TH234: {burn: false, xs: true, expandTo: []} + TH232: {burn: false, xs: true, expandTo: []} + AR40: {burn: false, xs: true, expandTo: []} + LFP39: {burn: false, xs: true, expandTo: []} + DUMP2: {burn: false, xs: true, expandTo: []} + LFP41: {burn: false, xs: true, expandTo: []} + LFP40: {burn: false, xs: true, expandTo: []} + PU242: {burn: false, xs: true, expandTo: []} + PU236: {burn: false, xs: true, expandTo: []} + U232: {burn: false, xs: true, expandTo: []} + DUMP1: {burn: false, xs: true, expandTo: []} + LFP38: {burn: false, xs: true, expandTo: []} + AM243: {burn: false, xs: true, expandTo: []} + PA231: {burn: false, xs: true, expandTo: []} + CM244: {burn: false, xs: true, expandTo: []} + CM242: {burn: false, xs: true, expandTo: []} + AM242: {burn: false, xs: true, expandTo: []} + CM245: {burn: false, xs: true, expandTo: []} + CM243: {burn: false, xs: true, expandTo: []} + CM246: {burn: false, xs: true, expandTo: []} + CM247: {burn: false, xs: true, expandTo: []} + O: {burn: false, xs: true, expandTo: [O16]} + N: {burn: false, xs: true, expandTo: [N14]} + ZR: {burn: false, xs: true, expandTo: []} custom isotopics: {} blocks: {} assemblies: diff --git a/armi/tests/test_runLog.py b/armi/tests/test_runLog.py index 15e5c1011..822ffd015 100644 --- a/armi/tests/test_runLog.py +++ b/armi/tests/test_runLog.py @@ -34,7 +34,13 @@ def test_setVerbosityFromInteger(self): self.assertEqual(verbosityRank, logging.DEBUG) def test_setVerbosityFromString(self): - """Test that the log verbosity can be set with a string.""" + """ + Test that the log verbosity can be set with a string. + + .. test:: The run log has configurable verbosity. + :id: T_ARMI_LOG0 + :tests: R_ARMI_LOG + """ log = runLog._RunLog(1) expectedStrVerbosity = "error" verbosityRank = log.getLogVerbosityRank(expectedStrVerbosity) @@ -79,6 +85,7 @@ def test_parentRunLogging(self): log.log("debug", "You shouldn't see this.", single=False, label=None) log.log("warning", "Hello, ", single=False, label=None) log.log("error", "world!", single=False, label=None) + log.logger.flush() log.logger.close() runLog.close(99) @@ -206,7 +213,16 @@ def validate_loggers(log): runLog.close(0) def test_setVerbosity(self): - """Let's test the setVerbosity() method carefully.""" + """Let's test the setVerbosity() method carefully. + + .. test:: The run log has configurable verbosity. + :id: T_ARMI_LOG1 + :tests: R_ARMI_LOG + + .. test:: The run log can log to stream. + :id: T_ARMI_LOG_IO0 + :tests: R_ARMI_LOG_IO + """ with mockRunLogs.BufferLog() as mock: # we should start with a clean slate self.assertEqual("", mock.getStdout()) @@ -307,7 +323,17 @@ def test_callingStartLogMultipleTimes(self): mock.emptyStdout() def test_concatenateLogs(self): - """Simple test of the concat logs function.""" + """ + Simple test of the concat logs function. + + .. test:: The run log combines logs from different processes. + :id: T_ARMI_LOG_MPI + :tests: R_ARMI_LOG_MPI + + .. test:: The run log can log to file. + :id: T_ARMI_LOG_IO1 + :tests: R_ARMI_LOG_IO + """ with TemporaryDirectoryChanger(): # create the log dir logDir = "test_concatenateLogs" diff --git a/armi/tests/test_user_plugins.py b/armi/tests/test_user_plugins.py index 8e12b2b2d..526797e5b 100644 --- a/armi/tests/test_user_plugins.py +++ b/armi/tests/test_user_plugins.py @@ -34,6 +34,7 @@ class UserPluginFlags(plugins.UserPlugin): @staticmethod @plugins.HOOKIMPL def defineFlags(): + """Function to provide new Flags definitions.""" return {"SPECIAL": utils.flags.auto()} @@ -43,6 +44,7 @@ class UserPluginFlags2(plugins.UserPlugin): @staticmethod @plugins.HOOKIMPL def defineFlags(): + """Function to provide new Flags definitions.""" return {"FLAG2": utils.flags.auto()} @@ -52,6 +54,7 @@ class UserPluginFlags3(plugins.UserPlugin): @staticmethod @plugins.HOOKIMPL def defineFlags(): + """Function to provide new Flags definitions.""" return {"FLAG3": utils.flags.auto()} @@ -74,6 +77,7 @@ class UserPluginBadDefinesSettings(plugins.UserPlugin): @staticmethod @plugins.HOOKIMPL def defineSettings(): + """Define settings for the plugin.""" return [1, 2, 3] @@ -83,6 +87,7 @@ class UserPluginBadDefineParameterRenames(plugins.UserPlugin): @staticmethod @plugins.HOOKIMPL def defineParameterRenames(): + """Return a mapping from old parameter names to new parameter names.""" return {"oldType": "type"} @@ -96,6 +101,7 @@ class UserPluginOnProcessCoreLoading(plugins.UserPlugin): @staticmethod @plugins.HOOKIMPL def onProcessCoreLoading(core, cs, dbLoad): + """Function to call whenever a Core object is newly built.""" blocks = core.getBlocks(Flags.FUEL) for b in blocks: b.p.height += 1.0 @@ -110,6 +116,7 @@ class UpInterface(interfaces.Interface): name = "UpInterface" def interactEveryNode(self, cycle, node): + """Logic to be carried out at every time node in the simulation.""" self.r.core.p.power += 100 @@ -119,6 +126,7 @@ class UserPluginWithInterface(plugins.UserPlugin): @staticmethod @plugins.HOOKIMPL def exposeInterfaces(cs): + """Function for exposing interface(s) to other code.""" return [ interfaces.InterfaceInfo( interfaces.STACK_ORDER.PREPROCESSING, UpInterface, {"enabled": True} diff --git a/armi/utils/densityTools.py b/armi/utils/densityTools.py index 2d6dc13ac..eb29e57df 100644 --- a/armi/utils/densityTools.py +++ b/armi/utils/densityTools.py @@ -196,6 +196,7 @@ def formatMaterialCard( mCard = ["m{matNum}\n".format(matNum=matNum)] else: mCard = ["m{}\n"] + for nuc, dens in sorted(densities.items()): # skip LFPs and Dummies. if isinstance(nuc, (nuclideBases.LumpNuclideBase)): @@ -214,6 +215,7 @@ def formatMaterialCard( if mcnp6Compatible: mCard.append(" nlib={lib}c\n".format(lib=mcnpLibrary)) + return mCard @@ -250,7 +252,7 @@ def filterNuclideList(nuclideVector, nuclides): def normalizeNuclideList(nuclideVector, normalization=1.0): """ - normalize the nuclide vector. + Normalize the nuclide vector. Parameters ---------- diff --git a/armi/utils/hexagon.py b/armi/utils/hexagon.py index 643b8d670..2f0360bb3 100644 --- a/armi/utils/hexagon.py +++ b/armi/utils/hexagon.py @@ -29,7 +29,13 @@ def area(pitch): - """Area of a hex given the flat-to-flat pitch.""" + """ + Area of a hex given the flat-to-flat pitch. + + Notes + ----- + The pitch is the distance between the center of the hexagons in the lattice. + """ return SQRT3 / 2.0 * pitch**2 @@ -44,6 +50,10 @@ def side(pitch): \frac{s}{2}^2 + \frac{p}{2}^2 = s^2 which you can solve to find p = sqrt(3)*s + + Notes + ----- + The pitch is the distance between the center of the hexagons in the lattice. """ return pitch / SQRT3 @@ -78,6 +88,13 @@ def corners(rotation=0): def pitch(side): + """ + Calculate the pitch from the length of a hexagon side. + + Notes + ----- + The pitch is the distance between the center of the hexagons in the lattice. + """ return side * SQRT3 From 363819ef5b5dd6aac15fd949015823593c9b1d7b Mon Sep 17 00:00:00 2001 From: bdlafleur Date: Wed, 8 Nov 2023 16:34:32 -0500 Subject: [PATCH 041/176] Adding test and impl tags to assemblies (#1463) --- armi/reactor/assemblies.py | 40 ++++++++++++++-- armi/reactor/tests/test_assemblies.py | 67 ++++++++++++++++++++++++++- armi/reactor/tests/test_blocks.py | 16 +++---- 3 files changed, 109 insertions(+), 14 deletions(-) diff --git a/armi/reactor/assemblies.py b/armi/reactor/assemblies.py index 529a645ad..ac0152b1b 100644 --- a/armi/reactor/assemblies.py +++ b/armi/reactor/assemblies.py @@ -170,12 +170,17 @@ def makeUnique(self): self.p.assemNum = randint(-9e12, -1) self.renumber(self.p.assemNum) - def add(self, obj): + def add(self, obj: blocks.Block): """ Add an object to this assembly. The simple act of adding a block to an assembly fully defines the location of the block in 3-D. + + .. impl:: Assemblies are made up of type Block + :id: I_ARMI_ASSEM_BLOCKS + :implements: R_ARMI_ASSEM_BLOCKS + """ composites.Composite.add(self, obj) obj.spatialLocator = self.spatialGrid[0, 0, len(self) - 1] @@ -218,6 +223,11 @@ def getLocation(self): grid/spatialLocator system and the ability to represent things like the SFP as siblings of a Core. In future, this will likely be re-implemented in terms of just spatialLocator objects. + + .. impl:: Assembly location is retrievable + :id: I_ARMI_ASSEM_POSI0 + :implements: R_ARMI_ASSEM_POSI + """ # just use ring and position, not axial (which is 0) if not self.parent: @@ -229,7 +239,13 @@ def getLocation(self): ) def coords(self): - """Return the location of the assembly in the plane using cartesian global coordinates.""" + """Return the location of the assembly in the plane using cartesian global + coordinates. + + .. impl:: Assembly coordinates are retrievable + :id: I_ARMI_ASSEM_POSI1 + :implements: R_ARMI_ASSEM_POSI + """ x, y, _z = self.spatialLocator.getGlobalCoordinates() return (x, y) @@ -238,6 +254,11 @@ def getArea(self): Return the area of the assembly by looking at its first block. The assumption is that all blocks in an assembly have the same area. + Calculate the total assembly volume in cm^3. + + .. impl:: Assembly area is retrievable + :id: I_ARMI_ASSEM_DIMS0 + :implements: R_ARMI_ASSEM_DIMS """ try: return self[0].getArea() @@ -248,7 +269,12 @@ def getArea(self): return 1.0 def getVolume(self): - """Calculate the total assembly volume in cm^3.""" + """Calculate the total assembly volume in cm^3. + + .. impl:: Assembly volume is retrievable + :id: I_ARMI_ASSEM_DIMS1 + :implements: R_ARMI_ASSEM_DIMS + """ return self.getArea() * self.getTotalHeight() def getPinPlenumVolumeInCubicMeters(self): @@ -449,6 +475,10 @@ def getTotalHeight(self, typeSpec=None): """ Determine the height of this assembly in cm. + .. impl:: Assembly height is retrievable + :id: I_ARMI_ASSEM_DIMS2 + :implements: R_ARMI_ASSEM_DIMS + Parameters ---------- typeSpec : See :py:meth:`armi.composites.Composite.hasFlags` @@ -1161,6 +1191,10 @@ def getDim(self, typeSpec, dimName): Then, look on that component for dimName. Example: getDim(Flags.WIRE, 'od') will return a wire's OD in cm. + + .. impl:: Assembly dimensions are retrievable + :id: I_ARMI_ASSEM_DIMS3 + :implements: R_ARMI_ASSEM_DIMS """ # prefer fuel blocks. bList = self.getBlocks(Flags.FUEL) diff --git a/armi/reactor/tests/test_assemblies.py b/armi/reactor/tests/test_assemblies.py index 47ee795d5..193af73c1 100644 --- a/armi/reactor/tests/test_assemblies.py +++ b/armi/reactor/tests/test_assemblies.py @@ -23,12 +23,12 @@ from armi import tests from armi.reactor import assemblies from armi.reactor import blueprints +from armi.reactor import blocks from armi.reactor import components from armi.reactor import parameters from armi.reactor import reactors from armi.reactor import geometry from armi.reactor.assemblies import ( - blocks, copy, Flags, grids, @@ -344,17 +344,36 @@ def test_getNum(self): self.assertEqual(cur, ref) def test_getLocation(self): + """ + Test for getting string location of assembly. + + .. test:: Assembly location is retrievable + :id: T_ARMI_ASSEM_POSI0 + :tests: R_ARMI_ASSEM_POSI + """ cur = self.assembly.getLocation() ref = str("005-003") self.assertEqual(cur, ref) def test_getArea(self): + """Tests area calculation for hex assembly. + + .. test:: Assembly area is retrievable + :id: T_ARMI_ASSEM_DIMS0 + :tests: R_ARMI_ASSEM_DIMS + """ cur = self.assembly.getArea() ref = math.sqrt(3) / 2.0 * self.hexDims["op"] ** 2 places = 6 self.assertAlmostEqual(cur, ref, places=places) def test_getVolume(self): + """Tests volume calculation for hex assembly. + + .. test:: Assembly volume is retrievable + :id: T_ARMI_ASSEM_DIMS1 + :tests: R_ARMI_ASSEM_DIMS + """ cur = self.assembly.getVolume() ref = math.sqrt(3) / 2.0 * self.hexDims["op"] ** 2 * self.height * NUM_BLOCKS places = 6 @@ -432,6 +451,14 @@ def test_getTotalHeight(self): self.assertAlmostEqual(cur, ref, places=places) def test_getHeight(self): + """ + Test height of assembly calculation. + + .. test:: Assembly height is retrievable + :id: T_ARMI_ASSEM_DIMS2 + :tests: R_ARMI_ASSEM_DIMS + + """ cur = self.assembly.getHeight() ref = self.height * NUM_BLOCKS places = 6 @@ -836,6 +863,12 @@ def test_countBlocksOfType(self): self.assertEqual(cur, 3) def test_getDim(self): + """Tests dimensions are retrievable. + + .. test:: Assembly dimensions are retrievable + :id: T_ARMI_ASSEM_DIMS3 + :tests: R_ARMI_ASSEM_DIMS + """ cur = self.assembly.getDim(Flags.FUEL, "op") ref = self.hexDims["op"] places = 6 @@ -969,7 +1002,12 @@ def test_hasContinuousCoolantChannel(self): self.assertTrue(modifiedAssem.hasContinuousCoolantChannel()) def test_carestianCoordinates(self): - """Check the coordinates of the assembly within the core with a CarestianGrid.""" + """Check the coordinates of the assembly within the core with a CarestianGrid. + + .. test:: Cartesian coordinates are retrievable + :id: T_ARMI_ASSEM_POSI1 + :test: R_ARMI_ASSEM_POSI + """ a = makeTestAssembly( numBlocks=1, assemNum=1, @@ -1062,6 +1100,31 @@ def test_rotate(self): a.rotate(math.radians(120)) self.assertIn("No rotation method defined", mock.getStdout()) + def test_assem_block_types(self): + """Test that all children of an assembly are blocks. + + .. test:: Validate child types of assembly are blocks + :id: T_ARMI_ASSEM_BLOCKS + :tests: R_ARMI_ASSEM_BLOCKS + """ + for b in self.assembly.getBlocks(): + + # Confirm children are blocks + self.assertIsInstance(b, blocks.Block) + + def test_assem_hex_type(self): + """Test that all children of a hex assembly are hexagons. + + .. test:: Validate child types of assembly are Hex type + :id: T_ARMI_ASSEM_HEX + :tests: R_ARMI_ASSEM_HEX + """ + for b in self.assembly.getBlocks(): + + # For a hex assem, confirm they are of type "Hexagon" + pitch_comp_type = b.PITCH_COMPONENT_TYPE[0] + self.assertEqual(pitch_comp_type.__name__, "Hexagon") + class AssemblyInReactor_TestCase(unittest.TestCase): def setUp(self): diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py index c2f992c1b..57afcdf48 100644 --- a/armi/reactor/tests/test_blocks.py +++ b/armi/reactor/tests/test_blocks.py @@ -851,10 +851,10 @@ def test_adjustUEnrich(self): def test_setLocation(self): """ - Retrieve a blocks location + Retrieve a blocks location. .. test:: Location of a block is retrievable - :id: T_ARMI_BLOCK_POSI + :id: T_ARMI_BLOCK_POSI0 :tests: R_ARMI_BLOCK_POSI """ b = self.block @@ -1549,9 +1549,7 @@ def test_setPitch(self): self.assertAlmostEqual(moles2, moles3) def test_setImportantParams(self): - """ - Confirm that important block parameters can be set and get. - """ + """Confirm that important block parameters can be set and get.""" # Test ability to set and get flux applyDummyData(self.block) self.assertEqual(self.block.p.mgFlux[0], 161720716762.12997) @@ -1830,7 +1828,7 @@ def test_component_type(self): """ Test that a hex block has the proper "hexagon" __name__. - .. test: Ability to create hex shaped blocks + .. test:: Ability to create hex shaped blocks :id: T_ARMI_BLOCK_HEX :tests: R_ARMI_BLOCK_HEX """ @@ -1841,8 +1839,8 @@ def test_coords(self): """ Test that coordinates are retrievable from a block. - .. test: Coordinates of a block are queryable - :id: T_ARMI_BLOCK_POSI + .. test:: Coordinates of a block are queryable + :id: T_ARMI_BLOCK_POSI1 :tests: R_ARMI_BLOCK_POSI """ r = self.HexBlock.r @@ -1873,7 +1871,7 @@ def test_block_dims(self): Tests that the block class can provide basic dimensionality information about itself. - .. test: Retrieve important block dimensions + .. test:: Retrieve important block dimensions :id: T_ARMI_BLOCK_DIMS :tests: R_ARMI_BLOCK_DIMS """ From 78477048c1f9d331bc531351bd450fe82d786e5e Mon Sep 17 00:00:00 2001 From: bdlafleur Date: Wed, 8 Nov 2023 18:00:57 -0500 Subject: [PATCH 042/176] Adding impl/test tags for Zones (#1467) --- armi/reactor/tests/test_zones.py | 35 ++++++++++++++++++++++++++++++++ armi/reactor/zones.py | 11 +++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/armi/reactor/tests/test_zones.py b/armi/reactor/tests/test_zones.py index 9d703363d..085a7dc7c 100644 --- a/armi/reactor/tests/test_zones.py +++ b/armi/reactor/tests/test_zones.py @@ -72,6 +72,13 @@ def setUp(self): self.bList.append(b) def test_addItem(self): + """ + Test adding an item. + + .. test:: Add item to a zone + :id: T_ARMI_ZONE0 + :tests: R_ARMI_ZONE + """ zone = zones.Zone("test_addItem") zone.addItem(self.aList[0]) self.assertIn(self.aList[0].getLocation(), zone) @@ -86,6 +93,13 @@ def test_removeItem(self): self.assertRaises(AssertionError, zone.removeItem, "also nope") def test_addItems(self): + """ + Test adding items. + + .. test:: Add multiple items to a zone + :id: T_ARMI_ZONE1 + :tests: R_ARMI_ZONE + """ zone = zones.Zone("test_addItems") zone.addItems(self.aList) for a in self.aList: @@ -98,6 +112,13 @@ def test_removeItems(self): self.assertNotIn(a.getLocation(), zone) def test_addLoc(self): + """ + Test adding a location. + + .. test:: Add location to a zone + :id: T_ARMI_ZONE2 + :tests: R_ARMI_ZONE + """ zone = zones.Zone("test_addLoc") zone.addLoc(self.aList[0].getLocation()) self.assertIn(self.aList[0].getLocation(), zone) @@ -112,6 +133,13 @@ def test_removeLoc(self): self.assertRaises(AssertionError, zone.removeLoc, 1234) def test_addLocs(self): + """ + Test adding locations. + + .. test:: Add multiple locations to a zone + :id: T_ARMI_ZONE3 + :tests: R_ARMI_ZONE + """ zone = zones.Zone("test_addLocs") zone.addLocs([a.getLocation() for a in self.aList]) for a in self.aList: @@ -179,6 +207,13 @@ def setUp(self): self.zonez = self.r.core.zones def test_dictionaryInterface(self): + """ + Test creating and interacting with the Zones object. + + .. test:: Create collection of Zones + :id: T_ARMI_ZONES + :tests: R_ARMI_ZONES + """ zs = zones.Zones() # validate the addZone() and __len__() work diff --git a/armi/reactor/zones.py b/armi/reactor/zones.py index 718ea89ca..c5b16fd50 100644 --- a/armi/reactor/zones.py +++ b/armi/reactor/zones.py @@ -28,6 +28,10 @@ class Zone: """ A group of locations in the Core, used to divide it up for analysis. Each location represents an Assembly or a Block. + + .. impl:: A collection of armi locations + :id: I_ARMI_ZONE + :implements: R_ARMI_ZONE """ VALID_TYPES = (Assembly, Block) @@ -196,7 +200,12 @@ def removeItems(self, items: List) -> None: class Zones: - """Collection of Zone objects.""" + """Collection of Zone objects. + + .. impl:: A collection of armi zones + :id: I_ARMI_ZONES + :implements: R_ARMI_ZONES + """ def __init__(self): """Build a Zones object.""" From 63c9264b0ae5be9251eee301e6e58e8b93b22865 Mon Sep 17 00:00:00 2001 From: bdlafleur Date: Mon, 13 Nov 2023 13:21:31 -0500 Subject: [PATCH 043/176] Adding test/impl tags for reactors (#1468) --- armi/cases/tests/test_cases.py | 2 +- armi/reactor/reactors.py | 25 +++++++++++++-- armi/reactor/tests/test_reactors.py | 48 +++++++++++++++++++++++++++-- armi/utils/tests/test_utils.py | 8 ++--- 4 files changed, 72 insertions(+), 11 deletions(-) diff --git a/armi/cases/tests/test_cases.py b/armi/cases/tests/test_cases.py index c6e29835c..203906fc0 100644 --- a/armi/cases/tests/test_cases.py +++ b/armi/cases/tests/test_cases.py @@ -579,7 +579,7 @@ def test_failOnDuplicateSetting(self): app.pluginManager.register(TestPluginWithDuplicateSetting) with self.assertRaises(ValueError): - cs = settings.Settings(ARMI_RUN_PATH) + _ = settings.Settings(ARMI_RUN_PATH) def test_copyInterfaceInputs_multipleFiles(self): # register the new Plugin diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index 610d7052e..084d0a2e9 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -164,6 +164,10 @@ def loadFromCs(cs) -> Reactor: """ Load a Reactor based on the input settings. + .. impl:: Create reactor from input yaml file + :id: I_ARMI_R_CORE + :implements: R_ARMI_R_CORE + Parameters ---------- cs: CaseSettings @@ -754,6 +758,10 @@ def getNumRings(self, indexBased=False): """ Returns the number of rings in this reactor. Based on location so indexing will start at 1. + .. impl:: Retrieve number of rings in core + :id: I_ARMI_R_NUM_RINGS + :implements: R_ARMI_R_NUM_RINGS + Warning ------- If you loop through range(maxRing) then ring+1 is the one you want! @@ -1110,6 +1118,10 @@ def getAssemblyByName(self, name): """ Find the assembly that has this name. + .. impl:: Get assembly by name + :id: I_ARMI_R_GET_ASSEM_NAME + :implements: R_ARMI_R_GET_ASSEM_NAME + Parameters ---------- name : str @@ -1621,7 +1633,12 @@ def getAssemblyWithAssemNum(self, assemNum): return self.getAssembly(assemNum=assemNum) def getAssemblyWithStringLocation(self, locationString): - """Returns an assembly or none if given a location string like 'B0014'.""" + """Returns an assembly or none if given a location string like 'B0014'. + + .. impl:: Get assembly by location + :id: I_ARMI_R_GET_ASSEM_LOC + :implements: R_ARMI_R_GET_ASSEM_LOC + """ ring, pos, _ = grids.locatorLabelToIndices(locationString) loc = self.spatialGrid.getLocatorFromRingAndPos(ring, pos) assem = self.childrenByLocator.get(loc) @@ -1647,11 +1664,15 @@ def findNeighbors( self, a, showBlanks=True, duplicateAssembliesOnReflectiveBoundary=False ): """ - Find assemblies that are next this assembly. + Find assemblies that are next to this assembly. Return a list of neighboring assemblies from the 30 degree point (point 1) then counterclockwise around. + .. impl:: Retrieve neighboring assemblies of a given assembly + :id: I_ARMI_R_FIND_NEIGHBORS + :implements: R_ARMI_R_FIND_NEIGHBORS + Parameters ---------- a : Assembly object diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 1370bf6bd..51ad62380 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -32,6 +32,7 @@ from armi.reactor import geometry from armi.reactor import grids from armi.reactor import reactors +from armi.reactor.composites import Composite from armi.reactor.components import Hexagon, Rectangle from armi.reactor.converters import geometryConverters from armi.reactor.converters.axialExpansionChanger import AxialExpansionChanger @@ -237,6 +238,13 @@ def setUp(self): ) def test_factorySortSetting(self): + """ + Create a core object from an input yaml. + + .. test:: Create core object from input yaml + :id: T_ARMI_R_CORE + :tests: R_ARMI_R_CORE + """ # get a sorted Reactor (the default) cs = settings.Settings(fName="armiRun.yaml") r0 = reactors.loadFromCs(cs) @@ -255,6 +263,9 @@ def test_factorySortSetting(self): a1 = [a.name for a in r1.core] self.assertNotEqual(a0, a1) + # The reactor object is a Composite + self.assertTrue(isinstance(r0.core, Composite)) + def test_getSetParameters(self): """ This test works through multiple levels of the hierarchy to test ability to @@ -546,6 +557,13 @@ def test_findAllRadMeshPoints(self): assert_allclose(expectedPoints, radPoints) def test_findNeighbors(self): + """ + Find neighbors of a given assembly. + + .. test:: Retrieve neighboring assemblies of a given assembly + :id: T_ARMI_R_FIND_NEIGHBORS + :tests: R_ARMI_R_FIND_NEIGHBORS + """ loc = self.r.core.spatialGrid.getLocatorFromRingAndPos(1, 1) a = self.r.core.childrenByLocator[loc] neighbs = self.r.core.findNeighbors( @@ -675,12 +693,30 @@ def test_getMinimumPercentFluxInFuel(self): with self.assertRaises(ZeroDivisionError): _targetRing, _fluxFraction = self.r.core.getMinimumPercentFluxInFuel() - def test_getAssembly(self): + def test_getAssemblyWithLoc(self): + """ + Get assembly by location. + + .. test:: Get assembly by location + :id: T_ARMI_R_GET_ASSEM_LOC + :tests: R_ARMI_R_GET_ASSEM_LOC + """ a1 = self.r.core.getAssemblyWithAssemNum(assemNum=10) a2 = self.r.core.getAssembly(locationString="003-001") - a3 = self.r.core.getAssembly(assemblyName="A0010") - self.assertEqual(a1, a3) + self.assertEqual(a1, a2) + + def test_getAssemblyWithName(self): + """ + Get assembly by name. + + .. test:: Get assembly by name + :id: T_ARMI_R_GET_ASSEM_NAME + :tests: R_ARMI_R_GET_ASSEM_NAME + """ + a1 = self.r.core.getAssemblyWithAssemNum(assemNum=10) + a2 = self.r.core.getAssembly(assemblyName="A0010") + self.assertEqual(a1, a2) def test_restoreReactor(self): @@ -889,6 +925,12 @@ def test_removeAssembliesInRing(self): self.assertEqual(a.spatialLocator.grid, self.r.sfp.spatialGrid) def test_removeAssembliesInRingByCount(self): + """Tests retrieving ring numbers and removing a ring. + + .. test:: Retrieve number of rings in core. + :id: T_ARMI_R_NUM_RINGS + :tests: R_ARMI_R_NUM_RINGS + """ self.assertEqual(self.r.core.getNumRings(), 9) self.r.core.removeAssembliesInRing(9, self.o.cs) self.assertEqual(self.r.core.getNumRings(), 8) diff --git a/armi/utils/tests/test_utils.py b/armi/utils/tests/test_utils.py index 19a221ab5..c2131c308 100644 --- a/armi/utils/tests/test_utils.py +++ b/armi/utils/tests/test_utils.py @@ -156,16 +156,14 @@ def test_classesInHierarchy(self): self.assertGreater(len(r.core.getBlocks()), 200) def test_codeTiming(self): - """ - Test that codeTiming preserves function attributes when it wraps a function - """ + """Test that codeTiming preserves function attributes when it wraps a function.""" @codeTiming.timed def testFunc(): - """Test function docstring""" + """Test function docstring.""" pass - self.assertEqual(getattr(testFunc, "__doc__"), "Test function docstring") + self.assertEqual(getattr(testFunc, "__doc__"), "Test function docstring.") self.assertEqual(getattr(testFunc, "__name__"), "testFunc") From 18d71cff0b1d6662dc42049f8aa87f62e70fae03 Mon Sep 17 00:00:00 2001 From: bdlafleur Date: Mon, 13 Nov 2023 13:39:41 -0500 Subject: [PATCH 044/176] Adding tests/impl tags for Utils (#1471) --- armi/utils/densityTools.py | 16 ++++++++++ armi/utils/hexagon.py | 11 ++++++- armi/utils/tests/test_densityTools.py | 35 +++++++++++++++++++++ armi/utils/tests/test_hexagon.py | 45 +++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 armi/utils/tests/test_hexagon.py diff --git a/armi/utils/densityTools.py b/armi/utils/densityTools.py index eb29e57df..91cb90f87 100644 --- a/armi/utils/densityTools.py +++ b/armi/utils/densityTools.py @@ -24,6 +24,10 @@ def getNDensFromMasses(rho, massFracs, normalize=False): """ Convert density (g/cc) and massFracs vector into a number densities vector (#/bn-cm). + .. impl:: Get number densities + :id: I_ARMI_UTIL_MASS2N_DENS + :implements: R_ARMI_UTIL_MASS2N_DENS + Parameters ---------- rho : float @@ -168,6 +172,10 @@ def formatMaterialCard( """ Formats nuclides and densities into a MCNP material card. + .. impl:: Create MCNP material card + :id: I_ARMI_UTIL_MCNP_MAT_CARD + :implements: R_ARMI_UTIL_MCNP_MAT_CARD + Parameters ---------- densities : dict @@ -254,6 +262,10 @@ def normalizeNuclideList(nuclideVector, normalization=1.0): """ Normalize the nuclide vector. + .. impl:: Normalize nuclide vector + :id: I_ARMI_UTIL_DENS_TOOLS + :implements: R_ARMI_UTIL_DENS_TOOLS + Parameters ---------- nuclideVector : dict @@ -287,6 +299,10 @@ def expandElementalMassFracsToNuclides( ----- This indirectly updates number densities through mass fractions. + .. impl:: Expand mass fractions to nuclides + :id: I_ARMI_UTIL_EXP_MASS_FRACS + :implements: R_ARMI_UTIL_EXP_MASS_FRACS + Parameters ---------- massFracs : dict(str, float) diff --git a/armi/utils/hexagon.py b/armi/utils/hexagon.py index 2f0360bb3..dc4e706f3 100644 --- a/armi/utils/hexagon.py +++ b/armi/utils/hexagon.py @@ -32,6 +32,10 @@ def area(pitch): """ Area of a hex given the flat-to-flat pitch. + .. impl:: Compute hexagonal area + :id: I_ARMI_UTIL_HEXAGON0 + :implements: R_ARMI_UTIL_HEXAGON + Notes ----- The pitch is the distance between the center of the hexagons in the lattice. @@ -129,5 +133,10 @@ def numRingsToHoldNumCells(numCells): def numPositionsInRing(ring): - """Number of positions in ring (starting at 1) of a hex lattice.""" + """Number of positions in ring (starting at 1) of a hex lattice. + + .. impl:: Compute hexagonal area + :id: I_ARMI_UTIL_HEXAGON1 + :implements: R_ARMI_UTIL_HEXAGON + """ return (ring - 1) * 6 if ring != 1 else 1 diff --git a/armi/utils/tests/test_densityTools.py b/armi/utils/tests/test_densityTools.py index 3a1b45a60..dbe89cc0d 100644 --- a/armi/utils/tests/test_densityTools.py +++ b/armi/utils/tests/test_densityTools.py @@ -21,6 +21,13 @@ class Test_densityTools(unittest.TestCase): def test_expandElementalMassFracsToNuclides(self): + """ + Expand mass fraction to nuclides. + + .. test:: Expand mass fractions to nuclides + :id: T_ARMI_UTIL_EXP_MASS_FRACS + :tests: R_ARMI_UTIL_EXP_MASS_FRACS + """ element = elements.bySymbol["N"] mass = {"N": 1.0} densityTools.expandElementalMassFracsToNuclides(mass, [(element, None)]) @@ -99,7 +106,21 @@ def test_applyIsotopicsMix(self): ) # HM blended self.assertAlmostEqual(uo2.massFrac["O"], massFracO) # non-HM stays unchanged + def test_getNDensFromMasses(self): + """ + Number densities from masses. + + .. test:: Get number densities + :id: T_ARMI_UTIL_MASS2N_DENS + :tests: R_ARMI_UTIL_MASS2N_DENS + """ + nDens = densityTools.getNDensFromMasses(1, {"O": 1, "H": 2}) + + self.assertAlmostEqual(nDens["O"], 0.03764, 5) + self.assertAlmostEqual(nDens["H"], 1.19490, 5) + def test_getMassFractions(self): + """Number densities to mass fraction.""" numDens = {"O17": 0.1512, "PU239": 1.5223, "U234": 0.135} massFracs = densityTools.getMassFractions(numDens) @@ -108,6 +129,7 @@ def test_getMassFractions(self): self.assertAlmostEqual(massFracs["U234"], 0.07937081219437897) def test_calculateNumberDensity(self): + """Mass fraction to number density.""" nDens = densityTools.calculateNumberDensity("U235", 1, 1) self.assertAlmostEqual(nDens, 0.0025621344549254283) @@ -128,6 +150,13 @@ def test_getMassInGrams(self): self.assertAlmostEqual(m, 843.5790671316283) def test_normalizeNuclideList(self): + """ + Normalize a nuclide list. + + .. test:: Normalize nuclide vector + :id: T_ARMI_UTIL_DENS_TOOLS + :tests: R_ARMI_UTIL_DENS_TOOLS + """ nList = {"PU239": 23.2342, "U234": 0.001234, "U235": 34.152} norm = densityTools.normalizeNuclideList(nList) @@ -136,6 +165,12 @@ def test_normalizeNuclideList(self): self.assertAlmostEqual(norm["U235"], 0.5951128604216736) def test_formatMaterialCard(self): + """Formatting material information into an MCNP input card. + + .. test:: Create MCNP material card + :id: T_ARMI_UTIL_MCNP_MAT_CARD + :tests: R_ARMI_UTIL_MCNP_MAT_CARD + """ u235 = nuclideBases.byName["U235"] pu239 = nuclideBases.byName["PU239"] o16 = nuclideBases.byName["O16"] diff --git a/armi/utils/tests/test_hexagon.py b/armi/utils/tests/test_hexagon.py new file mode 100644 index 000000000..2e8a8fa1d --- /dev/null +++ b/armi/utils/tests/test_hexagon.py @@ -0,0 +1,45 @@ +# Copyright 2023 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. +"""Test hexagon tools.""" +import math +import unittest + +from armi.utils import hexagon + + +class Test_hexagon(unittest.TestCase): + def test_hexagon_area(self): + """ + Area of a hexagon. + + .. test:: Compute hexagonal area + :id: T_ARMI_UTIL_HEXAGON0 + :test: R_ARMI_UTIL_HEXAGON + """ + # Calculate area given a pitch + self.assertEqual(hexagon.area(1), math.sqrt(3.0) / 2) + self.assertEqual(hexagon.area(2), 4 * math.sqrt(3.0) / 2) + + def test_numPositionsInRing(self): + """ + Calculate number of positions in a ring of hexagons. + + .. test:: Compute number of positions in ring + :id: T_ARMI_UTIL_HEXAGON1 + :tests: R_ARMI_UTIL_HEXAGON + """ + self.assertEqual(hexagon.numPositionsInRing(1), 1) + self.assertEqual(hexagon.numPositionsInRing(2), 6) + self.assertEqual(hexagon.numPositionsInRing(3), 12) + self.assertEqual(hexagon.numPositionsInRing(4), 18) From 2af81e9946bc77b869151fb2d294277378fe8ca8 Mon Sep 17 00:00:00 2001 From: bdlafleur Date: Mon, 13 Nov 2023 13:57:24 -0500 Subject: [PATCH 045/176] Adding Flag/Flags impl and test tags (#1470) --- armi/reactor/flags.py | 14 +++++++++++++ armi/reactor/tests/test_flags.py | 10 +++++++++- armi/utils/flags.py | 12 +++++++++-- armi/utils/tests/test_flags.py | 34 ++++++++++++++++++++++++++++++-- 4 files changed, 65 insertions(+), 5 deletions(-) diff --git a/armi/reactor/flags.py b/armi/reactor/flags.py index c59775a80..4c9fb97dd 100644 --- a/armi/reactor/flags.py +++ b/armi/reactor/flags.py @@ -284,10 +284,24 @@ def fromStringIgnoreErrors(cls, typeSpec): @classmethod def fromString(cls, typeSpec): + """ + Retrieve flag from a string. + + .. impl:: Retrieve flag from a string + :id: I_ARMI_FLAG_TO_STR0 + :implements: R_ARMI_FLAG_TO_STR + """ return _fromString(cls, typeSpec) @classmethod def toString(cls, typeSpec): + """ + Convert a flag to a string. + + .. impl:: Convert a flag to string + :id: I_ARMI_FLAG_TO_STR1 + :implements: R_ARMI_FLAG_TO_STR + """ return _toString(cls, typeSpec) diff --git a/armi/reactor/tests/test_flags.py b/armi/reactor/tests/test_flags.py index 01abc7f87..57ba77a7a 100644 --- a/armi/reactor/tests/test_flags.py +++ b/armi/reactor/tests/test_flags.py @@ -26,9 +26,17 @@ def test_fromString(self): self._help_fromString(flags.Flags.fromStringIgnoreErrors) self.assertEqual(flags.Flags.fromStringIgnoreErrors("invalid"), flags.Flags(0)) - def test_toString(self): + def test_flagsToAndFromString(self): + """ + Convert flag to and from string for serialization. + + .. test:: Convert flag to a string + :id: T_ARMI_FLAG_TO_STR + :tests: R_ARMI_FLAG_TO_STR + """ f = flags.Flags.FUEL self.assertEqual(flags.Flags.toString(f), "FUEL") + self.assertEqual(f, flags.Flags.fromString("FUEL")) def test_fromStringStrict(self): self._help_fromString(flags.Flags.fromString) diff --git a/armi/utils/flags.py b/armi/utils/flags.py index 7b478ae0c..d89e2b65a 100644 --- a/armi/utils/flags.py +++ b/armi/utils/flags.py @@ -123,6 +123,10 @@ class Flag(metaclass=_FlagMeta): after the class has been defined. Most docs for ``enum.Flag`` should be relevant here, but there are sure to be occasional differences. + .. impl:: No two flags have equivalence + :id: I_ARMI_FLAG_DEFINE + :implements: R_ARMI_FLAG_DEFINE + .. warning:: Python features arbitrary-width integers, allowing one to represent an practically unlimited number of fields. *However*, including more flags than can @@ -216,6 +220,10 @@ def extend(cls, fields: Dict[str, Union[int, auto]]): This alters the class that it is called upon! Existing instances should see the new data, since classes are mutable. + .. impl:: Set of flags are extensible without loss of uniqueness + :id: I_ARMI_FLAG_EXTEND + :implements: R_ARMI_FLAG_EXTEND + Parameters ---------- fields : dict @@ -248,8 +256,8 @@ def to_bytes(self, byteorder="little"): This is useful when storing Flags in a data type of limited size. Python ints can be of arbitrary size, while most other systems can only represent integers - of 32 or 64 bits. For compatibiliy, this function allows to convert the flags to - a sequence of single-byte elements. + of 32 or 64 bits. For compatibility, this function allows to convert the flags + to a sequence of single-byte elements. Note that this uses snake_case to mimic the method on the Python-native int type. diff --git a/armi/utils/tests/test_flags.py b/armi/utils/tests/test_flags.py index 5e93ee647..8054dcc13 100644 --- a/armi/utils/tests/test_flags.py +++ b/armi/utils/tests/test_flags.py @@ -69,14 +69,44 @@ class F(Flag): f2 = F.from_bytes(array) self.assertEqual(f, f2) - def test_collision(self): - """Make sure that we catch value collisions.""" + def test_collision_extension(self): + """Ensure the set of flags cannot be programmatically extended if duplicate created. + + .. test:: Set of flags are extensible without loss of uniqueness + :id: T_ARMI_FLAG_EXTEND + :tests: R_ARMI_FLAG_EXTEND + """ + + class F(Flag): + foo = auto() + bar = 1 + baz = auto() + + F.extend({"a": auto()}) + F.extend({"b": 1}) + + def test_collision_creation(self): + """Make sure that we catch value collisions upon creation. + + .. test:: No two flags have equivalence + :id: T_ARMI_FLAG_DEFINE + :tests: R_ARMI_FLAG_DEFINE + """ with self.assertRaises(AssertionError): class F(Flag): foo = 1 bar = 1 + class D(Flag): + foo = auto() + bar = auto() + baz = auto() + + self.assertEqual(D.foo._value, 1) + self.assertEqual(D.bar._value, 2) + self.assertEqual(D.baz._value, 4) + def test_bool(self): f = ExampleFlag() self.assertFalse(f) From 6603c1daac9e948d55b8f1a93245dec7192d9b41 Mon Sep 17 00:00:00 2001 From: Zachary Prince Date: Mon, 13 Nov 2023 16:17:10 -0700 Subject: [PATCH 046/176] Removing long value of static member from documentation (#1474) --- armi/reactor/tests/test_composites.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/armi/reactor/tests/test_composites.py b/armi/reactor/tests/test_composites.py index 6094b4010..22a92a645 100644 --- a/armi/reactor/tests/test_composites.py +++ b/armi/reactor/tests/test_composites.py @@ -39,7 +39,9 @@ class MockBP: allNuclidesInProblem = set(nuclideBases.byName.keys()) + """:meta hide-value:""" activeNuclides = allNuclidesInProblem + """:meta hide-value:""" inactiveNuclides = set() elementsToExpand = set() customIsotopics = {} From 5b3555cbc6f07470f03d395b00920b2dc4f05290 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 14 Nov 2023 15:00:00 -0800 Subject: [PATCH 047/176] Adding even more impl/test crumbs (#1473) Added test crumbs for: - ncases - materials And added impl and test crumbs for: - database3 - snapshots - uniform mesh converter -axial expansion changer --- armi/bookkeeping/db/__init__.py | 4 + armi/bookkeeping/db/database3.py | 2 - armi/bookkeeping/db/databaseInterface.py | 4 + armi/bookkeeping/db/tests/test_database3.py | 11 ++ armi/bookkeeping/snapshotInterface.py | 8 +- armi/bookkeeping/tests/test_snapshot.py | 14 +++ armi/cases/case.py | 8 ++ armi/cases/inputModifiers/inputModifiers.py | 4 + armi/cases/suiteBuilder.py | 4 + armi/cases/tests/test_cases.py | 18 ++- armi/cases/tests/test_suiteBuilder.py | 8 +- armi/materials/tests/test_air.py | 7 +- armi/materials/tests/test_materials.py | 110 +++++++++++++++++- armi/materials/tests/test_water.py | 7 +- .../converters/axialExpansionChanger.py | 12 ++ .../tests/test_axialExpansionChanger.py | 12 +- .../converters/tests/test_uniformMesh.py | 47 +++++++- armi/reactor/converters/uniformMesh.py | 29 ++++- 18 files changed, 293 insertions(+), 16 deletions(-) diff --git a/armi/bookkeeping/db/__init__.py b/armi/bookkeeping/db/__init__.py index 336d4c85d..7edf2f9c7 100644 --- a/armi/bookkeeping/db/__init__.py +++ b/armi/bookkeeping/db/__init__.py @@ -249,6 +249,10 @@ def _getH5File(db): All this being said, we are probably violating this already with genAuxiliaryData, but we have to start somewhere. + + .. impl:: The ARMI output file has a language-agnostic format. + :id: I_ARMI_DB_H5 + :implements: R_ARMI_DB_H5 """ if isinstance(db, Database3): return db.h5db diff --git a/armi/bookkeeping/db/database3.py b/armi/bookkeeping/db/database3.py index d7bbf9eba..cba50dbba 100644 --- a/armi/bookkeeping/db/database3.py +++ b/armi/bookkeeping/db/database3.py @@ -1458,7 +1458,6 @@ def packSpecialData( ``None`` with a magical value that shouldn't be encountered in realistic scenarios. - Parameters ---------- data @@ -1610,7 +1609,6 @@ def unpackSpecialData(data: numpy.ndarray, attrs, paramName: str) -> numpy.ndarr An ndarray containing the closest possible representation of the data that was originally written to the database. - See Also -------- packSpecialData diff --git a/armi/bookkeeping/db/databaseInterface.py b/armi/bookkeeping/db/databaseInterface.py index eddfd129a..1f5c17dd0 100644 --- a/armi/bookkeeping/db/databaseInterface.py +++ b/armi/bookkeeping/db/databaseInterface.py @@ -222,6 +222,10 @@ def prepRestartRun(self): `startCycle` and `startNode`, having loaded the state from all cycles prior to that in the requested database. + .. impl:: Runs at a particular timenode can be re-instantiated for a snapshot. + :id: I_ARMI_SNAPSHOT_RESTART + :implements: R_ARMI_SNAPSHOT_RESTART + Notes ----- Mixing the use of simple vs detailed cycles settings is allowed, provided diff --git a/armi/bookkeeping/db/tests/test_database3.py b/armi/bookkeeping/db/tests/test_database3.py index a20e49d8a..a9676692b 100644 --- a/armi/bookkeeping/db/tests/test_database3.py +++ b/armi/bookkeeping/db/tests/test_database3.py @@ -95,6 +95,13 @@ def test_writeToDB(self): ) def test_getH5File(self): + """ + Get the h5 file for the database, because that file format is language-agnostic. + + .. test:: Show the database is H5-formatted. + :id: T_ARMI_DB_H5 + :tests: R_ARMI_DB_H5 + """ with self.assertRaises(TypeError): _getH5File(None) @@ -185,6 +192,10 @@ def test_prepRestartRun(self): above. In that cs, `reloadDBName` is set to 'reloadingDB.h5', `startCycle` = 1, and `startNode` = 2. The nonexistent 'reloadingDB.h5' must first be created here for this test. + + .. test:: Runs can be restarted from a snapshot. + :id: T_ARMI_SNAPSHOT_RESTART + :tests: R_ARMI_SNAPSHOT_RESTART """ # first successfully call to prepRestartRun o, r = loadTestReactor( diff --git a/armi/bookkeeping/snapshotInterface.py b/armi/bookkeeping/snapshotInterface.py index e93353e11..fa4462a16 100644 --- a/armi/bookkeeping/snapshotInterface.py +++ b/armi/bookkeeping/snapshotInterface.py @@ -42,7 +42,13 @@ def describeInterfaces(cs): class SnapshotInterface(interfaces.Interface): - """Snapshot managerial interface.""" + """ + Snapshot managerial interface. + + .. impl:: Save extra data to be saved from a run, at specified time nodes. + :id: I_ARMI_SNAPSHOT0 + :implements: R_ARMI_SNAPSHOT + """ name = "snapshot" diff --git a/armi/bookkeeping/tests/test_snapshot.py b/armi/bookkeeping/tests/test_snapshot.py index 5bd670e4e..d22871191 100644 --- a/armi/bookkeeping/tests/test_snapshot.py +++ b/armi/bookkeeping/tests/test_snapshot.py @@ -59,6 +59,13 @@ def test_interactCoupled(self, mockSnapshotRequest): self.assertTrue(mockSnapshotRequest.called) def test_activeateDefaultSnapshots_30cycles2BurnSteps(self): + """ + Test snapshots for 30 cycles and 2 burnsteps, checking the dumpSnapshot setting. + + .. test:: Allow extra data to be saved from a run, at specified time nodes. + :id: T_ARMI_SNAPSHOT0 + :tests: R_ARMI_SNAPSHOT + """ self.assertEqual([], self.cs["dumpSnapshot"]) newSettings = {} @@ -72,6 +79,13 @@ def test_activeateDefaultSnapshots_30cycles2BurnSteps(self): self.assertEqual(["000000", "014000", "029002"], self.si.cs["dumpSnapshot"]) def test_activeateDefaultSnapshots_17cycles5BurnSteps(self): + """ + Test snapshots for 17 cycles and 5 burnsteps, checking the dumpSnapshot setting. + + .. test:: Allow extra data to be saved from a run, at specified time nodes. + :id: T_ARMI_SNAPSHOT1 + :tests: R_ARMI_SNAPSHOT + """ self.assertEqual([], self.cs["dumpSnapshot"]) newSettings = {} diff --git a/armi/cases/case.py b/armi/cases/case.py index e42879e6c..ce6cd0af7 100644 --- a/armi/cases/case.py +++ b/armi/cases/case.py @@ -336,6 +336,10 @@ def run(self): It also activates supervisory things like code coverage checking, profiling, or tracing, if requested by users during debugging. + .. impl:: The case class allows for a generic ARMI simulation. + :id: I_ARMI_CASE + :implements: R_ARMI_CASE + Notes ----- Room for improvement: The coverage, profiling, etc. stuff can probably be moved @@ -556,6 +560,10 @@ def checkInputs(self): """ Checks ARMI inputs for consistency. + .. impl:: Perform validity checks on case inputs. + :id: I_ARMI_CASE_CHECK + :implements: R_ARMI_CASE_CHECK + Returns ------- bool diff --git a/armi/cases/inputModifiers/inputModifiers.py b/armi/cases/inputModifiers/inputModifiers.py index 4e2bd14e8..0a220a03e 100644 --- a/armi/cases/inputModifiers/inputModifiers.py +++ b/armi/cases/inputModifiers/inputModifiers.py @@ -30,6 +30,10 @@ class InputModifier: Some subclasses are provided, but you are expected to make your own design-specific modifiers in most cases. + + .. impl:: A generic tool to modify user inputs on multiple cases. + :id: I_ARMI_CASE_MOD1 + :implements: R_ARMI_CASE_MOD """ FAIL_IF_AFTER = () diff --git a/armi/cases/suiteBuilder.py b/armi/cases/suiteBuilder.py index e2ce1de57..93ee41eae 100644 --- a/armi/cases/suiteBuilder.py +++ b/armi/cases/suiteBuilder.py @@ -45,6 +45,10 @@ class SuiteBuilder: """ Class for constructing a CaseSuite from combinations of modifications on base inputs. + .. impl:: A generic tool to modify user inputs on multiple cases. + :id: I_ARMI_CASE_MOD0 + :implements: R_ARMI_CASE_MOD + Attributes ---------- baseCase : armi.cases.case.Case diff --git a/armi/cases/tests/test_cases.py b/armi/cases/tests/test_cases.py index 203906fc0..4b02b6090 100644 --- a/armi/cases/tests/test_cases.py +++ b/armi/cases/tests/test_cases.py @@ -262,7 +262,12 @@ def test_clone(self): class TestCaseSuiteDependencies(unittest.TestCase): - """CaseSuite tests.""" + """CaseSuite tests. + + .. test:: Dependence allows for one case to start after the completion of another. + :id: T_ARMI_CASE_SUITE0 + :tests: R_ARMI_CASE_SUITE + """ def setUp(self): self.suite = cases.CaseSuite(settings.Settings()) @@ -284,6 +289,17 @@ def test_clone(self): with self.assertRaises(RuntimeError): _clone = self.suite.clone("test_clone") + def test_checkInputs(self): + """ + Test the checkInputs() method on a couple of cases. + + .. test:: Test the checkInputs method. + :id: T_ARMI_CASE_CHECK + :tests: R_ARMI_CASE_CHECK + """ + self.c1.checkInputs() + self.c2.checkInputs() + def test_dependenciesWithObscurePaths(self): """ Test directory dependence. diff --git a/armi/cases/tests/test_suiteBuilder.py b/armi/cases/tests/test_suiteBuilder.py index fc48d64f1..534245aa0 100644 --- a/armi/cases/tests/test_suiteBuilder.py +++ b/armi/cases/tests/test_suiteBuilder.py @@ -46,7 +46,13 @@ def __call__(self, cs, bp, geom): class TestLatinHyperCubeSuiteBuilder(unittest.TestCase): - """Class to test LatinHyperCubeSuiteBuilder.""" + """ + Class to test LatinHyperCubeSuiteBuilder. + + .. test:: A generic mechanism to allow users to modify user inputs in cases. + :id: T_ARMI_CASE_MOD0 + :tests: R_ARMI_CASE_MOD + """ def test_initialize(self): builder = LatinHyperCubeSuiteBuilder(case, size=20) diff --git a/armi/materials/tests/test_air.py b/armi/materials/tests/test_air.py index e183cb644..78311118f 100644 --- a/armi/materials/tests/test_air.py +++ b/armi/materials/tests/test_air.py @@ -178,7 +178,12 @@ class Test_Air(unittest.TestCase): - """unit tests for air materials.""" + """unit tests for air materials. + + .. test:: There is a base class for fluid materials. + :id: T_ARMI_MAT_FLUID1 + :tests: R_ARMI_MAT_FLUID + """ def test_pseudoDensity(self): """ diff --git a/armi/materials/tests/test_materials.py b/armi/materials/tests/test_materials.py index e60d692be..eaf1be3a7 100644 --- a/armi/materials/tests/test_materials.py +++ b/armi/materials/tests/test_materials.py @@ -35,7 +35,12 @@ def setUp(self): self.mat = self.MAT_CLASS() def test_isPicklable(self): - """Test that all materials are picklable so we can do MPI communication of state.""" + """Test that all materials are picklable so we can do MPI communication of state. + + .. test:: Test the material base class. + :id: T_ARMI_MAT_PROPERTIES0 + :tests: R_ARMI_MAT_PROPERTIES + """ stream = pickle.dumps(self.mat) mat = pickle.loads(stream) @@ -45,10 +50,21 @@ def test_isPicklable(self): ) def test_density(self): - """Test that all materials produce a zero density from density.""" + """Test that all materials produce a zero density from density. + + .. test:: Test the material base class. + :id: T_ARMI_MAT_PROPERTIES1 + :tests: R_ARMI_MAT_PROPERTIES + """ self.assertNotEqual(self.mat.density(500), 0) def test_TD(self): + """Test the material density. + + .. test:: Test the material base class. + :id: T_ARMI_MAT_PROPERTIES2 + :tests: R_ARMI_MAT_PROPERTIES + """ self.assertEqual(self.mat.getTD(), self.mat.theoreticalDensityFrac) self.mat.clearCache() @@ -59,6 +75,12 @@ def test_TD(self): self.assertEqual(self.mat.cached, {}) def test_duplicate(self): + """Test the material duplication. + + .. test:: Test the material base class. + :id: T_ARMI_MAT_PROPERTIES3 + :tests: R_ARMI_MAT_PROPERTIES + """ mat = self.mat.duplicate() self.assertEqual(len(mat.massFrac), len(self.mat.massFrac)) @@ -70,6 +92,12 @@ def test_duplicate(self): self.assertEqual(mat.theoreticalDensityFrac, self.mat.theoreticalDensityFrac) def test_cache(self): + """Test the material cache. + + .. test:: Test the material base class. + :id: T_ARMI_MAT_PROPERTIES4 + :tests: R_ARMI_MAT_PROPERTIES + """ self.mat.clearCache() self.assertEqual(len(self.mat.cached), 0) @@ -80,11 +108,23 @@ def test_cache(self): self.assertEqual(val, "Noether") def test_densityKgM3(self): + """Test the density for kg/m^3. + + .. test:: Test the material base class. + :id: T_ARMI_MAT_PROPERTIES5 + :tests: R_ARMI_MAT_PROPERTIES + """ dens = self.mat.density(500) densKgM3 = self.mat.densityKgM3(500) self.assertEqual(dens * 1000.0, densKgM3) def test_pseudoDensityKgM3(self): + """Test the pseudo density for kg/m^3. + + .. test:: Test the material base class. + :id: T_ARMI_MAT_PROPERTIES6 + :tests: R_ARMI_MAT_PROPERTIES + """ dens = self.mat.pseudoDensity(500) densKgM3 = self.mat.pseudoDensityKgM3(500) self.assertEqual(dens * 1000.0, densKgM3) @@ -101,6 +141,12 @@ class MaterialFindingTests(unittest.TestCase): """Make sure materials are discoverable as designed.""" def test_findMaterial(self): + """Test resolveMaterialClassByName() function. + + .. test:: You can find a meterial by name. + :id: T_ARMI_MAT_NAME + :tests: R_ARMI_MAT_NAME + """ self.assertIs( materials.resolveMaterialClassByName( "Void", namespaceOrder=["armi.materials"] @@ -737,6 +783,13 @@ class Thorium_TestCase(_Material_Test, unittest.TestCase): MAT_CLASS = materials.Thorium def test_setDefaultMassFracs(self): + """ + Test default mass fractions. + + .. test:: The materials generate nuclide mass fractions. + :id: T_ARMI_MAT_FRACS0 + :tests: R_ARMI_LOG + """ self.mat.setDefaultMassFracs() cur = self.mat.massFrac ref = {"TH232": 1.0} @@ -810,12 +863,23 @@ 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 + """ self.mat.setDefaultMassFracs() cur = self.mat.pseudoDensity() self.assertEqual(cur, 0.0) def test_density(self): - """This material has no density function.""" + """This material has no density. + + .. test:: There is a void material. + :id: T_ARMI_MAT_VOID1 + :tests: R_ARMI_MAT_VOID + """ self.assertEqual(self.mat.density(500), 0) self.mat.setDefaultMassFracs() @@ -823,11 +887,23 @@ def test_density(self): 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 + """ 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 + """ self.assertEqual(len(self.mat.propertyValidTemperature), 0) @@ -839,6 +915,13 @@ def test_density(self): self.assertEqual(self.mat.density(500), 0) def test_setDefaultMassFracs(self): + """ + Test default mass fractions. + + .. test:: The materials generate nuclide mass fractions. + :id: T_ARMI_MAT_FRACS1 + :tests: R_ARMI_LOG + """ self.mat.setDefaultMassFracs() cur = self.mat.pseudoDensity(500) self.assertEqual(cur, 0.0) @@ -852,6 +935,13 @@ def test_propertyValidTemperature(self): class Lead_TestCase(_Material_Test, unittest.TestCase): + """Unit tests for lead materials. + + .. test:: There is a base class for fluid materials. + :id: T_ARMI_MAT_FLUID2 + :tests: R_ARMI_MAT_FLUID + """ + MAT_CLASS = materials.Lead def test_volumetricExpansion(self): @@ -878,6 +968,13 @@ def test_linearExpansion(self): self.assertEqual(cur, ref) def test_setDefaultMassFracs(self): + """ + Test default mass fractions. + + .. test:: The materials generate nuclide mass fractions. + :id: T_ARMI_MAT_FRACS2 + :tests: R_ARMI_LOG + """ self.mat.setDefaultMassFracs() cur = self.mat.massFrac ref = {"PB": 1} @@ -908,6 +1005,13 @@ class LeadBismuth_TestCase(_Material_Test, unittest.TestCase): MAT_CLASS = materials.LeadBismuth def test_setDefaultMassFracs(self): + """ + Test default mass fractions. + + .. test:: The materials generate nuclide mass fractions. + :id: T_ARMI_MAT_FRACS3 + :tests: R_ARMI_LOG + """ self.mat.setDefaultMassFracs() cur = self.mat.massFrac ref = {"BI209": 0.555, "PB": 0.445} diff --git a/armi/materials/tests/test_water.py b/armi/materials/tests/test_water.py index fc6381b83..56b671527 100644 --- a/armi/materials/tests/test_water.py +++ b/armi/materials/tests/test_water.py @@ -19,7 +19,12 @@ class Test_Water(unittest.TestCase): - """Unit tests for water materials.""" + """Unit tests for water materials. + + .. test:: There is a base class for fluid materials. + :id: T_ARMI_MAT_FLUID0 + :tests: R_ARMI_MAT_FLUID + """ def test_water_at_freezing(self): """ diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index b3dd509a7..5f0818df6 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -65,6 +65,10 @@ def expandColdDimsToHot( """ 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 + Parameters ---------- assems: list[:py:class:`Assembly `] @@ -138,6 +142,10 @@ def performPrescribedAxialExpansion( ): """Perform axial expansion of an assembly given prescribed expansion percentages. + .. impl:: Perform expansion/contraction, given a list of components and expansion coefficients. + :id: I_ARMI_AXIAL_EXP_PRESC + :implements: R_ARMI_AXIAL_EXP_PRESC + Parameters ---------- a : :py:class:`Assembly ` @@ -168,6 +176,10 @@ def performThermalAxialExpansion( ): """Perform thermal expansion for an assembly given an axial temperature grid and field. + .. impl:: Perform thermal expansion/contraction, given an axial temp distribution over an assembly. + :id: I_ARMI_AXIAL_EXP_THERM + :implements: R_ARMI_AXIAL_EXP_THERM + Parameters ---------- a : :py:class:`Assembly ` diff --git a/armi/reactor/converters/tests/test_axialExpansionChanger.py b/armi/reactor/converters/tests/test_axialExpansionChanger.py index 4ba6af5e8..f40503b4b 100644 --- a/armi/reactor/converters/tests/test_axialExpansionChanger.py +++ b/armi/reactor/converters/tests/test_axialExpansionChanger.py @@ -775,7 +775,13 @@ def test_specifyTargetComponet_MultipleFound(self): self.assertEqual(the_exception.error_code, 3) def test_manuallySetTargetComponent(self): - """Ensures that target components can be manually set (is done in practice via blueprints).""" + """ + Ensures that target components can be manually set (is done in practice via blueprints). + + .. test:: Allow user-specified target axial expansion components on a given block. + :id: T_ARMI_MANUAL_TARG_COMP + :tests: R_ARMI_MANUAL_TARG_COMP + """ b = HexBlock("dummy", height=10.0) ductDims = {"Tinput": 25.0, "Thot": 25.0, "op": 17, "ip": 0.0, "mult": 1.0} duct = Hexagon("duct", "FakeMat", **ductDims) @@ -834,6 +840,10 @@ def setUp(self): def test_coldAssemblyExpansion(self): """Block heights are cold and should be expanded. + .. test:: Preserve the total height of a compatible ARMI assembly. + :id: T_ARMI_ASSEM_HEIGHT_PRES + :tests: R_ARMI_ASSEM_HEIGHT_PRES + Notes ----- Two assertions here: diff --git a/armi/reactor/converters/tests/test_uniformMesh.py b/armi/reactor/converters/tests/test_uniformMesh.py index a5a0869bd..b0319fef8 100644 --- a/armi/reactor/converters/tests/test_uniformMesh.py +++ b/armi/reactor/converters/tests/test_uniformMesh.py @@ -251,7 +251,13 @@ def test_computeAverageAxialMesh(self): self.assertNotEqual(refMesh[4], avgMesh[4], "Not equal above the fuel.") def test_filterMesh(self): - """Test that the mesh can be correctly filtered.""" + """ + Test that the mesh can be correctly filtered. + + .. test:: Try to preserve the boundaries of fuel and control material. + :id: T_ARMI_UMC_NON_UNIFORM1 + :tests: R_ARMI_UMC_NON_UNIFORM + """ meshList = [1.0, 3.0, 4.0, 7.0, 9.0, 12.0, 16.0, 19.0, 20.0] anchorPoints = [4.0, 16.0] combinedMesh = self.generator._filterMesh( @@ -295,7 +301,17 @@ def test_filteredTopAndBottom(self): self.assertListEqual(ctrlAndFuelTops, [75.0, 101.25, 105.0]) def test_generateCommonMesh(self): - """Covers generateCommonmesh() and _decuspAxialMesh().""" + """ + Covers generateCommonmesh() and _decuspAxialMesh(). + + .. test:: Produce a uniform mesh with a size no smaller than a user-specified value. + :id: T_ARMI_UMC_MIN_MESH + :tests: R_ARMI_UMC_MIN_MESH + + .. test:: Try to preserve the boundaries of fuel and control material. + :id: T_ARMI_UMC_NON_UNIFORM0 + :tests: R_ARMI_UMC_NON_UNIFORM + """ self.generator.generateCommonMesh() expectedMesh = [ 25.0, @@ -346,7 +362,7 @@ def test_blueprintCopy(self): "allNuclidesInProblem", "elementsToExpand", "inertNuclides", - ] # note, items within toCompare must be list or "list-like", like an ordered set + ] # Note: items within toCompare must be list or "list-like", like an ordered set for attr in toCompare: for c, o in zip(getattr(converted, attr), getattr(original, attr)): self.assertEqual(c, o) @@ -394,6 +410,13 @@ def setUp(self): ) def test_convertNumberDensities(self): + """ + Test the reactor mass before and after conversion. + + .. test:: Make a copy of the reactor where the new reactor core has a uniform axial mesh. + :id: T_ARMI_UMC + :tests: R_ARMI_UMC + """ refMass = self.r.core.getMass("U235") applyNonUniformHeightDistribution( self.r @@ -413,6 +436,13 @@ def test_convertNumberDensities(self): ) # conversion didn't change source reactor mass def test_applyStateToOriginal(self): + """ + Test applyStateToOriginal() to revert mesh conversion. + + .. test:: Map select parameters from composites on the new mesh to the original mesh. + :id: T_ARMI_UMC_PARAM_BACKWARD0 + :tests: R_ARMI_UMC_PARAM_BACKWARD + """ applyNonUniformHeightDistribution(self.r) # note: this perturbs the ref mass self.converter.convert(self.r) @@ -505,6 +535,13 @@ def test_convertNumberDensities(self): ) # conversion didn't change source reactor mass def test_applyStateToOriginal(self): + """ + Test applyStateToOriginal() to revert mesh conversion. + + .. test:: Map select parameters from composites on the new mesh to the original mesh. + :id: T_ARMI_UMC_PARAM_BACKWARD1 + :tests: R_ARMI_UMC_PARAM_BACKWARD + """ applyNonUniformHeightDistribution(self.r) # note: this perturbs the ref. mass # set original parameters on pre-mapped core with non-uniform assemblies @@ -612,6 +649,10 @@ def test_setStateFromOverlaps(self): Test that state is translated correctly from source to dest assems. Here we set flux and pdens to 3 on the source blocks. + + .. test:: Map select parameters from composites on the original mesh to the new mesh. + :id: T_ARMI_UMC_PARAM_FORWARD + :tests: R_ARMI_UMC_PARAM_FORWARD """ paramList = ["flux", "pdens"] for pName in paramList: diff --git a/armi/reactor/converters/uniformMesh.py b/armi/reactor/converters/uniformMesh.py index daeb320f5..19da7fe4e 100644 --- a/armi/reactor/converters/uniformMesh.py +++ b/armi/reactor/converters/uniformMesh.py @@ -118,6 +118,14 @@ def generateCommonMesh(self): """ Generate a common axial mesh to use. + .. impl:: Try to preserve the boundaries of fuel and control material. + :id: I_ARMI_UMC_NON_UNIFORM + :implements: R_ARMI_UMC_NON_UNIFORM + + .. impl:: Produce a mesh with a size no smaller than a user-specified value. + :id: I_ARMI_UMC_MIN_MESH + :implements: R_ARMI_UMC_MIN_MESH + Notes ----- Attempts to reduce the effect of fuel and control rod absorber smearing @@ -407,7 +415,17 @@ def __init__(self, cs=None): self._minimumMeshSize = cs[CONF_UNIFORM_MESH_MINIMUM_SIZE] def convert(self, r=None): - """Create a new reactor core with a uniform mesh.""" + """ + Create a new reactor core with a uniform mesh. + + .. impl:: Make a copy of the reactor where the new core has a uniform axial mesh. + :id: I_ARMI_UMC + :implements: R_ARMI_UMC + + .. impl:: Map select parameters from composites on the original mesh to the new mesh. + :id: I_ARMI_UMC_PARAM_FORWARD + :implements: R_ARMI_UMC_PARAM_FORWARD + """ if r is None: raise ValueError(f"No reactor provided in {self}") @@ -519,7 +537,14 @@ def initNewReactor(sourceReactor, cs): return newReactor def applyStateToOriginal(self): - """Apply the state of the converted reactor back to the original reactor, mapping number densities and block parameters.""" + """ + Apply the state of the converted reactor back to the original reactor, + mapping number densities and block parameters. + + .. impl:: Map select parameters from composites on the new mesh to the original mesh. + :id: I_ARMI_UMC_PARAM_BACKWARD + :implements: R_ARMI_UMC_PARAM_BACKWARD + """ runLog.extra( f"Applying uniform neutronics results from {self.convReactor} to {self._sourceReactor}" ) From c7995865ca66d97eefaeb958148aa758358e479c Mon Sep 17 00:00:00 2001 From: bdlafleur Date: Wed, 15 Nov 2023 09:44:37 -0500 Subject: [PATCH 048/176] Adding impl/test crumbs for blueprints (#1476) --- armi/reactor/blueprints/assemblyBlueprint.py | 4 ++++ armi/reactor/blueprints/blockBlueprint.py | 7 ++++++- armi/reactor/blueprints/componentBlueprint.py | 4 ++++ armi/reactor/blueprints/gridBlueprint.py | 11 +++++++++- armi/reactor/blueprints/reactorBlueprint.py | 8 ++++++++ .../tests/test_assemblyBlueprints.py | 7 +++++++ .../blueprints/tests/test_blockBlueprints.py | 20 ++++++++++++++++--- .../blueprints/tests/test_blueprints.py | 7 ++++++- .../blueprints/tests/test_gridBlueprints.py | 20 +++++++++++++++++++ .../tests/test_reactorBlueprints.py | 16 ++++++++++++++- 10 files changed, 97 insertions(+), 7 deletions(-) diff --git a/armi/reactor/blueprints/assemblyBlueprint.py b/armi/reactor/blueprints/assemblyBlueprint.py index 99fd33293..4864c64d2 100644 --- a/armi/reactor/blueprints/assemblyBlueprint.py +++ b/armi/reactor/blueprints/assemblyBlueprint.py @@ -92,6 +92,10 @@ class AssemblyBlueprint(yamlize.Object): This class utilizes ``yamlize`` to enable serialization to and from the blueprints YAML file. + + .. impl:: Create assembly from blueprint file + :id: I_ARMI_BP_ASSEM + :implements: R_ARMI_BP_ASSEM """ name = yamlize.Attribute(type=str) diff --git a/armi/reactor/blueprints/blockBlueprint.py b/armi/reactor/blueprints/blockBlueprint.py index 68443f8af..33df26448 100644 --- a/armi/reactor/blueprints/blockBlueprint.py +++ b/armi/reactor/blueprints/blockBlueprint.py @@ -42,7 +42,12 @@ def _configureGeomOptions(): class BlockBlueprint(yamlize.KeyedList): - """Input definition for Block.""" + """Input definition for Block. + + .. impl:: Create a Block from blueprint file + :id: I_ARMI_BP_BLOCK + :implements: R_ARMI_BP_BLOCK + """ item_type = componentBlueprint.ComponentBlueprint key_attr = componentBlueprint.ComponentBlueprint.name diff --git a/armi/reactor/blueprints/componentBlueprint.py b/armi/reactor/blueprints/componentBlueprint.py index 6ef83be55..51ba17857 100644 --- a/armi/reactor/blueprints/componentBlueprint.py +++ b/armi/reactor/blueprints/componentBlueprint.py @@ -119,6 +119,10 @@ class ComponentBlueprint(yamlize.Object): """ This class defines the inputs necessary to build ARMI component objects. It uses ``yamlize`` to enable serialization to and from YAML. + + .. impl:: Construct component from blueprint file + :id: I_ARMI_BP_COMP + :implements: R_ARMI_BP_COMP """ name = yamlize.Attribute(type=str) diff --git a/armi/reactor/blueprints/gridBlueprint.py b/armi/reactor/blueprints/gridBlueprint.py index 1cc6eb1d7..2ce46b968 100644 --- a/armi/reactor/blueprints/gridBlueprint.py +++ b/armi/reactor/blueprints/gridBlueprint.py @@ -246,7 +246,12 @@ def readFromLatticeMap(self, value): self._readFromLatticeMap = value def construct(self): - """Build a Grid from a grid definition.""" + """Build a Grid from a grid definition. + + .. impl:: Define a lattice map in reactor core + :id: I_ARMI_BP_GRID + :implements: R_ARMI_BP_GRID + """ self._readGridContents() grid = self._constructSpatialGrid() return grid @@ -534,6 +539,10 @@ def saveToStream(stream, bluep, full=False, tryMap=False): full: bool ~ Is this a full output file, or just a partial/grids? tryMap: regardless of input form, attempt to output as a lattice map. let's face it; they're prettier. + + .. impl:: Write a blueprint file from a blueprint object + :id: I_ARMI_BP_TO_DB + :implements: R_ARMI_BP_TO_DB """ # To save, we want to try our best to output our grid blueprints in the lattice # map style. However, we do not want to wreck the state that the current diff --git a/armi/reactor/blueprints/reactorBlueprint.py b/armi/reactor/blueprints/reactorBlueprint.py index 6a67b7e45..7667404d7 100644 --- a/armi/reactor/blueprints/reactorBlueprint.py +++ b/armi/reactor/blueprints/reactorBlueprint.py @@ -101,6 +101,14 @@ def _resolveSystemType(typ: str): def construct(self, cs, bp, reactor, geom=None, loadAssems=True): """Build a core/IVS/EVST/whatever and fill it with children. + .. impl:: Build core and spent fuel pool from blueprint + :id: I_ARMI_BP_SYSTEMS + :implements: R_ARMI_BP_SYSTEMS + + .. impl:: Create core object with blueprint + :id: I_ARMI_BP_CORE + :implements: R_ARMI_BP_CORE + Parameters ---------- cs : :py:class:`Settings ` object. diff --git a/armi/reactor/blueprints/tests/test_assemblyBlueprints.py b/armi/reactor/blueprints/tests/test_assemblyBlueprints.py index 1031de24b..1197f41bd 100644 --- a/armi/reactor/blueprints/tests/test_assemblyBlueprints.py +++ b/armi/reactor/blueprints/tests/test_assemblyBlueprints.py @@ -183,6 +183,13 @@ def loadCustomAssembly(self, assemblyInput): return design.assemblies["fuel a"] def test_checkParamConsistency(self): + """ + Load assembly from a blueprint file. + + .. test:: Create assembly from blueprint file + :id: T_ARMI_BP_ASSEM + :tests: R_ARMI_BP_ASSEM + """ # make sure a good example doesn't error a = self.loadCustomAssembly(self.twoBlockInput_correct) blockAxialMesh = a.getAxialMesh() diff --git a/armi/reactor/blueprints/tests/test_blockBlueprints.py b/armi/reactor/blueprints/tests/test_blockBlueprints.py index 36882a74d..02988516f 100644 --- a/armi/reactor/blueprints/tests/test_blockBlueprints.py +++ b/armi/reactor/blueprints/tests/test_blockBlueprints.py @@ -63,7 +63,7 @@ op: 16.75 other fuel: &block_fuel_other grid name: fuelgrid - flags: fuel test + flags: fuel test depletable fuel: shape: Circle material: UZr @@ -255,7 +255,12 @@ class TestGriddedBlock(unittest.TestCase): - """Tests for a block that has components in a lattice.""" + """Tests for a block that has components in a lattice. + + .. test:: Create block with blueprint file + :id: T_ARMI_BP_BLOCK + :tests: R_ARMI_BP_BLOCK + """ def setUp(self): self.cs = settings.Settings() @@ -301,6 +306,13 @@ def test_nonLatticeComponentHasRightMult(self): self.assertEqual(duct.getDimension("mult"), 1.0) def test_explicitFlags(self): + """ + Test flags are created from blueprint file. + + .. test:: Test depletable nuc flags + :id: T_ARMI_BP_NUC_FLAGS + :tests: R_ARMI_BP_NUC_FLAGS + """ a1 = self.blueprints.assemDesigns.bySpecifier["IC"].construct( self.cs, self.blueprints ) @@ -312,7 +324,9 @@ def test_explicitFlags(self): ) self.assertTrue(b1.hasFlags(Flags.FUEL, exact=True)) - self.assertTrue(b2.hasFlags(Flags.FUEL | Flags.TEST, exact=True)) + self.assertTrue( + b2.hasFlags(Flags.FUEL | Flags.TEST | Flags.DEPLETABLE, exact=True) + ) self.assertEqual(a1.p.flags, Flags.FUEL) self.assertTrue(a1.hasFlags(Flags.FUEL, exact=True)) diff --git a/armi/reactor/blueprints/tests/test_blueprints.py b/armi/reactor/blueprints/tests/test_blueprints.py index 118e54e99..ffd3540f9 100644 --- a/armi/reactor/blueprints/tests/test_blueprints.py +++ b/armi/reactor/blueprints/tests/test_blueprints.py @@ -87,7 +87,12 @@ def test_specialIsotopicVectors(self): self.assertAlmostEqual(mox["PU239"], 0.00286038) def test_componentDimensions(self): - """Tests that the user can specifiy the dimensions of a component with arbitray fidelity.""" + """Tests that the user can specify the dimensions of a component with arbitrary fidelity. + + .. test:: Test that a component can be correctly created from a blueprint file + :id: T_ARMI_BP_COMP + :tests: R_ARMI_BP_COMP + """ fuelAssem = self.blueprints.constructAssem(self.cs, name="igniter fuel") fuel = fuelAssem.getComponents(Flags.FUEL)[0] self.assertAlmostEqual(fuel.getDimension("od", cold=True), 0.86602) diff --git a/armi/reactor/blueprints/tests/test_gridBlueprints.py b/armi/reactor/blueprints/tests/test_gridBlueprints.py index 6735888f7..6e8f52a91 100644 --- a/armi/reactor/blueprints/tests/test_gridBlueprints.py +++ b/armi/reactor/blueprints/tests/test_gridBlueprints.py @@ -314,6 +314,13 @@ def test_contents(self): self.assertIn("core", self.grids) def test_roundTrip(self): + """ + Test saving blueprint data to a stream. + + .. test:: Write blueprints settings to disk + :id: T_ARMI_BP_TO_DB + :tests: R_ARMI_BP_TO_DB + """ stream = io.StringIO() saveToStream(stream, self.grids, False, True) stream.seek(0) @@ -321,6 +328,13 @@ def test_roundTrip(self): self.assertIn("third", gridBp["core"].symmetry) def test_tiny_map(self): + """ + Test that a lattice map can be defined, written, and read in from blueprint file. + + .. test:: Define a lattice map in reactor core + :id: T_ARMI_BP_GRID1 + :tests: R_ARMI_BP_GRID + """ grid = Grids.load(TINY_GRID) stream = io.StringIO() saveToStream(stream, grid, full=True, tryMap=True) @@ -381,6 +395,12 @@ def test_simpleRead(self): self.assertEqual(gridDesign4.gridContents[-4, -3], "1") def test_simpleReadLatticeMap(self): + """Read lattice map and create a grid. + + .. test:: Define a lattice map in reactor core + :id: T_ARMI_BP_GRID0 + :tests: R_ARMI_BP_GRID + """ # Cartesian full, even/odd hybrid gridDesign4 = self.grids["sfp even"] _grid = gridDesign4.construct() diff --git a/armi/reactor/blueprints/tests/test_reactorBlueprints.py b/armi/reactor/blueprints/tests/test_reactorBlueprints.py index 20cc9666b..b13ccdf05 100644 --- a/armi/reactor/blueprints/tests/test_reactorBlueprints.py +++ b/armi/reactor/blueprints/tests/test_reactorBlueprints.py @@ -16,7 +16,9 @@ import os import unittest +from armi.reactor.assemblyLists import SpentFuelPool from armi.reactor import blueprints +from armi.reactor.reactors import Core from armi import settings from armi.reactor import reactors from armi.reactor.blueprints import reactorBlueprint @@ -101,11 +103,23 @@ def _setupReactor(self): return core, sfp def test_construct(self): - """Actually construct some reactor systems.""" + """Actually construct some reactor systems. + + .. test:: Create core and spent fuel pool with blueprint + :id: T_ARMI_BP_SYSTEMS + :tests: R_ARMI_BP_SYSTEMS + + .. test:: Create core object with blueprint + :id: T_ARMI_BP_CORE + :tests: R_ARMI_BP_CORE + """ core, sfp = self._setupReactor() self.assertEqual(len(core), 2) self.assertEqual(len(sfp), 4) + self.assertIsInstance(core, Core) + self.assertIsInstance(sfp, SpentFuelPool) + def test_materialDataSummary(self): """Test that the material data summary for the core is valid as a printout to the stdout.""" expectedMaterialData = [ From 98566492147d6f5702233f89d308c279ff7e6353 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:59:11 -0800 Subject: [PATCH 049/176] Adding impl/test crumbs for reqs (#1477) --- armi/apps.py | 4 +++ armi/nuclearDataIO/cccc/compxs.py | 4 +-- armi/nuclearDataIO/cccc/dif3d.py | 9 +++++- armi/nuclearDataIO/cccc/dlayxs.py | 13 +++++++-- armi/nuclearDataIO/cccc/gamiso.py | 6 +++- armi/nuclearDataIO/cccc/geodst.py | 5 +++- armi/nuclearDataIO/cccc/isotxs.py | 18 ++++++++---- armi/nuclearDataIO/cccc/pmatrx.py | 10 +++++-- armi/nuclearDataIO/cccc/tests/test_dif3d.py | 22 ++++++++++++--- armi/nuclearDataIO/cccc/tests/test_dlayxs.py | 20 ++++++++----- armi/nuclearDataIO/cccc/tests/test_gamiso.py | 19 +++++++++++++ armi/nuclearDataIO/cccc/tests/test_geodst.py | 14 ++++++++-- armi/operators/operator.py | 25 +++++++++++++++-- armi/operators/operatorMPI.py | 8 +++++- armi/plugins.py | 13 ++++++++- armi/reactor/converters/geometryConverters.py | 10 ++++++- .../tests/test_geometryConverters.py | 9 ++++-- .../parameters/parameterDefinitions.py | 4 +++ armi/reactor/reactors.py | 25 ++++++++++++++++- armi/reactor/tests/test_reactors.py | 28 ++++++++++++++++++- armi/reactor/tests/test_rz_reactors.py | 7 +++++ 21 files changed, 234 insertions(+), 39 deletions(-) diff --git a/armi/apps.py b/armi/apps.py index a855840a4..9e9d0d5f7 100644 --- a/armi/apps.py +++ b/armi/apps.py @@ -55,6 +55,10 @@ class App: pain, as the results returned by the individual plugins may need to be merged and/or checked for errors. Adding that logic here reduces boilerplate throughout the rest of the code. + + .. impl:: An App has a plugin manager. + :id: I_ARMI_APP_PLUGINS + :implements: R_ARMI_APP_PLUGINS """ name = "armi" diff --git a/armi/nuclearDataIO/cccc/compxs.py b/armi/nuclearDataIO/cccc/compxs.py index acc8883ba..0ddc6d24e 100644 --- a/armi/nuclearDataIO/cccc/compxs.py +++ b/armi/nuclearDataIO/cccc/compxs.py @@ -162,7 +162,7 @@ class _CompxsIO(cccc.Stream): See Also -------- - armi.nuclearDataIO.cccc.isotxs._IsotxsIO + armi.nuclearDataIO.cccc.isotxs.IsotxsIO """ _METADATA_TAGS = ( @@ -223,7 +223,7 @@ def readWrite(self): See Also -------- - armi.nuclearDataIO.cccc.isotxs._IsotxsIO.readWrite : reading/writing ISOTXS files + armi.nuclearDataIO.cccc.isotxs.IsotxsIO.readWrite : reading/writing ISOTXS files """ runLog.info( "{} macroscopic cross library {}".format( diff --git a/armi/nuclearDataIO/cccc/dif3d.py b/armi/nuclearDataIO/cccc/dif3d.py index f2e5083ce..81848e28a 100644 --- a/armi/nuclearDataIO/cccc/dif3d.py +++ b/armi/nuclearDataIO/cccc/dif3d.py @@ -93,6 +93,8 @@ def __init__(self): class Dif3dStream(cccc.StreamWithDataContainer): + """Tool to read and write DIF3D files.""" + @staticmethod def _getDataContainer() -> Dif3dData: return Dif3dData() @@ -197,7 +199,12 @@ def _rw5DRecord(self) -> None: ) def readWrite(self): - """Reads or writes metadata and data from 5 records.""" + """Reads or writes metadata and data from 5 records. + + .. impl:: Tool to read and write DIF3D files. + :id: I_ARMI_NUCDATA_DIF3D + :implements: R_ARMI_NUCDATA_DIF3D + """ msg = f"{'Reading' if 'r' in self._fileMode else 'Writing'} DIF3D binary data {self}" runLog.info(msg) diff --git a/armi/nuclearDataIO/cccc/dlayxs.py b/armi/nuclearDataIO/cccc/dlayxs.py index 8d7e99fcc..51736b851 100644 --- a/armi/nuclearDataIO/cccc/dlayxs.py +++ b/armi/nuclearDataIO/cccc/dlayxs.py @@ -108,7 +108,7 @@ def _write(delay, fileName, fileMode): def _readWrite(delay, fileName, fileMode): - with _DlayxsIO(fileName, fileMode, delay) as rw: + with DlayxsIO(fileName, fileMode, delay) as rw: rw.readWrite() return delay @@ -126,7 +126,6 @@ class Dlayxs(collections.OrderedDict): If you want an average over all nuclides, then you need to produce it using the properly-computed average contributions of each nuclide. - Attributes ---------- nuclideFamily : dict @@ -216,13 +215,21 @@ def _checkContributions(self): ) -class _DlayxsIO(cccc.Stream): +class DlayxsIO(cccc.Stream): + """Contains DLAYXS read/writers.""" + def __init__(self, fileName, fileMode, dlayxs): cccc.Stream.__init__(self, fileName, fileMode) self.dlayxs = dlayxs self.metadata = dlayxs.metadata def readWrite(self): + """Read and write DLAYXS files. + + .. impl:: Tool to read and write DLAYXS files. + :id: I_ARMI_NUCDATA_DLAYXS + :implements: R_ARMI_NUCDATA_DLAYXS + """ runLog.info( "{} DLAYXS library {}".format( "Reading" if "r" in self._fileMode else "Writing", self diff --git a/armi/nuclearDataIO/cccc/gamiso.py b/armi/nuclearDataIO/cccc/gamiso.py index 2db1ac40f..ef07598d5 100644 --- a/armi/nuclearDataIO/cccc/gamiso.py +++ b/armi/nuclearDataIO/cccc/gamiso.py @@ -18,6 +18,10 @@ GAMISO is a binary file created by MC**2-v3 that contains multigroup microscopic gamma cross sections. GAMISO data is contained within a :py:class:`~armi.nuclearDataIO.xsLibraries.XSLibrary`. +.. impl:: Tool to read and write GAMISO files. + :id: I_ARMI_NUCDATA_GAMISO + :implements: R_ARMI_NUCDATA_GAMISO + See [GAMSOR]_. .. [GAMSOR] Smith, M. A., Lee, C. H., and Hill, R. N. GAMSOR: Gamma Source Preparation and DIF3D Flux Solution. United States: @@ -110,7 +114,7 @@ def addDummyNuclidesToLibrary(lib, dummyNuclides): return any(dummyNuclideKeysAddedToLibrary) -class _GamisoIO(isotxs._IsotxsIO): +class _GamisoIO(isotxs.IsotxsIO): """ A reader/writer for GAMISO data files. diff --git a/armi/nuclearDataIO/cccc/geodst.py b/armi/nuclearDataIO/cccc/geodst.py index afe9a2470..ee777bcad 100644 --- a/armi/nuclearDataIO/cccc/geodst.py +++ b/armi/nuclearDataIO/cccc/geodst.py @@ -120,7 +120,6 @@ class GeodstStream(cccc.StreamWithDataContainer): fileMode: str string indicating if ``fileName`` is being read or written, and in ascii or binary format - """ @staticmethod @@ -133,6 +132,10 @@ def readWrite(self): Logic to control which records will be present is here, which comes directly off the File specification. + + .. impl:: Read and write GEODST files. + :id: I_ARMI_NUCDATA_GEODST + :implements: R_ARMI_NUCDATA_GEODST """ self._rwFileID() self._rw1DRecord() diff --git a/armi/nuclearDataIO/cccc/isotxs.py b/armi/nuclearDataIO/cccc/isotxs.py index 7ad8822dd..c708b7244 100644 --- a/armi/nuclearDataIO/cccc/isotxs.py +++ b/armi/nuclearDataIO/cccc/isotxs.py @@ -187,7 +187,7 @@ def addDummyNuclidesToLibrary(lib, dummyNuclides): return any(dummyNuclideKeysAddedToLibrary) -class _IsotxsIO(cccc.Stream): +class IsotxsIO(cccc.Stream): """ A semi-abstract stream for reading and writing to a :py:class:`~armi.nuclearDataIO.isotxs.Isotxs`. @@ -263,6 +263,12 @@ def _updateFileLabel(self): self._metadata["label"] = self._FILE_LABEL def readWrite(self): + """Read and write ISOTSX file. + + .. impl:: Tool to read and write ISOTXS files. + :id: I_ARMI_NUCDATA_ISOTXS + :implements: R_ARMI_NUCDATA_ISOTXS + """ self._rwMessage() properties.unlockImmutableProperties(self._lib) try: @@ -381,10 +387,10 @@ def _computeNumIsotxsRecords(self, nuclide): return numRecords -readBinary = _IsotxsIO.readBinary -readAscii = _IsotxsIO.readAscii -writeBinary = _IsotxsIO.writeBinary -writeAscii = _IsotxsIO.writeAscii +readBinary = IsotxsIO.readBinary +readAscii = IsotxsIO.readAscii +writeBinary = IsotxsIO.writeBinary +writeAscii = IsotxsIO.writeAscii class _IsotxsNuclideIO: @@ -393,7 +399,7 @@ class _IsotxsNuclideIO: Notes ----- - This is to be used in conjunction with an _IsotxsIO object. + This is to be used in conjunction with an IsotxsIO object. """ def __init__(self, nuclide, isotxsIO, lib): diff --git a/armi/nuclearDataIO/cccc/pmatrx.py b/armi/nuclearDataIO/cccc/pmatrx.py index 53b155f7a..9c1610f35 100644 --- a/armi/nuclearDataIO/cccc/pmatrx.py +++ b/armi/nuclearDataIO/cccc/pmatrx.py @@ -162,12 +162,12 @@ def _write(lib, fileName, fileMode): def _readWrite(lib, fileName, fileMode, getNuclideFunc): - with _PmatrxIO(fileName, lib, fileMode, getNuclideFunc) as rw: + with PmatrxIO(fileName, lib, fileMode, getNuclideFunc) as rw: rw.readWrite() return lib -class _PmatrxIO(cccc.Stream): +class PmatrxIO(cccc.Stream): def __init__(self, fileName, xsLib, fileMode, getNuclideFunc): cccc.Stream.__init__(self, fileName, fileMode) self._lib = xsLib @@ -184,6 +184,12 @@ def _rwMessage(self): ) def readWrite(self): + """Read and write PMATRX files. + + .. impl:: Tool to read and write PMATRX files. + :id: I_ARMI_NUCDATA_PMATRX + :implements: R_ARMI_NUCDATA_PMATRX + """ self._rwMessage() properties.unlockImmutableProperties(self._lib) try: diff --git a/armi/nuclearDataIO/cccc/tests/test_dif3d.py b/armi/nuclearDataIO/cccc/tests/test_dif3d.py index 5720c9a1e..11cb86063 100644 --- a/armi/nuclearDataIO/cccc/tests/test_dif3d.py +++ b/armi/nuclearDataIO/cccc/tests/test_dif3d.py @@ -13,7 +13,6 @@ # limitations under the License. """Test reading/writing of DIF3D binary input.""" - import os import unittest @@ -37,14 +36,24 @@ def setUpClass(cls): cls.df = dif3d.Dif3dStream.readBinary(SIMPLE_HEXZ_DIF3D) def test__rwFileID(self): - """Verify the file identification info.""" + """Verify the file identification info. + + .. test:: Test reading DIF3D files. + :id: T_ARMI_NUCDATA_DIF3D0 + :tests: R_ARMI_NUCDATA_DIF3D + """ self.assertEqual(self.df.metadata["HNAME"], "DIF3D") self.assertEqual(self.df.metadata["HUSE1"], "") self.assertEqual(self.df.metadata["HUSE2"], "") self.assertEqual(self.df.metadata["VERSION"], 1) def test__rwFile1DRecord(self): - """Verify the rest of the metadata.""" + """Verify the rest of the metadata. + + .. test:: Test reading DIF3D files. + :id: T_ARMI_NUCDATA_DIF3D1 + :tests: R_ARMI_NUCDATA_DIF3D + """ TITLE_A6 = ["3D Hex", "-Z to", "genera", "te NHF", "LUX fi", "le"] EXPECTED_TITLE = TITLE_A6 + [""] * 5 for i in range(dif3d.TITLE_RANGE): @@ -133,7 +142,12 @@ def test__rw5DRecord(self): self.assertEqual(self.df.fiveD, None) def test_writeBinary(self): - """Verify binary equivalence of written DIF3D file.""" + """Verify binary equivalence of written DIF3D file. + + .. test:: Test writing DIF3D files. + :id: T_ARMI_NUCDATA_DIF3D2 + :tests: R_ARMI_NUCDATA_DIF3D + """ with TemporaryDirectoryChanger(): dif3d.Dif3dStream.writeBinary(self.df, "DIF3D2") with open(SIMPLE_HEXZ_DIF3D, "rb") as f1, open("DIF3D2", "rb") as f2: diff --git a/armi/nuclearDataIO/cccc/tests/test_dlayxs.py b/armi/nuclearDataIO/cccc/tests/test_dlayxs.py index 50411e6f9..929b7450b 100644 --- a/armi/nuclearDataIO/cccc/tests/test_dlayxs.py +++ b/armi/nuclearDataIO/cccc/tests/test_dlayxs.py @@ -32,7 +32,12 @@ def setUpClass(cls): cls.dlayxs3 = dlayxs.readBinary(test_xsLibraries.DLAYXS_MCC3) def test_decayConstants(self): - """Test that all emission spectrum delayEmissionSpectrum is normalized.""" + """Test that all emission spectrum delayEmissionSpectrum is normalized. + + .. test:: Test reading DLAYXS files. + :id: T_ARMI_NUCDATA_DLAYXS0 + :tests: R_ARMI_NUCDATA_DLAYXS + """ delay = self.dlayxs3 self.assertTrue( numpy.allclose( @@ -933,7 +938,7 @@ def _assertDC(self, nucName, endfProvidedData): "All the delayNeutronsPerFission data from mcc-v3 does not agree, this may be because they are from ENDV/B VI.8." ) def test_ENDFVII1NeutronsPerFission(self): - r""" + """ Build delayed nu based on ENDF/B-VII data. Notes @@ -1071,6 +1076,12 @@ def test_compare(self): self.assertTrue(dlayxs.compare(self.dlayxs3, copy.deepcopy(self.dlayxs3))) def test_writeBinary_mcc3(self): + """Verify binary equivalence of written DLAYXS file. + + .. test:: Test writing DLAYXS files. + :id: T_ARMI_NUCDATA_DLAYXS1 + :tests: R_ARMI_NUCDATA_DLAYXS + """ with TemporaryDirectoryChanger(): dlayxs.writeBinary(self.dlayxs3, "test_writeBinary_mcc3.temp") self.assertTrue( @@ -1112,8 +1123,3 @@ def test_avg(self): self.assertTrue( numpy.allclose(avg.delayEmissionSpectrum, dlayU235.delayEmissionSpectrum) ) - - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'DlayxsTests.test_writeBinary_mcc3'] - unittest.main(verbosity=2) diff --git a/armi/nuclearDataIO/cccc/tests/test_gamiso.py b/armi/nuclearDataIO/cccc/tests/test_gamiso.py index e40ca6316..6fd45bfa3 100644 --- a/armi/nuclearDataIO/cccc/tests/test_gamiso.py +++ b/armi/nuclearDataIO/cccc/tests/test_gamiso.py @@ -20,6 +20,7 @@ from armi.nuclearDataIO import xsLibraries from armi.nuclearDataIO.cccc import gamiso from armi.nuclearDataIO.xsNuclides import XSNuclide +from armi.utils.directoryChangers import TemporaryDirectoryChanger THIS_DIR = os.path.dirname(__file__) FIXTURE_DIR = os.path.join(THIS_DIR, "..", "..", "tests", "fixtures") @@ -31,10 +32,28 @@ def setUp(self): self.xsLib = xsLibraries.IsotxsLibrary() def test_compare(self): + """Compare the input binary GAMISO file. + + .. test:: Test reading GAMISO files. + :id: T_ARMI_NUCDATA_GAMISO0 + :tests: R_ARMI_NUCDATA_GAMISO + """ gamisoAA = gamiso.readBinary(GAMISO_AA) self.xsLib.merge(deepcopy(gamisoAA)) self.assertTrue(gamiso.compare(self.xsLib, gamisoAA)) + def test_writeBinary(self): + """Write a binary GAMISO file. + + .. test:: Test writing GAMISO files. + :id: T_ARMI_NUCDATA_GAMISO1 + :tests: R_ARMI_NUCDATA_GAMISO + """ + with TemporaryDirectoryChanger(): + data = gamiso.readBinary(GAMISO_AA) + binData = gamiso.writeBinary(data, "gamiso.out") + self.assertTrue(gamiso.compare(data, binData)) + def test_addDummyNuclidesToLibrary(self): dummyNuclides = [XSNuclide(None, "U238AA")] before = self.xsLib.getNuclides("") diff --git a/armi/nuclearDataIO/cccc/tests/test_geodst.py b/armi/nuclearDataIO/cccc/tests/test_geodst.py index 2d4cd516e..12d51fa1c 100644 --- a/armi/nuclearDataIO/cccc/tests/test_geodst.py +++ b/armi/nuclearDataIO/cccc/tests/test_geodst.py @@ -33,7 +33,12 @@ class TestGeodst(unittest.TestCase): """ def test_readGeodst(self): - """Ensure we can read a GEODST file.""" + """Ensure we can read a GEODST file. + + .. test:: Test reading GEODST files. + :id: T_ARMI_NUCDATA_GEODST0 + :tests: R_ARMI_NUCDATA_GEODST + """ geo = geodst.readBinary(SIMPLE_GEODST) self.assertEqual(geo.metadata["IGOM"], 18) self.assertAlmostEqual(geo.xmesh[1], 16.79, places=5) # hex pitch @@ -43,7 +48,12 @@ def test_readGeodst(self): self.assertEqual(geo.coarseMeshRegions.max(), geo.metadata["NREG"]) def test_writeGeodst(self): - """Ensure that we can write a modified GEODST.""" + """Ensure that we can write a modified GEODST. + + .. test:: Test writing GEODST files. + :id: T_ARMI_NUCDATA_GEODST1 + :tests: R_ARMI_NUCDATA_GEODST + """ with TemporaryDirectoryChanger(): geo = geodst.readBinary(SIMPLE_GEODST) geo.zmesh[-1] *= 2 diff --git a/armi/operators/operator.py b/armi/operators/operator.py index 9984c7e06..5e2adb6b4 100644 --- a/armi/operators/operator.py +++ b/armi/operators/operator.py @@ -73,9 +73,13 @@ class Operator: .. note:: The :doc:`/developer/guide` has some additional narrative on this topic. - .. impl:: The operator package shall expose an ordered list of interfaces, and loop over them in order. - :id: I_ARMI_OPERATOR_INTERFACES1 - :implements: R_ARMI_OPERATOR_INTERFACES + .. impl:: An operator will have a reactor object, to communicate between plugins. + :id: I_ARMI_OPERATOR_COMM + :implements: R_ARMI_OPERATOR_COMM + + .. impl:: An operator is built from user settings. + :id: I_ARMI_OPERATOR_SETTINGS + :implements: R_ARMI_OPERATOR_SETTINGS Attributes ---------- @@ -169,6 +173,13 @@ def maxBurnSteps(self): @property def stepLengths(self): + """ + Calculate step lengths. + + .. impl:: Calculate step lengths from cycles and burn steps. + :id: I_ARMI_FW_HISTORY + :implements: R_ARMI_FW_HISTORY + """ if not self._stepLengths: self._stepLengths = getStepLengths(self.cs) if self._stepLengths == [] and self.cs["nCycles"] == 1: @@ -391,6 +402,10 @@ def _timeNodeLoop(self, cycle, timeNode): def _performTightCoupling(self, cycle: int, timeNode: int, writeDB: bool = True): """If requested, perform tight coupling and write out database. + .. impl:: The operator shall allow tight coupling between physics systems. + :id: I_ARMI_OPERATOR_PHYSICS + :implements: R_ARMI_OPERATOR_PHYSICS + Notes ----- writeDB is False for OperatorSnapshots as the DB gets written at EOL. @@ -931,6 +946,10 @@ def getInterfaces(self): """ Get list of interfaces in interface stack. + .. impl:: An operator will expose an ordered list of interfaces. + :id: I_ARMI_OPERATOR_INTERFACES + :implements: R_ARMI_OPERATOR_INTERFACES + Notes ----- Returns a copy so you can manipulate the list in an interface, like dependencies. diff --git a/armi/operators/operatorMPI.py b/armi/operators/operatorMPI.py index 6cf7d64a1..eb3b62e37 100644 --- a/armi/operators/operatorMPI.py +++ b/armi/operators/operatorMPI.py @@ -46,7 +46,13 @@ class OperatorMPI(Operator): - """MPI-aware Operator.""" + """ + MPI-aware Operator. + + .. impl:: There is an MPI-aware operator. + :id: I_ARMI_OPERATOR_MPI + :implements: R_ARMI_OPERATOR_MPI + """ def __init__(self, cs): try: diff --git a/armi/plugins.py b/armi/plugins.py index 46064089d..0b858f067 100644 --- a/armi/plugins.py +++ b/armi/plugins.py @@ -89,7 +89,6 @@ deliberate design choice to keep the plugin system simple and to preclude a large class of potential bugs. At some point it may make sense to revisit this. - Other customization points -------------------------- While the Plugin API is the main place for ARMI framework customization, there are @@ -141,6 +140,10 @@ class ArmiPlugin: """ An ArmiPlugin provides a namespace to collect hook implementations provided by a single "plugin". This API is incomplete, unstable, and expected to change. + + .. impl:: Plugins have interfaces, to add code to the application. + :id: I_ARMI_PLUGIN + :implements: R_ARMI_PLUGIN """ @staticmethod @@ -149,6 +152,10 @@ def exposeInterfaces(cs) -> List: """ Function for exposing interface(s) to other code. + .. impl:: Plugins have interfaces to the operator. + :id: I_ARMI_PLUGIN_INTERFACES + :implements: R_ARMI_PLUGIN_INTERFACES + Returns ------- list @@ -173,6 +180,10 @@ def defineParameters() -> Dict: :id: I_ARMI_PLUGIN_PARAMS :implements: R_ARMI_PLUGIN_PARAMS + .. impl:: Define an arbitrary physical parameter. + :id: I_ARMI_PARAM + :implements: R_ARMI_PARAM + Returns ------- dict diff --git a/armi/reactor/converters/geometryConverters.py b/armi/reactor/converters/geometryConverters.py index 7ddfdd727..0f1d5ef4e 100644 --- a/armi/reactor/converters/geometryConverters.py +++ b/armi/reactor/converters/geometryConverters.py @@ -1365,7 +1365,11 @@ def addEdgeAssemblies(self, core): """ Add the assemblies on the 120 degree symmetric line to 1/3 symmetric cases. - Needs to be called before a finite difference (DIF3D, DIFNT) or MCNP calculation + Needs to be called before a finite difference (DIF3D, DIFNT) or MCNP calculation. + + .. impl:: Add assemblies along the 120-degree line to a reactor. + :id: I_ARMI_ADD_EDGE_ASSEMS0 + :implements: R_ARMI_ADD_EDGE_ASSEMS Parameters ---------- @@ -1433,6 +1437,10 @@ def removeEdgeAssemblies(self, core): This makes use of the assemblies knowledge of if it is in a region that it needs to be removed. + .. impl:: Remove assemblies along the 120-degree line from a reactor. + :id: I_ARMI_ADD_EDGE_ASSEMS1 + :implements: R_ARMI_ADD_EDGE_ASSEMS + See Also -------- addEdgeAssemblies : adds the edge assemblies diff --git a/armi/reactor/converters/tests/test_geometryConverters.py b/armi/reactor/converters/tests/test_geometryConverters.py index 68559983f..a3021ee15 100644 --- a/armi/reactor/converters/tests/test_geometryConverters.py +++ b/armi/reactor/converters/tests/test_geometryConverters.py @@ -266,7 +266,7 @@ def test_createHomogenizedRZTBlock(self): class TestEdgeAssemblyChanger(unittest.TestCase): def setUp(self): - r"""Use the related setup in the testFuelHandlers module.""" + """Use the related setup in the testFuelHandlers module.""" self.o, self.r = loadTestReactor(TEST_ROOT) reduceTestReactorRings(self.r, self.o.cs, 3) @@ -275,7 +275,12 @@ def tearDown(self): del self.r def test_edgeAssemblies(self): - r"""Sanity check on adding edge assemblies.""" + """Sanity check on adding edge assemblies. + + .. test:: Test adding/removing assemblies from a reactor. + :id: T_ARMI_ADD_EDGE_ASSEMS + :tests: R_ARMI_ADD_EDGE_ASSEMS + """ converter = geometryConverters.EdgeAssemblyChanger() converter.addEdgeAssemblies(self.r.core) diff --git a/armi/reactor/parameters/parameterDefinitions.py b/armi/reactor/parameters/parameterDefinitions.py index e4049f978..f3305d41c 100644 --- a/armi/reactor/parameters/parameterDefinitions.py +++ b/armi/reactor/parameters/parameterDefinitions.py @@ -340,6 +340,10 @@ def __get__(self, obj, cls=None): def setter(self, setter): """Decorator method for assigning setter. + .. impl:: Provide a way to signal if a parameter needs updating across processes. + :id: I_ARMI_PARAM_PARALLEL + :implements: R_ARMI_PARAM_PARALLEL + Notes ----- Unlike the traditional Python ``property`` class, this does not return a new diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index 084d0a2e9..e24b017df 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -70,6 +70,14 @@ class Reactor(composites.Composite): model. Historically, the `Reactor` contained only the core. To support better representation of ex-core structures, the old `Reactor` functionality was moved to the newer `Core` class, which has a `Reactor` parent. + + .. impl:: The user-specified reactor. + :id: I_ARMI_R + :implements: R_ARMI_R + + .. impl:: The reactor includes a core and a spent fuel pool. + :id: I_ARMI_R_CHILDREN + :implements: R_ARMI_R_CHILDREN """ pDefs = reactorParameters.defineReactorParameters() @@ -336,12 +344,24 @@ def r(self): @property def symmetry(self) -> geometry.SymmetryType: + """Getter for symmetry type. + + .. impl:: Get core symmetry. + :id: I_ARMI_R_SYMM0 + :implements: R_ARMI_R_SYMM + """ if not self.spatialGrid: raise ValueError("Cannot access symmetry before a spatialGrid is attached.") return self.spatialGrid.symmetry @symmetry.setter def symmetry(self, val: str): + """Setter for symmetry type. + + .. impl:: Set core symmetry. + :id: I_ARMI_R_SYMM1 + :implements: R_ARMI_R_SYMM + """ self.spatialGrid.symmetry = str(val) self.clearCache() @@ -1918,6 +1938,10 @@ def findAllMeshPoints(self, assems=None, applySubMesh=True): """ Return all mesh positions in core including both endpoints. + .. impl:: Construct a mesh based on core blocks. + :id: I_ARMI_R_MESH + :implements: R_ARMI_R_MESH + Parameters ---------- assems : list, optional @@ -1925,7 +1949,6 @@ def findAllMeshPoints(self, assems=None, applySubMesh=True): applySubMesh : bool, optional Apply submeshing parameters to make mesh points smaller than blocks. Default=True. - Returns ------- meshVals : tuple diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 51ad62380..e933b05b5 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -32,8 +32,9 @@ from armi.reactor import geometry from armi.reactor import grids from armi.reactor import reactors -from armi.reactor.composites import Composite +from armi.reactor.assemblyLists import SpentFuelPool from armi.reactor.components import Hexagon, Rectangle +from armi.reactor.composites import Composite from armi.reactor.converters import geometryConverters from armi.reactor.converters.axialExpansionChanger import AxialExpansionChanger from armi.reactor.flags import Flags @@ -237,6 +238,24 @@ def setUp(self): self.directoryChanger.destination, customSettings={"trackAssems": True} ) + def test_coreSfp(self): + """The reactor object includes a core and an SFP. + + .. test:: The reactor object is a composite. + :id: T_ARMI_R + :tests: R_ARMI_R + + .. test:: The reactor object includes a core and an SFP. + :id: T_ARMI_R_CHILDREN + :tests: R_ARMI_R_CHILDREN + """ + self.assertTrue(isinstance(self.r.core, reactors.Core)) + self.assertTrue(isinstance(self.r.sfp, SpentFuelPool)) + + self.assertTrue(isinstance(self.r, Composite)) + self.assertTrue(isinstance(self.r.core, Composite)) + self.assertTrue(isinstance(self.r.sfp, Composite)) + def test_factorySortSetting(self): """ Create a core object from an input yaml. @@ -756,6 +775,13 @@ def test_getDominantMaterial(self): self.assertEqual(list(dominantCool.getNuclides()), ["NA23"]) def test_getSymmetryFactor(self): + """ + Test getSymmetryFactor(). + + .. test:: Get the core symmetry. + :id: T_ARMI_R_SYMM + :tests: R_ARMI_R_SYMM + """ for b in self.r.core.getBlocks(): sym = b.getSymmetryFactor() i, j, _ = b.spatialLocator.getCompleteIndices() diff --git a/armi/reactor/tests/test_rz_reactors.py b/armi/reactor/tests/test_rz_reactors.py index 15d02c527..261817aac 100644 --- a/armi/reactor/tests/test_rz_reactors.py +++ b/armi/reactor/tests/test_rz_reactors.py @@ -38,6 +38,13 @@ def test_loadRZT(self): self.assertTrue(all(aziMesh == 8 for aziMesh in aziMeshes)) def test_findAllMeshPoints(self): + """ + Test findAllMeshPoints(). + + .. test:: Test that the reactor can calculate its core block mesh. + :id: T_ARMI_R_MESH + :tests: R_ARMI_R_MESH + """ i, _, _ = self.r.core.findAllMeshPoints() self.assertLess(i[-1], 2 * math.pi) From bb78218c195ca1029c161278dae1a3a8ffb87995 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 21 Nov 2023 10:29:04 -0800 Subject: [PATCH 050/176] Adding impl/test crumbs for nucDir (#1482) --- armi/nucDirectory/elements.py | 8 +++ armi/nucDirectory/nuclideBases.py | 70 +++++++++++++++----- armi/nucDirectory/tests/test_elements.py | 28 ++++++++ armi/nucDirectory/tests/test_nuclideBases.py | 49 +++++++++++++- 4 files changed, 136 insertions(+), 19 deletions(-) diff --git a/armi/nucDirectory/elements.py b/armi/nucDirectory/elements.py index a69975914..0eee7642c 100644 --- a/armi/nucDirectory/elements.py +++ b/armi/nucDirectory/elements.py @@ -16,6 +16,10 @@ This module provides fundamental element information to be used throughout the framework and applications. +.. impl:: A tool for querying basic data for elements of the periodic table. + :id: I_ARMI_ND_ELEMENTS0 + :implements: R_ARMI_ND_ELEMENTS + The element class structure is outlined :ref:`here `. .. _elements-class-diagram: @@ -149,6 +153,10 @@ def __init__(self, z, symbol, name, phase="UNKNOWN", group="UNKNOWN"): """ Creates an instance of an Element. + .. impl:: An element of the periodic table. + :id: I_ARMI_ND_ELEMENTS1 + :implements: R_ARMI_ND_ELEMENTS + Parameters ---------- z : int diff --git a/armi/nucDirectory/nuclideBases.py b/armi/nucDirectory/nuclideBases.py index 6a2143ba5..5f83b7ed6 100644 --- a/armi/nucDirectory/nuclideBases.py +++ b/armi/nucDirectory/nuclideBases.py @@ -15,6 +15,10 @@ This module provides fundamental nuclide information to be used throughout the framework and applications. +.. impl:: Isotopes and isomers can be queried by name, label, MC2-3 ID, MCNP ID, and AAAZZZS ID. + :id: I_ARMI_ND_ISOTOPES0 + :implements: R_ARMI_ND_ISOTOPES + The nuclide class structure is outlined :ref:`here `. .. _nuclide-bases-class-diagram: @@ -423,7 +427,7 @@ def _processBurnData(self, burnInfo): ) def getDecay(self, decayType): - r"""Get a :py:class:`~armi.nucDirectory.transmutations.DecayMode`. + """Get a :py:class:`~armi.nucDirectory.transmutations.DecayMode`. Retrieve the first :py:class:`~armi.nucDirectory.transmutations.DecayMode` matching the specified decType. @@ -493,7 +497,12 @@ def getAAAZZZSId(self): class NuclideBase(INuclide, IMcnpNuclide): - """Represents an individual nuclide/isotope.""" + """Represents an individual nuclide/isotope. + + .. impl:: Isotopes and isomers can be queried by name and label. + :id: I_ARMI_ND_ISOTOPES1 + :implements: R_ARMI_ND_ISOTOPES + """ def __init__(self, element, a, weight, abundance, state, halflife): IMcnpNuclide.__init__(self) @@ -558,22 +567,35 @@ def getNaturalIsotopics(self): return self.element.getNaturalIsotopics() def getMcc2Id(self): - """Return the MC2-2 nuclide identification label based on the ENDF/B-V.2 cross section library.""" + """Return the MC2-2 nuclide identification label based on the ENDF/B-V.2 cross section library. + + .. impl:: Isotopes and isomers can be queried by MC2-2 ID. + :id: I_ARMI_ND_ISOTOPES2 + :implements: R_ARMI_ND_ISOTOPES + """ return self.mcc2id def getMcc3Id(self): - """Return the MC2-3 nuclide identification label based on the ENDF/B-VII.0 cross section library.""" + """Return the MC2-3 nuclide identification label based on the ENDF/B-VII.0 cross section library. + + .. impl:: Isotopes and isomers can be queried by MC2-3 ID. + :id: I_ARMI_ND_ISOTOPES3 + :implements: R_ARMI_ND_ISOTOPES + """ return self.mcc3id def getMcnpId(self): """ Gets the MCNP label for this nuclide. + .. impl:: Isotopes and isomers can be queried by MCNP ID. + :id: I_ARMI_ND_ISOTOPES4 + :implements: R_ARMI_ND_ISOTOPES + Returns ------- id : str The MCNP ID e.g. ``92235``, ``94239``, ``6000`` - """ z, a = self.z, self.a @@ -595,6 +617,10 @@ def getAAAZZZSId(self): """ Return a string that is ordered by the mass number, A, the atomic number, Z, and the isomeric state, S. + .. impl:: Isotopes and isomers can be queried by AAAZZZS ID. + :id: I_ARMI_ND_ISOTOPES5 + :implements: R_ARMI_ND_ISOTOPES + Notes ----- An example would be for U235, where A=235, Z=92, and S=0, returning ``2350920``. @@ -629,7 +655,6 @@ def getEndfMatNum(self): ------- id : str The MAT number e.g. ``9237`` for U238 - """ z, a = self.z, self.a if self.element.symbol in BASE_ENDFB7_MAT_NUM: @@ -679,7 +704,7 @@ def __repr__(self): return f"<{self.__class__.__name__} {self.name}: Z:{self.z}, W:{self.weight:<12.6e}, Label:{self.label}>" def getNaturalIsotopics(self): - r"""Gets the natural isotopics root :py:class:`~elements.Element`. + """Gets the natural isotopics root :py:class:`~elements.Element`. Gets the naturally occurring nuclides for this nuclide. @@ -789,7 +814,7 @@ def __lt__(self, other): ) def getNaturalIsotopics(self): - r"""Gets the natural isotopics, an empty iterator. + """Gets the natural isotopics, an empty iterator. Gets the naturally occurring nuclides for this nuclide. @@ -855,7 +880,7 @@ def __lt__(self, other): ) def getNaturalIsotopics(self): - r"""Gets the natural isotopics, an empty iterator. + """Gets the natural isotopics, an empty iterator. Gets the naturally occurring nuclides for this nuclide. @@ -1115,12 +1140,12 @@ def factory(): "Nuclides are already initialized and cannot be re-initialized unless " "`nuclideBases.destroyGlobalNuclides` is called first." ) - __addNuclideBases() + addNuclideBases() __addNaturalNuclideBases() __addDummyNuclideBases() __addLumpedFissionProductNuclideBases() - __updateNuclideBasesForSpecialCases() - __readMCCNuclideData() + updateNuclideBasesForSpecialCases() + readMCCNuclideData() __renormalizeNuclideToElementRelationship() __deriveElementalWeightsByNaturalNuclideAbundances() @@ -1130,11 +1155,15 @@ def factory(): thermalScattering.factory() -def __addNuclideBases(): +def addNuclideBases(): """ Read natural abundances of any natural nuclides. This adjusts already-existing NuclideBases and Elements with the new information. + + .. impl:: Separating natural abundance data from code. + :id: I_ARMI_ND_DATA0 + :implements: R_ARMI_ND_DATA """ with open(os.path.join(context.RES, "nuclides.dat")) as f: for line in f: @@ -1190,8 +1219,13 @@ def __addLumpedFissionProductNuclideBases(): LumpNuclideBase(name="LREGN", weight=1.0) -def __readMCCNuclideData(): - """Read in the label data for the MC2-2 and MC2-3 cross section codes to the nuclide bases.""" +def readMCCNuclideData(): + """Read in the label data for the MC2-2 and MC2-3 cross section codes to the nuclide bases. + + .. impl:: Separating MCC data from code. + :id: I_ARMI_ND_DATA1 + :implements: R_ARMI_ND_DATA + """ with open(os.path.join(context.RES, "mcc-nuclides.yaml"), "r") as f: yaml = YAML(typ="rt") nuclides = yaml.load(f) @@ -1208,10 +1242,14 @@ def __readMCCNuclideData(): byMcc3Id[nb.getMcc3Id()] = nb -def __updateNuclideBasesForSpecialCases(): +def updateNuclideBasesForSpecialCases(): """ Update the nuclide bases for special case name changes. + .. impl:: The special case name Am242g is supported. + :id: I_ARMI_ND_ISOTOPES6 + :implements: R_ARMI_ND_ISOTOPES + Notes ----- This function is specifically added to change the definition of diff --git a/armi/nucDirectory/tests/test_elements.py b/armi/nucDirectory/tests/test_elements.py index 08199fc70..26e3156bd 100644 --- a/armi/nucDirectory/tests/test_elements.py +++ b/armi/nucDirectory/tests/test_elements.py @@ -35,14 +35,32 @@ def test_elements_elementBulkProperties(self): self.assertIsNotNone(ee.standardWeight) def test_element_elementByNameReturnsElement(self): + """Get elements by name. + + .. test:: Get elements by name. + :id: I_ARMI_ND_ELEMENTS0 + :tests: R_ARMI_ND_ELEMENTS + """ for ee in elements.byZ.values(): self.assertIs(ee, elements.byName[ee.name]) def test_element_elementByZReturnsElement(self): + """Get elements by Z. + + .. test:: Get elements by Z. + :id: I_ARMI_ND_ELEMENTS1 + :tests: R_ARMI_ND_ELEMENTS + """ for ee in elements.byZ.values(): self.assertIs(ee, elements.byZ[ee.z]) def test_element_elementBySymbolReturnsElement(self): + """Get elements by symbol. + + .. test:: Get elements by symbol. + :id: I_ARMI_ND_ELEMENTS2 + :tests: R_ARMI_ND_ELEMENTS + """ for ee in elements.byZ.values(): self.assertIs(ee, elements.bySymbol[ee.symbol]) @@ -84,6 +102,10 @@ def test_element_isNaturallyOccurring(self): Uses RIPL definitions of naturally occurring. Protactinium is debated as naturally occurring. Yeah it exists as a U235 decay product but it's kind of pseudo-natural. + + .. test:: Get elements by Z, to show if they are naturally occurring. + :id: I_ARMI_ND_ELEMENTS3 + :tests: R_ARMI_ND_ELEMENTS """ for ee in elements.byZ.values(): if ee.z == 43 or ee.z == 61 or 84 <= ee.z <= 89 or ee.z >= 93: @@ -104,6 +126,12 @@ def test_abundancesAddToOne(self): ) def test_isHeavyMetal(self): + """Get elements by Z. + + .. test:: Get elements by Z, to show if they are heavy metals. + :id: I_ARMI_ND_ELEMENTS4 + :tests: R_ARMI_ND_ELEMENTS + """ for ee in elements.byZ.values(): if ee.z > 89: self.assertTrue(ee.isHeavyMetal()) diff --git a/armi/nucDirectory/tests/test_nuclideBases.py b/armi/nucDirectory/tests/test_nuclideBases.py index d8403276c..904c033d7 100644 --- a/armi/nucDirectory/tests/test_nuclideBases.py +++ b/armi/nucDirectory/tests/test_nuclideBases.py @@ -137,6 +137,12 @@ def test_NaturalNuclide_atomicWeightIsAverageOfNaturallyOccuringIsotopes(self): ) def test_nucBases_labelAndNameCollsionsAreForSameNuclide(self): + """The name and labels for correct for nuclides. + + .. test:: Validate the name, label, and DB name are accessible for nuclides. + :id: I_ARMI_ND_ISOTOPES0 + :tests: R_ARMI_ND_ISOTOPES + """ count = 0 for nuc in nuclideBases.where(lambda nn: nn.name == nn.label): count += 1 @@ -185,6 +191,12 @@ def test_nucBases_imposeBurnChainTransmutationBulkStatistics(self): ) # ternary fission def test_nucBases_imposeBurn_nuSF(self): + """Test the nuclide data from file (specifically neutrons / sponaneous fission). + + .. test:: Test that nuclide data was read from file instead of code. + :id: I_ARMI_ND_DATA0 + :tests: R_ARMI_ND_DATA + """ actual = { nn.name: nn.nuSF for nn in nuclideBases.where(lambda nn: nn.nuSF > 0.0) } @@ -235,6 +247,12 @@ def test_nucBases_AllDatabaseNamesAreUnique(self): ) def test_nucBases_Am242m(self): + """Test the correct am242g and am242m abbreviations are supported. + + .. test:: Specifically test for Am242 and Am242g because it is a special case. + :id: I_ARMI_ND_ISOTOPES1 + :tests: R_ARMI_ND_ISOTOPES + """ am242m = nuclideBases.byName["AM242"] self.assertEqual(am242m, nuclideBases.byName["AM242M"]) self.assertEqual("nAm242m", am242m.getDatabaseName()) @@ -263,6 +281,12 @@ def test_getDecay(self): self.assertIsNone(nb.getDecay("sf")) def test_getEndfMatNum(self): + """Test get nuclides by name. + + .. test:: Test get nuclides by name. + :id: I_ARMI_ND_ISOTOPES2 + :tests: R_ARMI_ND_ISOTOPES + """ self.assertEqual(nuclideBases.byName["U235"].getEndfMatNum(), "9228") self.assertEqual(nuclideBases.byName["U238"].getEndfMatNum(), "9237") self.assertEqual(nuclideBases.byName["PU239"].getEndfMatNum(), "9437") @@ -365,7 +389,12 @@ def test_curieDefinitionWithRa226(self): self.assertAlmostEqual(activity, 0.9885593, places=6) def test_loadMcc2Data(self): - """Tests consistency with the `mcc-nuclides.yaml` input and the nuclides in the data model.""" + """Tests consistency with the `mcc-nuclides.yaml` input and the nuclides in the data model. + + .. test:: Test that MCC v2 IDs can be queried by nuclides. + :id: I_ARMI_ND_ISOTOPES3 + :tests: R_ARMI_ND_ISOTOPES + """ with open(os.path.join(RES, "mcc-nuclides.yaml")) as f: yaml = YAML(typ="rt") data = yaml.load(f) @@ -381,7 +410,16 @@ def test_loadMcc2Data(self): self.assertEqual(len(nuclideBases.byMcc2Id), len(expectedNuclides)) def test_loadMcc3Data(self): - """Tests consistency with the `mcc-nuclides.yaml` input and the nuclides in the data model.""" + """Tests consistency with the `mcc-nuclides.yaml` input and the nuclides in the data model. + + .. test:: Test that MCC v3 IDs can be queried by nuclides. + :id: I_ARMI_ND_ISOTOPES4 + :tests: R_ARMI_ND_ISOTOPES + + .. test:: Test the MCC nuclide data that was read from file instead of code. + :id: I_ARMI_ND_DATA1 + :tests: R_ARMI_ND_DATA + """ with open(os.path.join(RES, "mcc-nuclides.yaml")) as f: yaml = YAML(typ="rt") data = yaml.load(f) @@ -398,9 +436,14 @@ def test_loadMcc3Data(self): self.assertEqual(len(nuclideBases.byMcc3Id), len(expectedNuclides) - 1) -class test_getAAAZZZSId(unittest.TestCase): +class TestAAAZZZSId(unittest.TestCase): def test_AAAZZZSNameGenerator(self): + """Test that AAAZZS ID name generator. + .. test:: Query the AAAZZS IDs can be retrieved for nuclides. + :id: I_ARMI_ND_ISOTOPES5 + :tests: R_ARMI_ND_ISOTOPES + """ referenceNucNames = [ ("C", "120060"), ("U235", "2350920"), From e2dba79a4e68c23a0dd06b90ea2e4cbaca335095 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 21 Nov 2023 11:17:37 -0800 Subject: [PATCH 051/176] Fixing broken req test crumbs (#1484) --- armi/nucDirectory/tests/test_elements.py | 10 +++++----- armi/nucDirectory/tests/test_nuclideBases.py | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/armi/nucDirectory/tests/test_elements.py b/armi/nucDirectory/tests/test_elements.py index 26e3156bd..9ad2a2dac 100644 --- a/armi/nucDirectory/tests/test_elements.py +++ b/armi/nucDirectory/tests/test_elements.py @@ -38,7 +38,7 @@ def test_element_elementByNameReturnsElement(self): """Get elements by name. .. test:: Get elements by name. - :id: I_ARMI_ND_ELEMENTS0 + :id: T_ARMI_ND_ELEMENTS0 :tests: R_ARMI_ND_ELEMENTS """ for ee in elements.byZ.values(): @@ -48,7 +48,7 @@ def test_element_elementByZReturnsElement(self): """Get elements by Z. .. test:: Get elements by Z. - :id: I_ARMI_ND_ELEMENTS1 + :id: T_ARMI_ND_ELEMENTS1 :tests: R_ARMI_ND_ELEMENTS """ for ee in elements.byZ.values(): @@ -58,7 +58,7 @@ def test_element_elementBySymbolReturnsElement(self): """Get elements by symbol. .. test:: Get elements by symbol. - :id: I_ARMI_ND_ELEMENTS2 + :id: T_ARMI_ND_ELEMENTS2 :tests: R_ARMI_ND_ELEMENTS """ for ee in elements.byZ.values(): @@ -104,7 +104,7 @@ def test_element_isNaturallyOccurring(self): occurring. Yeah it exists as a U235 decay product but it's kind of pseudo-natural. .. test:: Get elements by Z, to show if they are naturally occurring. - :id: I_ARMI_ND_ELEMENTS3 + :id: T_ARMI_ND_ELEMENTS3 :tests: R_ARMI_ND_ELEMENTS """ for ee in elements.byZ.values(): @@ -129,7 +129,7 @@ def test_isHeavyMetal(self): """Get elements by Z. .. test:: Get elements by Z, to show if they are heavy metals. - :id: I_ARMI_ND_ELEMENTS4 + :id: T_ARMI_ND_ELEMENTS4 :tests: R_ARMI_ND_ELEMENTS """ for ee in elements.byZ.values(): diff --git a/armi/nucDirectory/tests/test_nuclideBases.py b/armi/nucDirectory/tests/test_nuclideBases.py index 904c033d7..0d0773852 100644 --- a/armi/nucDirectory/tests/test_nuclideBases.py +++ b/armi/nucDirectory/tests/test_nuclideBases.py @@ -140,7 +140,7 @@ def test_nucBases_labelAndNameCollsionsAreForSameNuclide(self): """The name and labels for correct for nuclides. .. test:: Validate the name, label, and DB name are accessible for nuclides. - :id: I_ARMI_ND_ISOTOPES0 + :id: T_ARMI_ND_ISOTOPES0 :tests: R_ARMI_ND_ISOTOPES """ count = 0 @@ -194,7 +194,7 @@ def test_nucBases_imposeBurn_nuSF(self): """Test the nuclide data from file (specifically neutrons / sponaneous fission). .. test:: Test that nuclide data was read from file instead of code. - :id: I_ARMI_ND_DATA0 + :id: T_ARMI_ND_DATA0 :tests: R_ARMI_ND_DATA """ actual = { @@ -250,7 +250,7 @@ def test_nucBases_Am242m(self): """Test the correct am242g and am242m abbreviations are supported. .. test:: Specifically test for Am242 and Am242g because it is a special case. - :id: I_ARMI_ND_ISOTOPES1 + :id: T_ARMI_ND_ISOTOPES1 :tests: R_ARMI_ND_ISOTOPES """ am242m = nuclideBases.byName["AM242"] @@ -284,7 +284,7 @@ def test_getEndfMatNum(self): """Test get nuclides by name. .. test:: Test get nuclides by name. - :id: I_ARMI_ND_ISOTOPES2 + :id: T_ARMI_ND_ISOTOPES2 :tests: R_ARMI_ND_ISOTOPES """ self.assertEqual(nuclideBases.byName["U235"].getEndfMatNum(), "9228") @@ -392,7 +392,7 @@ def test_loadMcc2Data(self): """Tests consistency with the `mcc-nuclides.yaml` input and the nuclides in the data model. .. test:: Test that MCC v2 IDs can be queried by nuclides. - :id: I_ARMI_ND_ISOTOPES3 + :id: T_ARMI_ND_ISOTOPES3 :tests: R_ARMI_ND_ISOTOPES """ with open(os.path.join(RES, "mcc-nuclides.yaml")) as f: @@ -413,11 +413,11 @@ def test_loadMcc3Data(self): """Tests consistency with the `mcc-nuclides.yaml` input and the nuclides in the data model. .. test:: Test that MCC v3 IDs can be queried by nuclides. - :id: I_ARMI_ND_ISOTOPES4 + :id: T_ARMI_ND_ISOTOPES4 :tests: R_ARMI_ND_ISOTOPES .. test:: Test the MCC nuclide data that was read from file instead of code. - :id: I_ARMI_ND_DATA1 + :id: T_ARMI_ND_DATA1 :tests: R_ARMI_ND_DATA """ with open(os.path.join(RES, "mcc-nuclides.yaml")) as f: @@ -441,7 +441,7 @@ def test_AAAZZZSNameGenerator(self): """Test that AAAZZS ID name generator. .. test:: Query the AAAZZS IDs can be retrieved for nuclides. - :id: I_ARMI_ND_ISOTOPES5 + :id: T_ARMI_ND_ISOTOPES5 :tests: R_ARMI_ND_ISOTOPES """ referenceNucNames = [ From a44e1c83994e700039653c5b0ff521d156e3d86e Mon Sep 17 00:00:00 2001 From: bdlafleur Date: Tue, 21 Nov 2023 20:59:06 -0500 Subject: [PATCH 052/176] Adding impl/test crumbs from Nala req comments (#1483) --- armi/cases/suite.py | 4 + armi/cases/tests/test_cases.py | 9 +- armi/nuclearDataIO/cccc/gamiso.py | 4 +- armi/nuclearDataIO/cccc/isotxs.py | 2 +- armi/nuclearDataIO/cccc/pmatrx.py | 4 +- armi/nuclearDataIO/cccc/tests/test_pmatrx.py | 2 +- .../globalFlux/globalFluxInterface.py | 17 +- .../tests/test_globalFluxInterface.py | 18 +- armi/plugins.py | 2 +- armi/reactor/components/basicShapes.py | 32 ++- armi/reactor/components/complexShapes.py | 18 +- armi/reactor/components/component.py | 30 ++- armi/reactor/composites.py | 18 ++ armi/reactor/converters/blockConverters.py | 18 +- .../converters/tests/test_blockConverter.py | 28 ++- armi/reactor/grids/hexagonal.py | 13 +- armi/reactor/grids/tests/test_grids.py | 12 ++ armi/reactor/tests/test_assemblies.py | 2 +- armi/reactor/tests/test_components.py | 190 +++++++++++++++++- armi/reactor/tests/test_composites.py | 23 +++ 20 files changed, 413 insertions(+), 33 deletions(-) diff --git a/armi/cases/suite.py b/armi/cases/suite.py index f6aeba914..d5bda9875 100644 --- a/armi/cases/suite.py +++ b/armi/cases/suite.py @@ -49,6 +49,10 @@ class CaseSuite: subclass a CaseSuite to meet the needs of a specific calculation. A CaseSuite is a collection that is keyed off Case titles. + + .. impl:: CaseSuite allows for one case to start after another completes. + :id: I_ARMI_CASE_SUITE + :implements: R_ARMI_CASE_SUITE """ def __init__(self, cs): diff --git a/armi/cases/tests/test_cases.py b/armi/cases/tests/test_cases.py index 4b02b6090..0fea932ee 100644 --- a/armi/cases/tests/test_cases.py +++ b/armi/cases/tests/test_cases.py @@ -199,6 +199,13 @@ def test_endProfiling(self): self.assertTrue(isinstance(prof, cProfile.Profile)) def test_run(self): + """ + Test running a case. + + .. test:: Generic mechanism to allow simulation runs + :id: T_ARMI_CASE + :tests: R_ARMI_CASE + """ with directoryChangers.TemporaryDirectoryChanger(): cs = settings.Settings(ARMI_RUN_PATH) newSettings = { @@ -265,7 +272,7 @@ class TestCaseSuiteDependencies(unittest.TestCase): """CaseSuite tests. .. test:: Dependence allows for one case to start after the completion of another. - :id: T_ARMI_CASE_SUITE0 + :id: T_ARMI_CASE_SUITE :tests: R_ARMI_CASE_SUITE """ diff --git a/armi/nuclearDataIO/cccc/gamiso.py b/armi/nuclearDataIO/cccc/gamiso.py index ef07598d5..b53eb0827 100644 --- a/armi/nuclearDataIO/cccc/gamiso.py +++ b/armi/nuclearDataIO/cccc/gamiso.py @@ -22,9 +22,9 @@ :id: I_ARMI_NUCDATA_GAMISO :implements: R_ARMI_NUCDATA_GAMISO -See [GAMSOR]_. +See [GAMSOR]_. -.. [GAMSOR] Smith, M. A., Lee, C. H., and Hill, R. N. GAMSOR: Gamma Source Preparation and DIF3D Flux Solution. United States: +.. [GAMSOR] Smith, M. A., Lee, C. H., and Hill, R. N. GAMSOR: Gamma Source Preparation and DIF3D Flux Solution. United States: N. p., 2016. Web. doi:10.2172/1343095. `On OSTI `_ """ diff --git a/armi/nuclearDataIO/cccc/isotxs.py b/armi/nuclearDataIO/cccc/isotxs.py index c708b7244..3b3f42ee4 100644 --- a/armi/nuclearDataIO/cccc/isotxs.py +++ b/armi/nuclearDataIO/cccc/isotxs.py @@ -18,7 +18,7 @@ ISOTXS is a binary file that contains multigroup microscopic cross sections. ISOTXS stands for *Isotope Cross Sections*. -ISOTXS files are often created by a lattice physics code such as MC2 or DRAGON and +ISOTXS files are often created by a lattice physics code such as MC2 or DRAGON and used as input to a global flux solver such as DIF3D. This module implements reading and writing of the diff --git a/armi/nuclearDataIO/cccc/pmatrx.py b/armi/nuclearDataIO/cccc/pmatrx.py index 9c1610f35..83836ba98 100644 --- a/armi/nuclearDataIO/cccc/pmatrx.py +++ b/armi/nuclearDataIO/cccc/pmatrx.py @@ -17,8 +17,8 @@ See [GAMSOR]_ and [MC23]_. -.. [MC23] Lee, Changho, Jung, Yeon Sang, and Yang, Won Sik. MC2-3: Multigroup Cross Section Generation Code for Fast Reactor - Analysis Nuclear. United States: N. p., 2018. Web. doi:10.2172/1483949. +.. [MC23] Lee, Changho, Jung, Yeon Sang, and Yang, Won Sik. MC2-3: Multigroup Cross Section Generation Code for Fast Reactor + Analysis Nuclear. United States: N. p., 2018. Web. doi:10.2172/1483949. (`OSTI `_) """ diff --git a/armi/nuclearDataIO/cccc/tests/test_pmatrx.py b/armi/nuclearDataIO/cccc/tests/test_pmatrx.py index cf4180c22..4d8e90e3f 100644 --- a/armi/nuclearDataIO/cccc/tests/test_pmatrx.py +++ b/armi/nuclearDataIO/cccc/tests/test_pmatrx.py @@ -117,7 +117,7 @@ def test_pmatrxGammaEnergies(self): ] self.assertTrue((energies == self.lib.gammaEnergyUpperBounds).all()) - def test_pmatrxNeutronEneries(self): + def test_pmatrxNeutronEnergies(self): energies = [ 14190675.0, 10000000.0, diff --git a/armi/physics/neutronics/globalFlux/globalFluxInterface.py b/armi/physics/neutronics/globalFlux/globalFluxInterface.py index 3b998b8f6..752ffdfe7 100644 --- a/armi/physics/neutronics/globalFlux/globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/globalFluxInterface.py @@ -237,7 +237,12 @@ def interactCoupled(self, iteration): GlobalFluxInterface.interactCoupled(self, iteration) def getTightCouplingValue(self): - """Return the parameter value.""" + """Return the parameter value. + + .. impl:: Return k-eff or assembly-wise power distribution for coupled interactions. + :id: I_ARMI_FLUX_COUPLING_VALUE + :implements: R_ARMI_FLUX_COUPLING_VALUE + """ if self.coupler.parameter == "keff": return self.r.core.p.keff if self.coupler.parameter == "power": @@ -532,10 +537,6 @@ def _performGeometryTransformations(self, makePlots=False): In both cases, we need to undo the modifications between reading the output and applying the result to the data model. - .. impl:: Ensure the mesh in the reactor model is appropriate for neutronics solver execution. - :id: I_ARMI_FLUX_RX_RATES - :implements: R_ARMI_FLUX_RX_RATES - See Also -------- _undoGeometryTransformations @@ -1202,14 +1203,14 @@ def computeDpaRate(mgFlux, dpaXs): DPA rate = displacement density rate / (number of atoms/cc) = dr [#/cm^3/s] / (nHT9) [1/cm^3] - = flux * barn * 1e-24 + = flux * barn * 1e-24 .. math:: \frac{\text{dpa}}{s} = \frac{\phi N \sigma}{N} = \phi * \sigma - the Number density of the structural material cancels out. It's in the macroscopic + the Number density of the structural material cancels out. It's in the macroscopic XS and in the original number of atoms. Raises @@ -1252,7 +1253,7 @@ def calcReactionRates(obj, keff, lib): r""" Compute 1-group reaction rates for this object (usually a block). - .. impl:: Return the reaction rates for a given ArmiObject. + .. impl:: Return the reaction rates for a given ArmiObject :id: I_ARMI_FLUX_RX_RATES :implements: R_ARMI_FLUX_RX_RATES diff --git a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py index 5c5b4f131..1c910766a 100644 --- a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py @@ -167,6 +167,13 @@ def test_getHistoryParams(self): self.assertIn("detailedDpa", params) def test_checkEnergyBalance(self): + """ + Test energy balance check. + + .. test:: Block-wise power is consistent with reactor data model power + :id: T_ARMI_FLUX_CHECK_POWER + :tests: R_ARMI_FLUX_CHECK_POWER + """ cs = settings.Settings() _o, r = test_reactors.loadTestReactor() gfi = MockGlobalFluxInterface(r, cs) @@ -209,7 +216,12 @@ def test_setTightCouplingDefaults(self): self._setTightCouplingFalse() def test_getTightCouplingValue(self): - """Test getTightCouplingValue returns the correct value for keff and type for power.""" + """Test getTightCouplingValue returns the correct value for keff and type for power. + + .. test:: Get k-eff or assembly-wise power for coupling interactions + :id: T_ARMI_FLUX_COUPLING_VALUE + :tests: R_ARMI_FLUX_COUPLING_VALUE + """ self._setTightCouplingTrue() self.assertEqual(self.gfi.getTightCouplingValue(), 1.0) # set in setUp self.gfi.coupler.parameter = "power" @@ -355,6 +367,10 @@ def test_calcReactionRates(self): """ Test that the reaction rate code executes and sets a param > 0.0. + .. test:: Return the reaction rates for a given ArmiObject + :id: T_ARMI_FLUX_RX_RATES + :tests: R_ARMI_FLUX_RX_RATES + .. warning: This does not validate the reaction rate calculation. """ b = test_blocks.loadTestBlock() diff --git a/armi/plugins.py b/armi/plugins.py index 0b858f067..f8ed8bb69 100644 --- a/armi/plugins.py +++ b/armi/plugins.py @@ -141,7 +141,7 @@ class ArmiPlugin: An ArmiPlugin provides a namespace to collect hook implementations provided by a single "plugin". This API is incomplete, unstable, and expected to change. - .. impl:: Plugins have interfaces, to add code to the application. + .. impl:: Plugins have interfaces to add code to the application. :id: I_ARMI_PLUGIN :implements: R_ARMI_PLUGIN """ diff --git a/armi/reactor/components/basicShapes.py b/armi/reactor/components/basicShapes.py index c91ed9109..02b98c2c6 100644 --- a/armi/reactor/components/basicShapes.py +++ b/armi/reactor/components/basicShapes.py @@ -25,7 +25,12 @@ class Circle(ShapedComponent): - """A Circle.""" + """A Circle. + + .. impl:: Circle shaped component + :id: I_ARMI_COMP_SHAPES0 + :implements: R_ARMI_COMP_SHAPES + """ is3D = False @@ -84,7 +89,12 @@ def isEncapsulatedBy(self, other): class Hexagon(ShapedComponent): - """A Hexagon.""" + """A Hexagon. + + .. impl:: Hexagon shaped component + :id: I_ARMI_COMP_SHAPES1 + :implements: R_ARMI_COMP_SHAPES + """ is3D = False @@ -164,7 +174,12 @@ def getPitchData(self): class Rectangle(ShapedComponent): - """A rectangle component.""" + """A Rectangle. + + .. impl:: Rectangle shaped component + :id: I_ARMI_COMP_SHAPES2 + :implements: R_ARMI_COMP_SHAPES + """ is3D = False @@ -304,7 +319,12 @@ def getComponentArea(self, cold=False): class Square(Rectangle): - """Square component that can be solid or hollow.""" + """Square component that can be solid or hollow. + + .. impl:: Square shaped component + :id: I_ARMI_COMP_SHAPES3 + :implements: R_ARMI_COMP_SHAPES + """ is3D = False @@ -377,6 +397,10 @@ class Triangle(ShapedComponent): """ Triangle with defined base and height. + .. impl:: Triangle shaped component + :id: I_ARMI_COMP_SHAPES4 + :implements: R_ARMI_COMP_SHAPES + Notes ----- The exact angles of the triangle are undefined. The exact side lenths and angles diff --git a/armi/reactor/components/complexShapes.py b/armi/reactor/components/complexShapes.py index ac456e26f..5cd8c742b 100644 --- a/armi/reactor/components/complexShapes.py +++ b/armi/reactor/components/complexShapes.py @@ -22,7 +22,12 @@ class HoledHexagon(basicShapes.Hexagon): - """Hexagon with n uniform circular holes hollowed out of it.""" + """Hexagon with n uniform circular holes hollowed out of it. + + .. impl:: Holed hexagon shaped component + :id: I_ARMI_COMP_SHAPES5 + :implements: R_ARMI_COMP_SHAPES + """ THERMAL_EXPANSION_DIMS = {"op", "holeOD"} @@ -190,7 +195,12 @@ def getCircleInnerDiameter(self, Tc=None, cold=False): class HoledSquare(basicShapes.Square): - """Square with one circular hole in it.""" + """Square with one circular hole in it. + + .. impl:: Holed square shaped component + :id: I_ARMI_COMP_SHAPES6 + :implements: R_ARMI_COMP_SHAPES + """ THERMAL_EXPANSION_DIMS = {"widthOuter", "holeOD"} @@ -242,6 +252,10 @@ def getCircleInnerDiameter(self, Tc=None, cold=False): class Helix(ShapedComponent): """A spiral wire component used to model a pin wire-wrap. + .. impl:: Helix shaped component + :id: I_ARMI_COMP_SHAPES7 + :implements: R_ARMI_COMP_SHAPES + Notes ----- http://mathworld.wolfram.com/Helix.html diff --git a/armi/reactor/components/component.py b/armi/reactor/components/component.py index ed667ccc0..3084b525e 100644 --- a/armi/reactor/components/component.py +++ b/armi/reactor/components/component.py @@ -169,6 +169,10 @@ class Component(composites.Composite, metaclass=ComponentType): Could be fuel pins, cladding, duct, wire wrap, etc. One component object may represent multiple physical components via the ``multiplicity`` mechanism. + .. impl:: Define a physical piece of a reactor + :id: I_ARMI_COMP_DEF + :implements: R_ARMI_COMP_DEF + Attributes ---------- temperatureInC : float @@ -378,7 +382,12 @@ def getHeightFactor(self, newHot): return self.getThermalExpansionFactor(Tc=newHot, T0=self.temperatureInC) def getProperties(self): - """Return the active Material object defining thermo-mechanical properties.""" + """Return the active Material object defining thermo-mechanical properties. + + .. impl:: Material properties are retrievable + :id: I_ARMI_COMP_MAT0 + :implements: R_ARMI_COMP_MAT + """ return self.material @property @@ -418,6 +427,10 @@ def getArea(self, cold=False): """ Get the area of a component in cm^2. + .. impl:: Set a dimension of a component + :id: I_ARMI_COMP_VOL0 + :implements: R_ARMI_COMP_VOL + See Also -------- block.getVolumeFractions: component coolant is typically the "leftover" and is calculated and set here @@ -439,6 +452,10 @@ def getVolume(self): """ Return the volume [cm^3] of the component. + .. impl:: Set a dimension of a component + :id: I_ARMI_COMP_VOL1 + :implements: R_ARMI_COMP_VOL + Notes ----- ``self.p.volume`` is not set until this method is called, @@ -537,7 +554,12 @@ def containsVoidMaterial(self): return isinstance(self.material, void.Void) def containsSolidMaterial(self): - """Returns True if the component material is a solid.""" + """Returns True if the component material is a solid. + + .. impl:: Determine if material is solid + :id: I_ARMI_COMP_SOLID + :implements: R_ARMI_COMP_SOLID + """ return not isinstance(self.material, material.Fluid) def getComponentArea(self, cold=False): @@ -790,6 +812,10 @@ def getDimension(self, key, Tc=None, cold=False): """ Return a specific dimension at temperature as determined by key. + .. impl:: Retrieve a dimension at a specified temperature + :id: I_ARMI_COMP_DIMS + :implements: R_ARMI_COMP_DIMS + Parameters ---------- key : str diff --git a/armi/reactor/composites.py b/armi/reactor/composites.py index 58841fde0..c750bd1db 100644 --- a/armi/reactor/composites.py +++ b/armi/reactor/composites.py @@ -653,6 +653,12 @@ def nameContains(self, s): return s.lower() in name def getName(self): + """Get composite name. + + .. impl:: Composite name is accessible + :id: I_ARMI_CMP_GET_NAME + :implements: R_ARMI_CMP_GET_NAME + """ return self.name def setName(self, name): @@ -662,6 +668,10 @@ def hasFlags(self, typeID: TypeSpec, exact=False): """ Determine if this object is of a certain type. + .. impl:: Flags can be queried + :id: I_ARMI_CMP_HAS_FLAGS + :implements: R_ARMI_CMP_HAS_FLAGS + Parameters ---------- typeID : TypeSpec @@ -867,6 +877,10 @@ def getMass(self, nuclideNames=None): """ Determine the mass in grams of nuclide(s) and/or elements in this object. + .. impl:: Get mass of composite + :id: I_ARMI_CMP_GET_MASS + :implements: R_ARMI_CMP_GET_MASS + Parameters ---------- nuclideNames : str, optional @@ -1262,6 +1276,10 @@ def getNumberDensities(self, expandFissionProducts=False): """ Retrieve the number densities in atoms/barn-cm of all nuclides (or those requested) in the object. + .. impl:: Number density of composite is retrievable + :id: I_ARMI_CMP_GET_NDENS + :implements: R_ARMI_CMP_GET_NDENS + Parameters ---------- expandFissionProducts : bool (optional) diff --git a/armi/reactor/converters/blockConverters.py b/armi/reactor/converters/blockConverters.py index 245f0eae4..10363cd9d 100644 --- a/armi/reactor/converters/blockConverters.py +++ b/armi/reactor/converters/blockConverters.py @@ -211,7 +211,12 @@ def convert(self): class ComponentMerger(BlockConverter): - """For a provided block, merged the solute component into the solvent component.""" + """For a provided block, merged the solute component into the solvent component. + + .. impl:: Homogenize one component into another + :id: I_ARMI_BLOCKCONV0 + :implements: R_ARMI_BLOCKCONV + """ def __init__(self, sourceBlock, soluteName, solventName): """ @@ -248,6 +253,10 @@ class MultipleComponentMerger(BlockConverter): This could be implemented on the regular ComponentMerger, as the Flags system has enough power in the type specification arguments to things like ``getComponents()``, ``hasFlags()``, etc., to do single and multiple components with the same code. + + .. impl:: Homogenize one component into another + :id: I_ARMI_BLOCKCONV1 + :implements: R_ARMI_BLOCKCONV """ def __init__(self, sourceBlock, soluteNames, solventName, specifiedMinID=0.0): @@ -526,7 +535,12 @@ def __init__( ) def convert(self): - """Perform the conversion.""" + """Perform the conversion. + + .. impl:: Convert hex blocks to cylindrical blocks + :id: I_ARMI_BLOCKCONV_HEX_TO_CYL + :implements: R_ARMI_BLOCKCONV_HEX_TO_CYL + """ runLog.info( "Converting representative block {} to its equivalent cylindrical model".format( self._sourceBlock diff --git a/armi/reactor/converters/tests/test_blockConverter.py b/armi/reactor/converters/tests/test_blockConverter.py index 2e587a151..02c025f72 100644 --- a/armi/reactor/converters/tests/test_blockConverter.py +++ b/armi/reactor/converters/tests/test_blockConverter.py @@ -41,6 +41,13 @@ def tearDown(self): self.td.__exit__(None, None, None) def test_dissolveWireIntoCoolant(self): + """ + Test dissolving wire into coolant. + + .. test:: Homogenize one component into another + :id: T_ARMI_BLOCKCONV0 + :tests: R_ARMI_BLOCKCONV + """ self._test_dissolve(loadTestBlock(), "wire", "coolant") hotBlock = loadTestBlock(cold=False) self._test_dissolve(hotBlock, "wire", "coolant") @@ -48,6 +55,13 @@ def test_dissolveWireIntoCoolant(self): self._test_dissolve(hotBlock, "wire", "coolant") def test_dissolveLinerIntoClad(self): + """ + Test dissolving liner into clad. + + .. test:: Homogenize one component into another + :id: T_ARMI_BLOCKCONV1 + :tests: R_ARMI_BLOCKCONV + """ self._test_dissolve(loadTestBlock(), "outer liner", "clad") hotBlock = loadTestBlock(cold=False) self._test_dissolve(hotBlock, "outer liner", "clad") @@ -91,7 +105,12 @@ def test_build_NthRing(self): ) def test_convert(self): - """Test conversion with no fuel driver.""" + """Test conversion with no fuel driver. + + .. test:: Convert hex blocks to cylindrical blocks + :id: T_ARMI_BLOCKCONV_HEX_TO_CYL1 + :tests: R_ARMI_BLOCKCONV_HEX_TO_CYL + """ block = ( loadTestReactor(TEST_ROOT)[1] .core.getAssemblies(Flags.FUEL)[2] @@ -122,7 +141,12 @@ def test_convert(self): self._checkCiclesAreInContact(converter.convertedBlock) def test_convertHexWithFuelDriver(self): - """Test conversion with fuel driver.""" + """Test conversion with fuel driver. + + .. test:: Convert hex blocks to cylindrical blocks + :id: T_ARMI_BLOCKCONV_HEX_TO_CYL0 + :tests: R_ARMI_BLOCKCONV_HEX_TO_CYL + """ driverBlock = ( loadTestReactor(TEST_ROOT)[1] .core.getAssemblies(Flags.FUEL)[2] diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py index 60b0fc241..a3f8a3bef 100644 --- a/armi/reactor/grids/hexagonal.py +++ b/armi/reactor/grids/hexagonal.py @@ -311,6 +311,12 @@ def overlapsWhichSymmetryLine(self, indices: IJType) -> Optional[int]: return symmetryLine def getSymmetricEquivalents(self, indices: IJKType) -> List[IJType]: + """Retrieve e quivalent contents based on 3rd symmetry. + + .. impl:: Equivalent contents in 3rd geometry are retrievable + :id: I_ARMI_GRID_EQUIVALENTS + :implements: R_ARMI_GRID_EQUIVALENTS + """ if ( self.symmetry.domain == geometry.DomainType.THIRD_CORE and self.symmetry.boundary == geometry.BoundaryType.PERIODIC @@ -360,7 +366,12 @@ def locatorInDomain(self, locator, symmetryOverlap: Optional[bool] = False) -> b return True def isInFirstThird(self, locator, includeTopEdge=False) -> bool: - """True if locator is in first third of hex grid.""" + """True if locator is in first third of hex grid. + + .. impl:: Determine if grid in first third + :id: I_ARMI_GRID_SYMMETRY_LOC + :implements: R_ARMI_GRID_SYMMETRY_LOC + """ ring, pos = self.getRingPos(locator.indices) if ring == 1: return True diff --git a/armi/reactor/grids/tests/test_grids.py b/armi/reactor/grids/tests/test_grids.py index 0cae25308..bb96cd84f 100644 --- a/armi/reactor/grids/tests/test_grids.py +++ b/armi/reactor/grids/tests/test_grids.py @@ -302,6 +302,12 @@ def test_overlapsWhichSymmetryLine(self): ) def test_getSymmetricIdenticalsThird(self): + """Retrieve equivalent contents based on 3rd symmetry. + + .. test:: Equivalent contents in 3rd geometry are retrievable + :id: T_ARMI_GRID_EQUIVALENTS + :tests: R_ARMI_GRID_EQUIVALENTS + """ grid = grids.HexGrid.fromPitch(1.0) grid.symmetry = str( geometry.SymmetryType( @@ -401,6 +407,12 @@ def test_badIndices(self): grid.getCoordinates((0, 5, -1)) def test_isInFirstThird(self): + """Determine if grid is in first third. + + .. test:: Determine if grid in first third + :id: T_ARMI_GRID_SYMMETRY_LOC + :tests: R_ARMI_GRID_SYMMETRY_LOC + """ grid = grids.HexGrid.fromPitch(1.0, numRings=10) self.assertTrue(grid.isInFirstThird(grid[0, 0, 0])) self.assertTrue(grid.isInFirstThird(grid[1, 0, 0])) diff --git a/armi/reactor/tests/test_assemblies.py b/armi/reactor/tests/test_assemblies.py index 193af73c1..7348a35d9 100644 --- a/armi/reactor/tests/test_assemblies.py +++ b/armi/reactor/tests/test_assemblies.py @@ -1006,7 +1006,7 @@ def test_carestianCoordinates(self): .. test:: Cartesian coordinates are retrievable :id: T_ARMI_ASSEM_POSI1 - :test: R_ARMI_ASSEM_POSI + :tests: R_ARMI_ASSEM_POSI """ a = makeTestAssembly( numBlocks=1, diff --git a/armi/reactor/tests/test_components.py b/armi/reactor/tests/test_components.py index 074b6e9cc..e6eddafe1 100644 --- a/armi/reactor/tests/test_components.py +++ b/armi/reactor/tests/test_components.py @@ -44,6 +44,7 @@ ComponentType, ) from armi.reactor.components import materials +from armi.materials import air, alloy200 class TestComponentFactory(unittest.TestCase): @@ -167,7 +168,13 @@ class TestComponent(TestGeneralComponents): componentCls = Component - def test_initializeComponent(self): + def test_initializeComponentMaterial(self): + """Creating component with single material. + + .. test:: Components are made of one material + :id: T_ARMI_COMP_1MAT0 + :tests: R_ARMI_COMP_1MAT + """ expectedName = "TestComponent" actualName = self.component.getName() expectedMaterialName = "HT9" @@ -175,6 +182,44 @@ def test_initializeComponent(self): self.assertEqual(expectedName, actualName) self.assertEqual(expectedMaterialName, actualMaterialName) + def test_setNumberDensity(self): + """Test setting a single number density. + + .. test:: Set Component number density + :id: T_ARMI_COMP_NUCLIDE_FRASCS0 + :tests: R_ARMI_COMP_NUCLIDE_FRACS + """ + component = self.component + self.assertAlmostEqual(component.getNumberDensity("C"), 0.000780, 6) + component.setNumberDensity("C", 0.57) + self.assertEqual(component.getNumberDensity("C"), 0.57) + + def test_setNumberDensities(self): + """Test setting multiple number densities. + + .. test:: Set Component number density + :id: T_ARMI_COMP_NUCLIDE_FRASCS1 + :tests: R_ARMI_COMP_NUCLIDE_FRACS + """ + component = self.component + self.assertAlmostEqual(component.getNumberDensity("MN"), 0.000426, 6) + component.setNumberDensities({"C": 1, "MN": 0.58}) + self.assertEqual(component.getNumberDensity("C"), 1.0) + self.assertEqual(component.getNumberDensity("MN"), 0.58) + + def test_solid_material(self): + """Determine if material is solid. + + .. test:: Determine if material is solid + :id: T_ARMI_COMP_SOLID + :tests: R_ARMI_COMP_SOLID + """ + self.component.material = air.Air() + self.assertFalse(self.component.containsSolidMaterial()) + + self.component.material = alloy200.Alloy200() + self.assertTrue(self.component.containsSolidMaterial()) + class TestNullComponent(TestGeneralComponents): componentCls = NullComponent @@ -190,6 +235,12 @@ def test_nonzero(self): self.assertEqual(cur, ref) def test_getDimension(self): + """Test getting empty component. + + .. test:: Retrieve a null dimension + :id: T_ARMI_COMP_DIMS0 + :tests: R_ARMI_COMP_DIMS + """ self.assertEqual(self.component.getDimension(""), 0.0) @@ -285,12 +336,20 @@ def test_preserveMassDuringThermalExpansion(self): ) def test_volumeAfterClearCache(self): + """ + Test volume after cache has been cleared. + + .. test:: Clear cache after a dimensions updated + :id: T_ARMI_COMP_VOL0 + :tests: R_ARMI_COMP_VOL + """ c = UnshapedVolumetricComponent("testComponent", "Custom", 0, 0, volume=1) self.assertAlmostEqual(c.getVolume(), 1, 6) c.clearCache() self.assertAlmostEqual(c.getVolume(), 1, 6) def test_densityConsistent(self): + """Testing the Component matches quick hand calc.""" c = self.component # no volume defined @@ -352,6 +411,8 @@ def test_getBoundingCircleOuterDiameter(self): class TestCircle(TestShapedComponent): + """Test circle shaped component.""" + componentCls = Circle _id = 5.0 _od = 10 @@ -365,7 +426,12 @@ class TestCircle(TestShapedComponent): } def test_getThermalExpansionFactorConservedMassByLinearExpansionPercent(self): - """Test that when ARMI thermally expands a circle, mass is conserved.""" + """Test that when ARMI thermally expands a circle, mass is conserved. + + .. test:: Circle shaped component + :id: T_ARMI_COMP_SHAPES0 + :tests: R_ARMI_COMP_SHAPES + """ hotTemp = 700.0 dLL = self.component.material.linearExpansionFactor( Tc=hotTemp, T0=self._coldTemp @@ -375,6 +441,12 @@ def test_getThermalExpansionFactorConservedMassByLinearExpansionPercent(self): self.assertAlmostEqual(cur, ref) def test_getDimension(self): + """Test getting component dimension at specific temperature. + + .. test:: Retrieve a dimension at a temperature + :id: T_ARMI_COMP_DIMS1 + :tests: R_ARMI_COMP_DIMS + """ hotTemp = 700.0 ref = self._od * self.component.getThermalExpansionFactor(Tc=hotTemp) cur = self.component.getDimension("od", Tc=hotTemp) @@ -401,6 +473,12 @@ def test_dimensionThermallyExpands(self): self.assertEqual(cur, ref[i]) def test_getArea(self): + """Calculate area of circle. + + .. test:: Calculate area of circle. + :id: T_ARMI_COMP_VOL1 + :tests: R_ARMI_COMP_VOL + """ od = self.component.getDimension("od") idd = self.component.getDimension("id") mult = self.component.getDimension("mult") @@ -676,6 +754,8 @@ def expansionConservationColdHeightDefined(self, mat: str): class TestTriangle(TestShapedComponent): + """Test triangle shaped component.""" + componentCls = Triangle componentDims = { "Tinput": 25.0, @@ -686,6 +766,16 @@ class TestTriangle(TestShapedComponent): } def test_getArea(self): + """Calculate area of triangle. + + .. test:: Calculate area of triangle + :id: T_ARMI_COMP_VOL2 + :tests: R_ARMI_COMP_VOL + + .. test:: Triangle shaped component + :id: T_ARMI_COMP_SHAPES1 + :tests: R_ARMI_COMP_SHAPES + """ b = self.component.getDimension("base") h = self.component.getDimension("height") mult = self.component.getDimension("mult") @@ -706,6 +796,8 @@ def test_dimensionThermallyExpands(self): class TestRectangle(TestShapedComponent): + """Test rectangle shaped component.""" + componentCls = Rectangle componentDims = { "Tinput": 25.0, @@ -738,6 +830,12 @@ def test_negativeArea(self): negativeRectangle.getArea() def test_getBoundingCircleOuterDiameter(self): + """Get outer diameter bounding circle. + + .. test:: Rectangle shaped component + :id: T_ARMI_COMP_SHAPES2 + :tests: R_ARMI_COMP_SHAPES + """ ref = math.sqrt(61.0) cur = self.component.getBoundingCircleOuterDiameter(cold=True) self.assertAlmostEqual(ref, cur) @@ -747,6 +845,12 @@ def test_getCircleInnerDiameter(self): self.assertAlmostEqual(math.sqrt(25.0), cur) def test_getArea(self): + """Calculate area of rectangle. + + .. test:: Calculate area of rectangle + :id: T_ARMI_COMP_VOL3 + :tests: R_ARMI_COMP_VOL + """ outerL = self.component.getDimension("lengthOuter") innerL = self.component.getDimension("lengthInner") outerW = self.component.getDimension("widthOuter") @@ -790,6 +894,12 @@ def test_getBoundingCircleOuterDiameter(self): self.assertAlmostEqual(ref, cur) def test_getArea(self): + """Calculate area of solid rectangle. + + .. test:: Calculate area of solid rectangle. + :id: T_ARMI_COMP_VOL4 + :tests: R_ARMI_COMP_VOL + """ outerL = self.component.getDimension("lengthOuter") outerW = self.component.getDimension("widthOuter") mult = self.component.getDimension("mult") @@ -810,6 +920,8 @@ def test_dimensionThermallyExpands(self): class TestSquare(TestShapedComponent): + """Test square shaped component.""" + componentCls = Square componentDims = { "Tinput": 25.0, @@ -838,6 +950,12 @@ def test_negativeArea(self): negativeRectangle.getArea() def test_getBoundingCircleOuterDiameter(self): + """Get bounding circle outer diameter. + + .. test:: Square shaped component + :id: T_ARMI_COMP_SHAPES3 + :tests: R_ARMI_COMP_SHAPES + """ ref = math.sqrt(18.0) cur = self.component.getBoundingCircleOuterDiameter(cold=True) self.assertAlmostEqual(ref, cur) @@ -848,6 +966,12 @@ def test_getCircleInnerDiameter(self): self.assertAlmostEqual(ref, cur) def test_getArea(self): + """Calculate area of square. + + .. test:: Calculate area of square. + :id: T_ARMI_COMP_VOL5 + :tests: R_ARMI_COMP_VOL + """ outerW = self.component.getDimension("widthOuter") innerW = self.component.getDimension("widthInner") mult = self.component.getDimension("mult") @@ -904,6 +1028,12 @@ def test_negativeVolume(self): negativeCube.getVolume() def test_getVolume(self): + """Calculate area of cube. + + .. test:: Calculate area of cube. + :id: T_ARMI_COMP_VOL6 + :tests: R_ARMI_COMP_VOL + """ lengthO = self.component.getDimension("lengthOuter") widthO = self.component.getDimension("widthOuter") heightO = self.component.getDimension("heightOuter") @@ -921,10 +1051,18 @@ def test_thermallyExpands(self): class TestHexagon(TestShapedComponent): + """Test hexagon shaped component.""" + componentCls = Hexagon componentDims = {"Tinput": 25.0, "Thot": 430.0, "op": 10.0, "ip": 5.0, "mult": 1} def test_getPerimeter(self): + """Get perimeter of hexagon. + + .. test:: Hexagon shaped component + :id: T_ARMI_COMP_SHAPES4 + :tests: R_ARMI_COMP_SHAPES + """ ip = self.component.getDimension("ip") mult = self.component.getDimension("mult") ref = 6 * (ip / math.sqrt(3)) * mult @@ -942,6 +1080,12 @@ def test_getCircleInnerDiameter(self): self.assertAlmostEqual(ref, cur) def test_getArea(self): + """Calculate area of hexagon. + + .. test:: Calculate area of hexagon. + :id: T_ARMI_COMP_VOL7 + :tests: R_ARMI_COMP_VOL + """ cur = self.component.getArea() mult = self.component.getDimension("mult") op = self.component.getDimension("op") @@ -962,6 +1106,8 @@ def test_dimensionThermallyExpands(self): class TestHoledHexagon(TestShapedComponent): + """Test holed hexagon shaped component.""" + componentCls = HoledHexagon componentDims = { "Tinput": 25.0, @@ -998,6 +1144,12 @@ def test_getCircleInnerDiameter(self): ) def test_getArea(self): + """Calculate area of holed hexagon. + + .. test:: Calculate area of holed hexagon. + :id: T_ARMI_COMP_VOL8 + :tests: R_ARMI_COMP_VOL + """ op = self.component.getDimension("op") odHole = self.component.getDimension("holeOD") nHoles = self.component.getDimension("nHoles") @@ -1045,6 +1197,12 @@ def test_getCircleInnerDiameter(self): ) def test_getArea(self): + """Calculate area of hex holed circle. + + .. test:: Calculate area of hex holed circle. + :id: T_ARMI_COMP_VOL9 + :tests: R_ARMI_COMP_VOL + """ od = self.component.getDimension("od") holeOP = self.component.getDimension("holeOP") mult = self.component.getDimension("mult") @@ -1102,6 +1260,12 @@ def test_getCircleInnerDiameter(self): self.assertEqual(ref, cur) def test_getArea(self): + """Calculate area of holed rectangle. + + .. test:: Calculate area of holed rectangle. + :id: T_ARMI_COMP_VOL10 + :tests: R_ARMI_COMP_VOL + """ rectArea = self.length * self.width odHole = self.component.getDimension("holeOD") mult = self.component.getDimension("mult") @@ -1122,6 +1286,7 @@ def test_dimensionThermallyExpands(self): class TestHoledSquare(TestHoledRectangle): + """Test holed square shaped component.""" componentCls = HoledSquare @@ -1149,6 +1314,8 @@ def test_getCircleInnerDiameter(self): class TestHelix(TestShapedComponent): + """Test helix shaped component.""" + componentCls = Helix componentDims = { "Tinput": 25.0, @@ -1171,6 +1338,12 @@ def test_getCircleInnerDiameter(self): self.assertAlmostEqual(ref, cur) def test_getArea(self): + """Calculate area of helix. + + .. test:: Calculate area of helix. + :id: T_ARMI_COMP_VOL11 + :tests: R_ARMI_COMP_VOL + """ cur = self.component.getArea() axialPitch = self.component.getDimension("axialPitch") helixDiameter = self.component.getDimension("helixDiameter") @@ -1250,6 +1423,12 @@ class TestSphere(TestShapedComponent): componentDims = {"Tinput": 25.0, "Thot": 430.0, "od": 1.0, "id": 0.0, "mult": 3} def test_getVolume(self): + """Calculate area of sphere. + + .. test:: Calculate area of sphere. + :id: T_ARMI_COMP_VOL12 + :tests: R_ARMI_COMP_VOL + """ od = self.component.getDimension("od") idd = self.component.getDimension("id") mult = self.component.getDimension("mult") @@ -1319,6 +1498,13 @@ def test_getVolume(self): self.assertAlmostEqual(cur, ref) def test_updateDims(self): + """ + Test Update dimensions. + + .. test:: Dimensions can be updated + :id: T_ARMI_COMP_VOL13 + :tests: R_ARMI_COMP_VOL + """ self.assertEqual(self.component.getDimension("inner_radius"), 110) self.assertEqual(self.component.getDimension("radius_differential"), 60) self.component.updateDims() diff --git a/armi/reactor/tests/test_composites.py b/armi/reactor/tests/test_composites.py index 22a92a645..6655e96fb 100644 --- a/armi/reactor/tests/test_composites.py +++ b/armi/reactor/tests/test_composites.py @@ -218,6 +218,12 @@ def test_nucSpec(self): ) def test_hasFlags(self): + """Ensure flags are queryable. + + .. test:: Flags can be queried + :id: T_ARMI_CMP_HAS_FLAGS + :tests: R_ARMI_CMP_HAS_FLAGS + """ self.container.setType("fuel") self.assertFalse(self.container.hasFlags(Flags.SHIELD | Flags.FUEL, exact=True)) self.assertTrue(self.container.hasFlags(Flags.FUEL)) @@ -608,6 +614,12 @@ def setUp(self): self.obj = loadTestBlock() def test_setMass(self): + """Test setting and retrieving mass. + + .. test:: Get mass of composite + :id: T_ARMI_CMP_GET_MASS + :tests: R_ARMI_CMP_GET_MASS + """ masses = {"U235": 5.0, "U238": 3.0} self.obj.setMasses(masses) self.assertAlmostEqual(self.obj.getMass("U235"), 5.0) @@ -624,6 +636,17 @@ def test_setMass(self): group.setMass("U235", 5) self.assertAlmostEqual(group.getMass("U235"), 5) + def test_getNumberDensities(self): + """Get number densities from composite. + + .. test:: Number density of composite is retrievable + :id: T_ARMI_CMP_GET_NDENS0 + :tests: R_ARMI_CMP_GET_NDENS + """ + ndens = self.obj.getNumberDensities() + self.assertAlmostEqual(0.0001096, ndens["SI"], 7) + self.assertAlmostEqual(0.0000368, ndens["W"], 7) + def test_dimensionReport(self): report = self.obj.setComponentDimensionsReport() self.assertEqual(len(report), len(self.obj)) From 6ae1598fbafbfc3a1cb4bba4737f85b62ee9f7da Mon Sep 17 00:00:00 2001 From: bdlafleur Date: Wed, 22 Nov 2023 13:39:38 -0500 Subject: [PATCH 053/176] Sweeping crumbs for grammatical fixes (#1488) --- armi/cases/__init__.py | 2 +- armi/cases/suite.py | 2 +- armi/cases/tests/test_cases.py | 4 +-- armi/materials/__init__.py | 6 ++-- armi/materials/tests/test_materials.py | 2 +- armi/nucDirectory/tests/test_elements.py | 4 +-- armi/nuclearDataIO/cccc/geodst.py | 2 +- armi/operators/operator.py | 2 +- .../tests/test_globalFluxInterface.py | 10 +++---- armi/reactor/assemblies.py | 14 ++++----- armi/reactor/blocks.py | 30 +++++++++---------- armi/reactor/blueprints/assemblyBlueprint.py | 2 +- armi/reactor/blueprints/blockBlueprint.py | 2 +- armi/reactor/blueprints/componentBlueprint.py | 2 +- armi/reactor/blueprints/gridBlueprint.py | 4 +-- armi/reactor/blueprints/reactorBlueprint.py | 2 +- .../tests/test_assemblyBlueprints.py | 2 +- .../blueprints/tests/test_blockBlueprints.py | 4 +-- .../blueprints/tests/test_blueprints.py | 2 +- .../blueprints/tests/test_gridBlueprints.py | 6 ++-- .../tests/test_reactorBlueprints.py | 4 +-- armi/reactor/components/component.py | 12 ++++---- armi/reactor/composites.py | 10 +++---- armi/reactor/converters/blockConverters.py | 6 ++-- .../converters/tests/test_blockConverter.py | 8 ++--- .../converters/tests/test_uniformMesh.py | 4 +-- armi/reactor/converters/uniformMesh.py | 5 ++-- armi/reactor/flags.py | 4 +-- armi/reactor/grids/hexagonal.py | 4 +-- armi/reactor/grids/tests/test_grids.py | 4 +-- .../parameters/parameterDefinitions.py | 4 +-- armi/reactor/reactors.py | 10 +++---- armi/reactor/tests/test_assemblies.py | 16 +++++----- armi/reactor/tests/test_blocks.py | 10 +++---- armi/reactor/tests/test_components.py | 20 ++++++------- armi/reactor/tests/test_composites.py | 6 ++-- armi/reactor/tests/test_flags.py | 2 +- armi/reactor/tests/test_parameters.py | 2 +- armi/reactor/tests/test_reactors.py | 12 ++++---- armi/reactor/tests/test_zones.py | 10 +++---- armi/reactor/zones.py | 4 +-- armi/utils/densityTools.py | 8 ++--- armi/utils/flags.py | 4 +-- armi/utils/tests/test_densityTools.py | 4 +-- armi/utils/tests/test_flags.py | 4 +-- armi/utils/tests/test_hexagon.py | 4 +-- 46 files changed, 143 insertions(+), 142 deletions(-) diff --git a/armi/cases/__init__.py b/armi/cases/__init__.py index f01b81939..7b6a9b48a 100644 --- a/armi/cases/__init__.py +++ b/armi/cases/__init__.py @@ -46,7 +46,7 @@ suite.discover('my-cases*.yaml', recursive=True) suite.run() -.. warning: Suite running may not work yet if the cases have interdependencies. +.. warning:: Suite running may not work yet if the cases have interdependencies. Create a ``burnStep`` sensitivity study from some base CS:: diff --git a/armi/cases/suite.py b/armi/cases/suite.py index d5bda9875..fb390aa2b 100644 --- a/armi/cases/suite.py +++ b/armi/cases/suite.py @@ -201,7 +201,7 @@ def run(self): """ Run each case, one after the other. - .. warning: Suite running may not work yet if the cases have interdependencies. + .. warning:: Suite running may not work yet if the cases have interdependencies. We typically run on a HPC but are still working on a platform independent way of handling HPCs. """ diff --git a/armi/cases/tests/test_cases.py b/armi/cases/tests/test_cases.py index 0fea932ee..952321148 100644 --- a/armi/cases/tests/test_cases.py +++ b/armi/cases/tests/test_cases.py @@ -202,7 +202,7 @@ def test_run(self): """ Test running a case. - .. test:: Generic mechanism to allow simulation runs + .. test:: There is a generic mechanism to allow simulation runs. :id: T_ARMI_CASE :tests: R_ARMI_CASE """ @@ -300,7 +300,7 @@ def test_checkInputs(self): """ Test the checkInputs() method on a couple of cases. - .. test:: Test the checkInputs method. + .. test:: Check the ARMI inputs for consistency and validity. :id: T_ARMI_CASE_CHECK :tests: R_ARMI_CASE_CHECK """ diff --git a/armi/materials/__init__.py b/armi/materials/__init__.py index 5fb42d25a..f231e44b6 100644 --- a/armi/materials/__init__.py +++ b/armi/materials/__init__.py @@ -15,9 +15,9 @@ """ The material package defines compositions and material-specific properties. -Properties in scope include temperature dependent thermo/mechanical properties +Properties in scope include temperature dependent thermo/mechanical properties (like heat capacity, linear expansion coefficients, viscosity, density), -and material-specific nuclear properties that can't exist at the nuclide level +and material-specific nuclear properties that can't exist at the nuclide level alone (like :py:mod:`thermal scattering laws `). As the fundamental macroscopic building blocks of any physical object, @@ -47,7 +47,7 @@ def setMaterialNamespaceOrder(order): """ - Set the material namespace order at the Python interpretter, global level. + Set the material namespace order at the Python interpreter, global level. .. impl:: Materials can be searched across packages in a defined namespace. :id: I_ARMI_MAT_NAMESPACE diff --git a/armi/materials/tests/test_materials.py b/armi/materials/tests/test_materials.py index eaf1be3a7..e14683696 100644 --- a/armi/materials/tests/test_materials.py +++ b/armi/materials/tests/test_materials.py @@ -143,7 +143,7 @@ class MaterialFindingTests(unittest.TestCase): def test_findMaterial(self): """Test resolveMaterialClassByName() function. - .. test:: You can find a meterial by name. + .. test:: You can find a material by name. :id: T_ARMI_MAT_NAME :tests: R_ARMI_MAT_NAME """ diff --git a/armi/nucDirectory/tests/test_elements.py b/armi/nucDirectory/tests/test_elements.py index 9ad2a2dac..e508c8596 100644 --- a/armi/nucDirectory/tests/test_elements.py +++ b/armi/nucDirectory/tests/test_elements.py @@ -103,7 +103,7 @@ def test_element_isNaturallyOccurring(self): Uses RIPL definitions of naturally occurring. Protactinium is debated as naturally occurring. Yeah it exists as a U235 decay product but it's kind of pseudo-natural. - .. test:: Get elements by Z, to show if they are naturally occurring. + .. test:: Get elements by Z to show if they are naturally occurring. :id: T_ARMI_ND_ELEMENTS3 :tests: R_ARMI_ND_ELEMENTS """ @@ -128,7 +128,7 @@ def test_abundancesAddToOne(self): def test_isHeavyMetal(self): """Get elements by Z. - .. test:: Get elements by Z, to show if they are heavy metals. + .. test:: Get elements by Z to show if they are heavy metals. :id: T_ARMI_ND_ELEMENTS4 :tests: R_ARMI_ND_ELEMENTS """ diff --git a/armi/nuclearDataIO/cccc/geodst.py b/armi/nuclearDataIO/cccc/geodst.py index ee777bcad..00122cad2 100644 --- a/armi/nuclearDataIO/cccc/geodst.py +++ b/armi/nuclearDataIO/cccc/geodst.py @@ -133,7 +133,7 @@ def readWrite(self): Logic to control which records will be present is here, which comes directly off the File specification. - .. impl:: Read and write GEODST files. + .. impl:: Tool to read and write GEODST files. :id: I_ARMI_NUCDATA_GEODST :implements: R_ARMI_NUCDATA_GEODST """ diff --git a/armi/operators/operator.py b/armi/operators/operator.py index 5e2adb6b4..663aca42a 100644 --- a/armi/operators/operator.py +++ b/armi/operators/operator.py @@ -73,7 +73,7 @@ class Operator: .. note:: The :doc:`/developer/guide` has some additional narrative on this topic. - .. impl:: An operator will have a reactor object, to communicate between plugins. + .. impl:: An operator will have a reactor object to communicate between plugins. :id: I_ARMI_OPERATOR_COMM :implements: R_ARMI_OPERATOR_COMM diff --git a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py index 1c910766a..dbe6cf924 100644 --- a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py @@ -107,7 +107,7 @@ class TestGlobalFluxOptions(unittest.TestCase): """ Tests for GlobalFluxOptions. - .. test:: Tests GlobalFluxOptions + .. test:: Tests GlobalFluxOptions. :id: T_ARMI_FLUX_OPTIONS :tests: R_ARMI_FLUX_OPTIONS """ @@ -170,7 +170,7 @@ def test_checkEnergyBalance(self): """ Test energy balance check. - .. test:: Block-wise power is consistent with reactor data model power + .. test:: Block-wise power is consistent with reactor data model power. :id: T_ARMI_FLUX_CHECK_POWER :tests: R_ARMI_FLUX_CHECK_POWER """ @@ -218,7 +218,7 @@ def test_setTightCouplingDefaults(self): def test_getTightCouplingValue(self): """Test getTightCouplingValue returns the correct value for keff and type for power. - .. test:: Get k-eff or assembly-wise power for coupling interactions + .. test:: Return k-eff or assembly-wise power for coupling interactions. :id: T_ARMI_FLUX_COUPLING_VALUE :tests: R_ARMI_FLUX_COUPLING_VALUE """ @@ -367,11 +367,11 @@ def test_calcReactionRates(self): """ Test that the reaction rate code executes and sets a param > 0.0. - .. test:: Return the reaction rates for a given ArmiObject + .. test:: Return the reaction rates for a given ArmiObject. :id: T_ARMI_FLUX_RX_RATES :tests: R_ARMI_FLUX_RX_RATES - .. warning: This does not validate the reaction rate calculation. + .. warning:: This does not validate the reaction rate calculation. """ b = test_blocks.loadTestBlock() test_blocks.applyDummyData(b) diff --git a/armi/reactor/assemblies.py b/armi/reactor/assemblies.py index ac0152b1b..bff1866e9 100644 --- a/armi/reactor/assemblies.py +++ b/armi/reactor/assemblies.py @@ -177,7 +177,7 @@ def add(self, obj: blocks.Block): The simple act of adding a block to an assembly fully defines the location of the block in 3-D. - .. impl:: Assemblies are made up of type Block + .. impl:: Assemblies are made up of type Block. :id: I_ARMI_ASSEM_BLOCKS :implements: R_ARMI_ASSEM_BLOCKS @@ -224,7 +224,7 @@ def getLocation(self): siblings of a Core. In future, this will likely be re-implemented in terms of just spatialLocator objects. - .. impl:: Assembly location is retrievable + .. impl:: Assembly location is retrievable. :id: I_ARMI_ASSEM_POSI0 :implements: R_ARMI_ASSEM_POSI @@ -242,7 +242,7 @@ def coords(self): """Return the location of the assembly in the plane using cartesian global coordinates. - .. impl:: Assembly coordinates are retrievable + .. impl:: Assembly coordinates are retrievable. :id: I_ARMI_ASSEM_POSI1 :implements: R_ARMI_ASSEM_POSI """ @@ -256,7 +256,7 @@ def getArea(self): The assumption is that all blocks in an assembly have the same area. Calculate the total assembly volume in cm^3. - .. impl:: Assembly area is retrievable + .. impl:: Assembly area is retrievable. :id: I_ARMI_ASSEM_DIMS0 :implements: R_ARMI_ASSEM_DIMS """ @@ -271,7 +271,7 @@ def getArea(self): def getVolume(self): """Calculate the total assembly volume in cm^3. - .. impl:: Assembly volume is retrievable + .. impl:: Assembly volume is retrievable. :id: I_ARMI_ASSEM_DIMS1 :implements: R_ARMI_ASSEM_DIMS """ @@ -475,7 +475,7 @@ def getTotalHeight(self, typeSpec=None): """ Determine the height of this assembly in cm. - .. impl:: Assembly height is retrievable + .. impl:: Assembly height is retrievable. :id: I_ARMI_ASSEM_DIMS2 :implements: R_ARMI_ASSEM_DIMS @@ -1192,7 +1192,7 @@ def getDim(self, typeSpec, dimName): Example: getDim(Flags.WIRE, 'od') will return a wire's OD in cm. - .. impl:: Assembly dimensions are retrievable + .. impl:: Assembly dimensions are retrievable. :id: I_ARMI_ASSEM_DIMS3 :implements: R_ARMI_ASSEM_DIMS """ diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py index 699c03ae5..43fbaa078 100644 --- a/armi/reactor/blocks.py +++ b/armi/reactor/blocks.py @@ -583,7 +583,7 @@ def adjustUEnrich(self, newEnrich): def getLocation(self): """Return a string representation of the location. - .. impl:: Location of a block is retrievable + .. impl:: Location of a block is retrievable. :id: I_ARMI_BLOCK_POSI0 :implements: R_ARMI_BLOCK_POSI """ @@ -598,7 +598,7 @@ def coords(self, rotationDegreesCCW=0.0): """ Returns the coordinates of the block. - .. impl:: Coordinates of a block are queryable + .. impl:: Coordinates of a block are queryable. :id: I_ARMI_BLOCK_POSI1 :implements: R_ARMI_BLOCK_POSI """ @@ -682,7 +682,7 @@ def getVolume(self): """ Return the volume of a block. - .. impl:: Volume of block is retrievable + .. impl:: Volume of block is retrievable. :id: I_ARMI_BLOCK_DIMS0 :implements: R_ARMI_BLOCK_DIMS @@ -1247,7 +1247,7 @@ def getPitch(self, returnComp=False): Component that has the max pitch, if returnComp == True. If no component is found to define the pitch, returns None - .. impl:: Pitch of block is retrievable + .. impl:: Pitch of block is retrievable. :id: I_ARMI_BLOCK_DIMS1 :implements: R_ARMI_BLOCK_DIMS @@ -1598,7 +1598,7 @@ class HexBlock(Block): """ Defines a HexBlock. - .. impl:: Ability to create hex shaped blocks + .. impl:: ARMI has the ability to create hex shaped blocks. :id: I_ARMI_BLOCK_HEX :implements: R_ARMI_BLOCK_HEX """ @@ -1612,7 +1612,7 @@ def coords(self, rotationDegreesCCW=0.0): """ Returns the coordinates of the block. - .. impl:: Coordinates of a block are queryable + .. impl:: Coordinates of a block are queryable. :id: I_ARMI_BLOCK_POSI2 :implements: R_ARMI_BLOCK_POSI """ @@ -1628,7 +1628,7 @@ def _createHomogenizedCopy(self, pinSpatialLocators=False): """ Create a new homogenized copy of a block that is less expensive than a full deepcopy. - .. impl:: Homogenize the compositions of a block + .. impl:: Block compositions can be homogenized. :id: I_ARMI_BLOCK_HOMOG :implements: R_ARMI_BLOCK_HOMOG @@ -1713,7 +1713,7 @@ def getMaxArea(self): """ Compute the max area of this block if it was totally full. - .. impl:: Area of block is retrievable + .. impl:: Area of block is retrievable. :id: I_ARMI_BLOCK_DIMS2 :implements: R_ARMI_BLOCK_DIMS """ @@ -1726,7 +1726,7 @@ def getDuctIP(self): """ Returns the duct IP dimension. - .. impl:: IP dimension is retrievable + .. impl:: IP dimension is retrievable. :id: I_ARMI_BLOCK_DIMS3 :implements: R_ARMI_BLOCK_DIMS """ @@ -1737,7 +1737,7 @@ def getDuctOP(self): """ Returns the duct OP dimension. - .. impl:: OP dimension is retrievable + .. impl:: OP dimension is retrievable. :id: I_ARMI_BLOCK_DIMS4 :implements: R_ARMI_BLOCK_DIMS """ @@ -2041,7 +2041,7 @@ def getPinToDuctGap(self, cold=False): """ Returns the distance in cm between the outer most pin and the duct in a block. - .. impl:: Pin to duct gap of block is retrievable + .. impl:: Pin to duct gap of block is retrievable. :id: I_ARMI_BLOCK_DIMS5 :implements: R_ARMI_BLOCK_DIMS @@ -2225,7 +2225,7 @@ def getPinPitch(self, cold=False): Assumes that the pin pitch is defined entirely by contacting cladding tubes and wire wraps. Grid spacers not yet supported. - .. impl:: Pin pitch within block is retrievable + .. impl:: Pin pitch within block is retrievable. :id: I_ARMI_BLOCK_DIMS6 :implements: R_ARMI_BLOCK_DIMS @@ -2262,7 +2262,7 @@ def getPinPitch(self, cold=False): def getWettedPerimeter(self): """Return the total wetted perimeter of the block in cm. - .. impl:: Wetted perimeter of block is retrievable + .. impl:: Wetted perimeter of block is retrievable. :id: I_ARMI_BLOCK_DIMS7 :implements: R_ARMI_BLOCK_DIMS """ @@ -2340,7 +2340,7 @@ def getWettedPerimeter(self): def getFlowArea(self): """Return the total flowing coolant area of the block in cm^2. - .. impl:: Flow area of block is retrievable + .. impl:: Flow area of block is retrievable. :id: I_ARMI_BLOCK_DIMS8 :implements: R_ARMI_BLOCK_DIMS """ @@ -2361,7 +2361,7 @@ def getHydraulicDiameter(self): p = sqrt(3)*s l = 6*p/sqrt(3) - .. impl:: Hydraulic diameter of block is retrievable + .. impl:: Hydraulic diameter of block is retrievable. :id: I_ARMI_BLOCK_DIMS9 :implements: R_ARMI_BLOCK_DIMS """ diff --git a/armi/reactor/blueprints/assemblyBlueprint.py b/armi/reactor/blueprints/assemblyBlueprint.py index 4864c64d2..5d5b75246 100644 --- a/armi/reactor/blueprints/assemblyBlueprint.py +++ b/armi/reactor/blueprints/assemblyBlueprint.py @@ -93,7 +93,7 @@ class AssemblyBlueprint(yamlize.Object): This class utilizes ``yamlize`` to enable serialization to and from the blueprints YAML file. - .. impl:: Create assembly from blueprint file + .. impl:: Create assembly from blueprint file. :id: I_ARMI_BP_ASSEM :implements: R_ARMI_BP_ASSEM """ diff --git a/armi/reactor/blueprints/blockBlueprint.py b/armi/reactor/blueprints/blockBlueprint.py index 33df26448..fd737a6ed 100644 --- a/armi/reactor/blueprints/blockBlueprint.py +++ b/armi/reactor/blueprints/blockBlueprint.py @@ -44,7 +44,7 @@ def _configureGeomOptions(): class BlockBlueprint(yamlize.KeyedList): """Input definition for Block. - .. impl:: Create a Block from blueprint file + .. impl:: Create a Block from blueprint file. :id: I_ARMI_BP_BLOCK :implements: R_ARMI_BP_BLOCK """ diff --git a/armi/reactor/blueprints/componentBlueprint.py b/armi/reactor/blueprints/componentBlueprint.py index 51ba17857..244229517 100644 --- a/armi/reactor/blueprints/componentBlueprint.py +++ b/armi/reactor/blueprints/componentBlueprint.py @@ -120,7 +120,7 @@ class ComponentBlueprint(yamlize.Object): This class defines the inputs necessary to build ARMI component objects. It uses ``yamlize`` to enable serialization to and from YAML. - .. impl:: Construct component from blueprint file + .. impl:: Construct component from blueprint file. :id: I_ARMI_BP_COMP :implements: R_ARMI_BP_COMP """ diff --git a/armi/reactor/blueprints/gridBlueprint.py b/armi/reactor/blueprints/gridBlueprint.py index 2ce46b968..d2a12932d 100644 --- a/armi/reactor/blueprints/gridBlueprint.py +++ b/armi/reactor/blueprints/gridBlueprint.py @@ -248,7 +248,7 @@ def readFromLatticeMap(self, value): def construct(self): """Build a Grid from a grid definition. - .. impl:: Define a lattice map in reactor core + .. impl:: Define a lattice map in reactor core. :id: I_ARMI_BP_GRID :implements: R_ARMI_BP_GRID """ @@ -540,7 +540,7 @@ def saveToStream(stream, bluep, full=False, tryMap=False): tryMap: regardless of input form, attempt to output as a lattice map. let's face it; they're prettier. - .. impl:: Write a blueprint file from a blueprint object + .. impl:: Write a blueprint file from a blueprint object. :id: I_ARMI_BP_TO_DB :implements: R_ARMI_BP_TO_DB """ diff --git a/armi/reactor/blueprints/reactorBlueprint.py b/armi/reactor/blueprints/reactorBlueprint.py index 7667404d7..4875961de 100644 --- a/armi/reactor/blueprints/reactorBlueprint.py +++ b/armi/reactor/blueprints/reactorBlueprint.py @@ -105,7 +105,7 @@ def construct(self, cs, bp, reactor, geom=None, loadAssems=True): :id: I_ARMI_BP_SYSTEMS :implements: R_ARMI_BP_SYSTEMS - .. impl:: Create core object with blueprint + .. impl:: Create core object from blueprint. :id: I_ARMI_BP_CORE :implements: R_ARMI_BP_CORE diff --git a/armi/reactor/blueprints/tests/test_assemblyBlueprints.py b/armi/reactor/blueprints/tests/test_assemblyBlueprints.py index 1197f41bd..fd6863b9d 100644 --- a/armi/reactor/blueprints/tests/test_assemblyBlueprints.py +++ b/armi/reactor/blueprints/tests/test_assemblyBlueprints.py @@ -186,7 +186,7 @@ def test_checkParamConsistency(self): """ Load assembly from a blueprint file. - .. test:: Create assembly from blueprint file + .. test:: Create assembly from blueprint file. :id: T_ARMI_BP_ASSEM :tests: R_ARMI_BP_ASSEM """ diff --git a/armi/reactor/blueprints/tests/test_blockBlueprints.py b/armi/reactor/blueprints/tests/test_blockBlueprints.py index 02988516f..34a46e91f 100644 --- a/armi/reactor/blueprints/tests/test_blockBlueprints.py +++ b/armi/reactor/blueprints/tests/test_blockBlueprints.py @@ -257,7 +257,7 @@ class TestGriddedBlock(unittest.TestCase): """Tests for a block that has components in a lattice. - .. test:: Create block with blueprint file + .. test:: Create block with blueprint file. :id: T_ARMI_BP_BLOCK :tests: R_ARMI_BP_BLOCK """ @@ -309,7 +309,7 @@ def test_explicitFlags(self): """ Test flags are created from blueprint file. - .. test:: Test depletable nuc flags + .. test:: Nuc flags can define depletable objects. :id: T_ARMI_BP_NUC_FLAGS :tests: R_ARMI_BP_NUC_FLAGS """ diff --git a/armi/reactor/blueprints/tests/test_blueprints.py b/armi/reactor/blueprints/tests/test_blueprints.py index ffd3540f9..9661ad820 100644 --- a/armi/reactor/blueprints/tests/test_blueprints.py +++ b/armi/reactor/blueprints/tests/test_blueprints.py @@ -89,7 +89,7 @@ def test_specialIsotopicVectors(self): def test_componentDimensions(self): """Tests that the user can specify the dimensions of a component with arbitrary fidelity. - .. test:: Test that a component can be correctly created from a blueprint file + .. test:: A component can be correctly created from a blueprint file. :id: T_ARMI_BP_COMP :tests: R_ARMI_BP_COMP """ diff --git a/armi/reactor/blueprints/tests/test_gridBlueprints.py b/armi/reactor/blueprints/tests/test_gridBlueprints.py index 6e8f52a91..757e19084 100644 --- a/armi/reactor/blueprints/tests/test_gridBlueprints.py +++ b/armi/reactor/blueprints/tests/test_gridBlueprints.py @@ -317,7 +317,7 @@ def test_roundTrip(self): """ Test saving blueprint data to a stream. - .. test:: Write blueprints settings to disk + .. test:: Blueprints settings can be written to disk. :id: T_ARMI_BP_TO_DB :tests: R_ARMI_BP_TO_DB """ @@ -331,7 +331,7 @@ def test_tiny_map(self): """ Test that a lattice map can be defined, written, and read in from blueprint file. - .. test:: Define a lattice map in reactor core + .. test:: Define a lattice map in reactor core. :id: T_ARMI_BP_GRID1 :tests: R_ARMI_BP_GRID """ @@ -397,7 +397,7 @@ def test_simpleRead(self): def test_simpleReadLatticeMap(self): """Read lattice map and create a grid. - .. test:: Define a lattice map in reactor core + .. test:: Define a lattice map in reactor core. :id: T_ARMI_BP_GRID0 :tests: R_ARMI_BP_GRID """ diff --git a/armi/reactor/blueprints/tests/test_reactorBlueprints.py b/armi/reactor/blueprints/tests/test_reactorBlueprints.py index b13ccdf05..c50645c52 100644 --- a/armi/reactor/blueprints/tests/test_reactorBlueprints.py +++ b/armi/reactor/blueprints/tests/test_reactorBlueprints.py @@ -105,11 +105,11 @@ def _setupReactor(self): def test_construct(self): """Actually construct some reactor systems. - .. test:: Create core and spent fuel pool with blueprint + .. test:: Create core and spent fuel pool with blueprint. :id: T_ARMI_BP_SYSTEMS :tests: R_ARMI_BP_SYSTEMS - .. test:: Create core object with blueprint + .. test:: Create core object with blueprint. :id: T_ARMI_BP_CORE :tests: R_ARMI_BP_CORE """ diff --git a/armi/reactor/components/component.py b/armi/reactor/components/component.py index 3084b525e..49a1f5c4d 100644 --- a/armi/reactor/components/component.py +++ b/armi/reactor/components/component.py @@ -169,7 +169,7 @@ class Component(composites.Composite, metaclass=ComponentType): Could be fuel pins, cladding, duct, wire wrap, etc. One component object may represent multiple physical components via the ``multiplicity`` mechanism. - .. impl:: Define a physical piece of a reactor + .. impl:: Define a physical piece of a reactor. :id: I_ARMI_COMP_DEF :implements: R_ARMI_COMP_DEF @@ -384,7 +384,7 @@ def getHeightFactor(self, newHot): def getProperties(self): """Return the active Material object defining thermo-mechanical properties. - .. impl:: Material properties are retrievable + .. impl:: Material properties are retrievable. :id: I_ARMI_COMP_MAT0 :implements: R_ARMI_COMP_MAT """ @@ -427,7 +427,7 @@ def getArea(self, cold=False): """ Get the area of a component in cm^2. - .. impl:: Set a dimension of a component + .. impl:: Set a dimension of a component. :id: I_ARMI_COMP_VOL0 :implements: R_ARMI_COMP_VOL @@ -452,7 +452,7 @@ def getVolume(self): """ Return the volume [cm^3] of the component. - .. impl:: Set a dimension of a component + .. impl:: Set a dimension of a component. :id: I_ARMI_COMP_VOL1 :implements: R_ARMI_COMP_VOL @@ -556,7 +556,7 @@ def containsVoidMaterial(self): def containsSolidMaterial(self): """Returns True if the component material is a solid. - .. impl:: Determine if material is solid + .. impl:: Determine if a material is solid. :id: I_ARMI_COMP_SOLID :implements: R_ARMI_COMP_SOLID """ @@ -812,7 +812,7 @@ def getDimension(self, key, Tc=None, cold=False): """ Return a specific dimension at temperature as determined by key. - .. impl:: Retrieve a dimension at a specified temperature + .. impl:: Retrieve a dimension at a specified temperature. :id: I_ARMI_COMP_DIMS :implements: R_ARMI_COMP_DIMS diff --git a/armi/reactor/composites.py b/armi/reactor/composites.py index c750bd1db..40cfc9c19 100644 --- a/armi/reactor/composites.py +++ b/armi/reactor/composites.py @@ -296,7 +296,7 @@ class ArmiObject(metaclass=CompositeModelType): specialized subclasses (Block, Assembly) in preparation for this next step. As a result, the public API on this method should be considered unstable. - .. impl:: Parameters accessible throughout the armi tree + .. impl:: Parameters are accessible throughout the armi tree. :id: I_ARMI_PARAM_PART :implements: R_ARMI_PARAM_PART @@ -655,7 +655,7 @@ def nameContains(self, s): def getName(self): """Get composite name. - .. impl:: Composite name is accessible + .. impl:: Composite name is accessible. :id: I_ARMI_CMP_GET_NAME :implements: R_ARMI_CMP_GET_NAME """ @@ -668,7 +668,7 @@ def hasFlags(self, typeID: TypeSpec, exact=False): """ Determine if this object is of a certain type. - .. impl:: Flags can be queried + .. impl:: Flags can be queried. :id: I_ARMI_CMP_HAS_FLAGS :implements: R_ARMI_CMP_HAS_FLAGS @@ -877,7 +877,7 @@ def getMass(self, nuclideNames=None): """ Determine the mass in grams of nuclide(s) and/or elements in this object. - .. impl:: Get mass of composite + .. impl:: Return mass of composite. :id: I_ARMI_CMP_GET_MASS :implements: R_ARMI_CMP_GET_MASS @@ -1276,7 +1276,7 @@ def getNumberDensities(self, expandFissionProducts=False): """ Retrieve the number densities in atoms/barn-cm of all nuclides (or those requested) in the object. - .. impl:: Number density of composite is retrievable + .. impl:: Number density of composite is retrievable. :id: I_ARMI_CMP_GET_NDENS :implements: R_ARMI_CMP_GET_NDENS diff --git a/armi/reactor/converters/blockConverters.py b/armi/reactor/converters/blockConverters.py index 10363cd9d..6eadc5bf6 100644 --- a/armi/reactor/converters/blockConverters.py +++ b/armi/reactor/converters/blockConverters.py @@ -213,7 +213,7 @@ def convert(self): class ComponentMerger(BlockConverter): """For a provided block, merged the solute component into the solvent component. - .. impl:: Homogenize one component into another + .. impl:: Homogenize one component into another. :id: I_ARMI_BLOCKCONV0 :implements: R_ARMI_BLOCKCONV """ @@ -254,7 +254,7 @@ class MultipleComponentMerger(BlockConverter): in the type specification arguments to things like ``getComponents()``, ``hasFlags()``, etc., to do single and multiple components with the same code. - .. impl:: Homogenize one component into another + .. impl:: Homogenize one component into another. :id: I_ARMI_BLOCKCONV1 :implements: R_ARMI_BLOCKCONV """ @@ -537,7 +537,7 @@ def __init__( def convert(self): """Perform the conversion. - .. impl:: Convert hex blocks to cylindrical blocks + .. impl:: Convert hex blocks to cylindrical blocks. :id: I_ARMI_BLOCKCONV_HEX_TO_CYL :implements: R_ARMI_BLOCKCONV_HEX_TO_CYL """ diff --git a/armi/reactor/converters/tests/test_blockConverter.py b/armi/reactor/converters/tests/test_blockConverter.py index 02c025f72..e3f612c23 100644 --- a/armi/reactor/converters/tests/test_blockConverter.py +++ b/armi/reactor/converters/tests/test_blockConverter.py @@ -44,7 +44,7 @@ def test_dissolveWireIntoCoolant(self): """ Test dissolving wire into coolant. - .. test:: Homogenize one component into another + .. test:: Homogenize one component into another. :id: T_ARMI_BLOCKCONV0 :tests: R_ARMI_BLOCKCONV """ @@ -58,7 +58,7 @@ def test_dissolveLinerIntoClad(self): """ Test dissolving liner into clad. - .. test:: Homogenize one component into another + .. test:: Homogenize one component into another. :id: T_ARMI_BLOCKCONV1 :tests: R_ARMI_BLOCKCONV """ @@ -107,7 +107,7 @@ def test_build_NthRing(self): def test_convert(self): """Test conversion with no fuel driver. - .. test:: Convert hex blocks to cylindrical blocks + .. test:: Convert hex blocks to cylindrical blocks. :id: T_ARMI_BLOCKCONV_HEX_TO_CYL1 :tests: R_ARMI_BLOCKCONV_HEX_TO_CYL """ @@ -143,7 +143,7 @@ def test_convert(self): def test_convertHexWithFuelDriver(self): """Test conversion with fuel driver. - .. test:: Convert hex blocks to cylindrical blocks + .. test:: Convert hex blocks to cylindrical blocks. :id: T_ARMI_BLOCKCONV_HEX_TO_CYL0 :tests: R_ARMI_BLOCKCONV_HEX_TO_CYL """ diff --git a/armi/reactor/converters/tests/test_uniformMesh.py b/armi/reactor/converters/tests/test_uniformMesh.py index b0319fef8..febdc182e 100644 --- a/armi/reactor/converters/tests/test_uniformMesh.py +++ b/armi/reactor/converters/tests/test_uniformMesh.py @@ -254,7 +254,7 @@ def test_filterMesh(self): """ Test that the mesh can be correctly filtered. - .. test:: Try to preserve the boundaries of fuel and control material. + .. test:: Preserve the boundaries of fuel and control material. :id: T_ARMI_UMC_NON_UNIFORM1 :tests: R_ARMI_UMC_NON_UNIFORM """ @@ -308,7 +308,7 @@ def test_generateCommonMesh(self): :id: T_ARMI_UMC_MIN_MESH :tests: R_ARMI_UMC_MIN_MESH - .. test:: Try to preserve the boundaries of fuel and control material. + .. test:: Preserve the boundaries of fuel and control material. :id: T_ARMI_UMC_NON_UNIFORM0 :tests: R_ARMI_UMC_NON_UNIFORM """ diff --git a/armi/reactor/converters/uniformMesh.py b/armi/reactor/converters/uniformMesh.py index 19da7fe4e..57fc2cd87 100644 --- a/armi/reactor/converters/uniformMesh.py +++ b/armi/reactor/converters/uniformMesh.py @@ -31,7 +31,8 @@ well as the multigroup real and adjoint flux. -.. warning: This procedure can cause numerical diffusion in some cases. For example, +.. warning:: + This procedure can cause numerical diffusion in some cases. For example, if a control rod tip block has a large coolant block below it, things like peak absorption rate can get lost into it. We recalculate some but not all reaction rates in the re-mapping process based on a flux remapping. To avoid this, @@ -108,7 +109,7 @@ def __init__(self, r, minimumMeshSize=None): Reactor for which a common mesh is generated minimumMeshSize : float, optional Minimum allowed separation between axial mesh points in cm - If no miminmum mesh size is provided, no "decusping" is performed + If no minimum mesh size is provided, no "decusping" is performed """ self._sourceReactor = r self.minimumMeshSize = minimumMeshSize diff --git a/armi/reactor/flags.py b/armi/reactor/flags.py index 4c9fb97dd..8ced3de02 100644 --- a/armi/reactor/flags.py +++ b/armi/reactor/flags.py @@ -287,7 +287,7 @@ def fromString(cls, typeSpec): """ Retrieve flag from a string. - .. impl:: Retrieve flag from a string + .. impl:: Retrieve flag from a string. :id: I_ARMI_FLAG_TO_STR0 :implements: R_ARMI_FLAG_TO_STR """ @@ -298,7 +298,7 @@ def toString(cls, typeSpec): """ Convert a flag to a string. - .. impl:: Convert a flag to string + .. impl:: Convert a flag to string. :id: I_ARMI_FLAG_TO_STR1 :implements: R_ARMI_FLAG_TO_STR """ diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py index a3f8a3bef..745d1f432 100644 --- a/armi/reactor/grids/hexagonal.py +++ b/armi/reactor/grids/hexagonal.py @@ -313,7 +313,7 @@ def overlapsWhichSymmetryLine(self, indices: IJType) -> Optional[int]: def getSymmetricEquivalents(self, indices: IJKType) -> List[IJType]: """Retrieve e quivalent contents based on 3rd symmetry. - .. impl:: Equivalent contents in 3rd geometry are retrievable + .. impl:: Equivalent contents in 3rd geometry are retrievable. :id: I_ARMI_GRID_EQUIVALENTS :implements: R_ARMI_GRID_EQUIVALENTS """ @@ -368,7 +368,7 @@ def locatorInDomain(self, locator, symmetryOverlap: Optional[bool] = False) -> b def isInFirstThird(self, locator, includeTopEdge=False) -> bool: """True if locator is in first third of hex grid. - .. impl:: Determine if grid in first third + .. impl:: Determine if grid is in first third. :id: I_ARMI_GRID_SYMMETRY_LOC :implements: R_ARMI_GRID_SYMMETRY_LOC """ diff --git a/armi/reactor/grids/tests/test_grids.py b/armi/reactor/grids/tests/test_grids.py index bb96cd84f..7311a9b50 100644 --- a/armi/reactor/grids/tests/test_grids.py +++ b/armi/reactor/grids/tests/test_grids.py @@ -304,7 +304,7 @@ def test_overlapsWhichSymmetryLine(self): def test_getSymmetricIdenticalsThird(self): """Retrieve equivalent contents based on 3rd symmetry. - .. test:: Equivalent contents in 3rd geometry are retrievable + .. test:: Equivalent contents in 3rd geometry are retrievable. :id: T_ARMI_GRID_EQUIVALENTS :tests: R_ARMI_GRID_EQUIVALENTS """ @@ -409,7 +409,7 @@ def test_badIndices(self): def test_isInFirstThird(self): """Determine if grid is in first third. - .. test:: Determine if grid in first third + .. test:: Determine if grid in first third. :id: T_ARMI_GRID_SYMMETRY_LOC :tests: R_ARMI_GRID_SYMMETRY_LOC """ diff --git a/armi/reactor/parameters/parameterDefinitions.py b/armi/reactor/parameters/parameterDefinitions.py index f3305d41c..2844dce0b 100644 --- a/armi/reactor/parameters/parameterDefinitions.py +++ b/armi/reactor/parameters/parameterDefinitions.py @@ -152,7 +152,7 @@ class Serializer: their version. It is also good practice, whenever possible, to support reading old versions so that database files written by old versions can still be read. - .. impl:: Custom parameter serializer + .. impl:: Users can define custom parameter serializers. :id: I_ARMI_PARAM_SERIALIZE :implements: R_ARMI_PARAM_SERIALIZE @@ -583,7 +583,7 @@ def toWriteToDB(self, assignedMask: Optional[int] = None): """ Get a list of acceptable parameters to store to the database for a level of the data model. - .. impl:: Filter parameters to write to DB + .. impl:: Filter parameters to write to DB. :id: I_ARMI_PARAM_DB :implements: R_ARMI_PARAM_DB diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index e24b017df..8e9824bca 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -172,7 +172,7 @@ def loadFromCs(cs) -> Reactor: """ Load a Reactor based on the input settings. - .. impl:: Create reactor from input yaml file + .. impl:: Users can create a reactor from an input yaml file. :id: I_ARMI_R_CORE :implements: R_ARMI_R_CORE @@ -778,7 +778,7 @@ def getNumRings(self, indexBased=False): """ Returns the number of rings in this reactor. Based on location so indexing will start at 1. - .. impl:: Retrieve number of rings in core + .. impl:: Retrieve number of rings in core. :id: I_ARMI_R_NUM_RINGS :implements: R_ARMI_R_NUM_RINGS @@ -1138,7 +1138,7 @@ def getAssemblyByName(self, name): """ Find the assembly that has this name. - .. impl:: Get assembly by name + .. impl:: Get assembly by name. :id: I_ARMI_R_GET_ASSEM_NAME :implements: R_ARMI_R_GET_ASSEM_NAME @@ -1655,7 +1655,7 @@ def getAssemblyWithAssemNum(self, assemNum): def getAssemblyWithStringLocation(self, locationString): """Returns an assembly or none if given a location string like 'B0014'. - .. impl:: Get assembly by location + .. impl:: Get assembly by location. :id: I_ARMI_R_GET_ASSEM_LOC :implements: R_ARMI_R_GET_ASSEM_LOC """ @@ -1689,7 +1689,7 @@ def findNeighbors( Return a list of neighboring assemblies from the 30 degree point (point 1) then counterclockwise around. - .. impl:: Retrieve neighboring assemblies of a given assembly + .. impl:: Retrieve neighboring assemblies of a given assembly. :id: I_ARMI_R_FIND_NEIGHBORS :implements: R_ARMI_R_FIND_NEIGHBORS diff --git a/armi/reactor/tests/test_assemblies.py b/armi/reactor/tests/test_assemblies.py index 7348a35d9..128789cb5 100644 --- a/armi/reactor/tests/test_assemblies.py +++ b/armi/reactor/tests/test_assemblies.py @@ -347,7 +347,7 @@ def test_getLocation(self): """ Test for getting string location of assembly. - .. test:: Assembly location is retrievable + .. test:: Assembly location is retrievable. :id: T_ARMI_ASSEM_POSI0 :tests: R_ARMI_ASSEM_POSI """ @@ -358,7 +358,7 @@ def test_getLocation(self): def test_getArea(self): """Tests area calculation for hex assembly. - .. test:: Assembly area is retrievable + .. test:: Assembly area is retrievable. :id: T_ARMI_ASSEM_DIMS0 :tests: R_ARMI_ASSEM_DIMS """ @@ -370,7 +370,7 @@ def test_getArea(self): def test_getVolume(self): """Tests volume calculation for hex assembly. - .. test:: Assembly volume is retrievable + .. test:: Assembly volume is retrievable. :id: T_ARMI_ASSEM_DIMS1 :tests: R_ARMI_ASSEM_DIMS """ @@ -454,7 +454,7 @@ def test_getHeight(self): """ Test height of assembly calculation. - .. test:: Assembly height is retrievable + .. test:: Assembly height is retrievable. :id: T_ARMI_ASSEM_DIMS2 :tests: R_ARMI_ASSEM_DIMS @@ -865,7 +865,7 @@ def test_countBlocksOfType(self): def test_getDim(self): """Tests dimensions are retrievable. - .. test:: Assembly dimensions are retrievable + .. test:: Assembly dimensions are retrievable. :id: T_ARMI_ASSEM_DIMS3 :tests: R_ARMI_ASSEM_DIMS """ @@ -1004,7 +1004,7 @@ def test_hasContinuousCoolantChannel(self): def test_carestianCoordinates(self): """Check the coordinates of the assembly within the core with a CarestianGrid. - .. test:: Cartesian coordinates are retrievable + .. test:: Cartesian coordinates are retrievable. :id: T_ARMI_ASSEM_POSI1 :tests: R_ARMI_ASSEM_POSI """ @@ -1103,7 +1103,7 @@ def test_rotate(self): def test_assem_block_types(self): """Test that all children of an assembly are blocks. - .. test:: Validate child types of assembly are blocks + .. test:: Validate child types of assembly are blocks. :id: T_ARMI_ASSEM_BLOCKS :tests: R_ARMI_ASSEM_BLOCKS """ @@ -1115,7 +1115,7 @@ def test_assem_block_types(self): def test_assem_hex_type(self): """Test that all children of a hex assembly are hexagons. - .. test:: Validate child types of assembly are Hex type + .. test:: Validate child types of assembly are Hex type. :id: T_ARMI_ASSEM_HEX :tests: R_ARMI_ASSEM_HEX """ diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py index 57afcdf48..b24c0d42d 100644 --- a/armi/reactor/tests/test_blocks.py +++ b/armi/reactor/tests/test_blocks.py @@ -476,7 +476,7 @@ def test_homogenizedMixture(self): """ Confirms homogenized blocks have correct properties. - .. test:: Homogenize the compositions of a block + .. test:: Homogenize the compositions of a block. :id: T_ARMI_BLOCK_HOMOG :tests: R_ARMI_BLOCK_HOMOG """ @@ -853,7 +853,7 @@ def test_setLocation(self): """ Retrieve a blocks location. - .. test:: Location of a block is retrievable + .. test:: Location of a block is retrievable. :id: T_ARMI_BLOCK_POSI0 :tests: R_ARMI_BLOCK_POSI """ @@ -1828,7 +1828,7 @@ def test_component_type(self): """ Test that a hex block has the proper "hexagon" __name__. - .. test:: Ability to create hex shaped blocks + .. test:: Users can create hex shaped blocks. :id: T_ARMI_BLOCK_HEX :tests: R_ARMI_BLOCK_HEX """ @@ -1839,7 +1839,7 @@ def test_coords(self): """ Test that coordinates are retrievable from a block. - .. test:: Coordinates of a block are queryable + .. test:: Coordinates of a block are queryable. :id: T_ARMI_BLOCK_POSI1 :tests: R_ARMI_BLOCK_POSI """ @@ -1871,7 +1871,7 @@ def test_block_dims(self): Tests that the block class can provide basic dimensionality information about itself. - .. test:: Retrieve important block dimensions + .. test:: Important block dimensions are retrievable. :id: T_ARMI_BLOCK_DIMS :tests: R_ARMI_BLOCK_DIMS """ diff --git a/armi/reactor/tests/test_components.py b/armi/reactor/tests/test_components.py index e6eddafe1..899deeea0 100644 --- a/armi/reactor/tests/test_components.py +++ b/armi/reactor/tests/test_components.py @@ -171,7 +171,7 @@ class TestComponent(TestGeneralComponents): def test_initializeComponentMaterial(self): """Creating component with single material. - .. test:: Components are made of one material + .. test:: Components are made of one material. :id: T_ARMI_COMP_1MAT0 :tests: R_ARMI_COMP_1MAT """ @@ -185,7 +185,7 @@ def test_initializeComponentMaterial(self): def test_setNumberDensity(self): """Test setting a single number density. - .. test:: Set Component number density + .. test:: Users can set Component number density. :id: T_ARMI_COMP_NUCLIDE_FRASCS0 :tests: R_ARMI_COMP_NUCLIDE_FRACS """ @@ -197,7 +197,7 @@ def test_setNumberDensity(self): def test_setNumberDensities(self): """Test setting multiple number densities. - .. test:: Set Component number density + .. test:: Users can set Component number densities. :id: T_ARMI_COMP_NUCLIDE_FRASCS1 :tests: R_ARMI_COMP_NUCLIDE_FRACS """ @@ -210,7 +210,7 @@ def test_setNumberDensities(self): def test_solid_material(self): """Determine if material is solid. - .. test:: Determine if material is solid + .. test:: Determine if material is solid. :id: T_ARMI_COMP_SOLID :tests: R_ARMI_COMP_SOLID """ @@ -237,7 +237,7 @@ def test_nonzero(self): def test_getDimension(self): """Test getting empty component. - .. test:: Retrieve a null dimension + .. test:: Retrieve a null dimension. :id: T_ARMI_COMP_DIMS0 :tests: R_ARMI_COMP_DIMS """ @@ -339,7 +339,7 @@ def test_volumeAfterClearCache(self): """ Test volume after cache has been cleared. - .. test:: Clear cache after a dimensions updated + .. test:: Clear cache after a dimensions updated. :id: T_ARMI_COMP_VOL0 :tests: R_ARMI_COMP_VOL """ @@ -443,7 +443,7 @@ def test_getThermalExpansionFactorConservedMassByLinearExpansionPercent(self): def test_getDimension(self): """Test getting component dimension at specific temperature. - .. test:: Retrieve a dimension at a temperature + .. test:: Retrieve a dimension at a temperature. :id: T_ARMI_COMP_DIMS1 :tests: R_ARMI_COMP_DIMS """ @@ -768,7 +768,7 @@ class TestTriangle(TestShapedComponent): def test_getArea(self): """Calculate area of triangle. - .. test:: Calculate area of triangle + .. test:: Calculate area of triangle. :id: T_ARMI_COMP_VOL2 :tests: R_ARMI_COMP_VOL @@ -847,7 +847,7 @@ def test_getCircleInnerDiameter(self): def test_getArea(self): """Calculate area of rectangle. - .. test:: Calculate area of rectangle + .. test:: Calculate area of rectangle. :id: T_ARMI_COMP_VOL3 :tests: R_ARMI_COMP_VOL """ @@ -1501,7 +1501,7 @@ def test_updateDims(self): """ Test Update dimensions. - .. test:: Dimensions can be updated + .. test:: Dimensions can be updated. :id: T_ARMI_COMP_VOL13 :tests: R_ARMI_COMP_VOL """ diff --git a/armi/reactor/tests/test_composites.py b/armi/reactor/tests/test_composites.py index 6655e96fb..31f8d3f54 100644 --- a/armi/reactor/tests/test_composites.py +++ b/armi/reactor/tests/test_composites.py @@ -220,7 +220,7 @@ def test_nucSpec(self): def test_hasFlags(self): """Ensure flags are queryable. - .. test:: Flags can be queried + .. test:: Flags can be queried. :id: T_ARMI_CMP_HAS_FLAGS :tests: R_ARMI_CMP_HAS_FLAGS """ @@ -616,7 +616,7 @@ def setUp(self): def test_setMass(self): """Test setting and retrieving mass. - .. test:: Get mass of composite + .. test:: Mass of a composite is retrievable. :id: T_ARMI_CMP_GET_MASS :tests: R_ARMI_CMP_GET_MASS """ @@ -639,7 +639,7 @@ def test_setMass(self): def test_getNumberDensities(self): """Get number densities from composite. - .. test:: Number density of composite is retrievable + .. test:: Number density of composite is retrievable. :id: T_ARMI_CMP_GET_NDENS0 :tests: R_ARMI_CMP_GET_NDENS """ diff --git a/armi/reactor/tests/test_flags.py b/armi/reactor/tests/test_flags.py index 57ba77a7a..bbc4c1cb8 100644 --- a/armi/reactor/tests/test_flags.py +++ b/armi/reactor/tests/test_flags.py @@ -30,7 +30,7 @@ def test_flagsToAndFromString(self): """ Convert flag to and from string for serialization. - .. test:: Convert flag to a string + .. test:: Convert flag to a string. :id: T_ARMI_FLAG_TO_STR :tests: R_ARMI_FLAG_TO_STR """ diff --git a/armi/reactor/tests/test_parameters.py b/armi/reactor/tests/test_parameters.py index c2c01ca8f..2302d4fb5 100644 --- a/armi/reactor/tests/test_parameters.py +++ b/armi/reactor/tests/test_parameters.py @@ -73,7 +73,7 @@ def test_writeSomeParamsToDB(self): ParameterDefinitionCollection.toWriteToDB() is used to filter for which parameters to include in the database. - .. test:: Restrict parameters from DB write + .. test:: Restrict parameters from DB write. :id: T_ARMI_PARAM_DB :tests: R_ARMI_PARAM_DB """ diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index e933b05b5..9a3aabac9 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -260,7 +260,7 @@ def test_factorySortSetting(self): """ Create a core object from an input yaml. - .. test:: Create core object from input yaml + .. test:: Create core object from input yaml. :id: T_ARMI_R_CORE :tests: R_ARMI_R_CORE """ @@ -290,11 +290,11 @@ def test_getSetParameters(self): This test works through multiple levels of the hierarchy to test ability to modify parameters at different levels. - .. test:: Parameters accessible throughout the armi tree + .. test:: Parameters are accessible throughout the armi tree. :id: T_ARMI_PARAM_PART :tests: R_ARMI_PARAM_PART - .. impl:: Prove there is a setting for total core power. + .. impl:: Ensure there is a setting for total core power. :id: T_ARMI_SETTINGS_POWER :implements: R_ARMI_SETTINGS_POWER """ @@ -579,7 +579,7 @@ def test_findNeighbors(self): """ Find neighbors of a given assembly. - .. test:: Retrieve neighboring assemblies of a given assembly + .. test:: Retrieve neighboring assemblies of a given assembly. :id: T_ARMI_R_FIND_NEIGHBORS :tests: R_ARMI_R_FIND_NEIGHBORS """ @@ -716,7 +716,7 @@ def test_getAssemblyWithLoc(self): """ Get assembly by location. - .. test:: Get assembly by location + .. test:: Get assembly by location. :id: T_ARMI_R_GET_ASSEM_LOC :tests: R_ARMI_R_GET_ASSEM_LOC """ @@ -729,7 +729,7 @@ def test_getAssemblyWithName(self): """ Get assembly by name. - .. test:: Get assembly by name + .. test:: Get assembly by name. :id: T_ARMI_R_GET_ASSEM_NAME :tests: R_ARMI_R_GET_ASSEM_NAME """ diff --git a/armi/reactor/tests/test_zones.py b/armi/reactor/tests/test_zones.py index 085a7dc7c..4ded16430 100644 --- a/armi/reactor/tests/test_zones.py +++ b/armi/reactor/tests/test_zones.py @@ -75,7 +75,7 @@ def test_addItem(self): """ Test adding an item. - .. test:: Add item to a zone + .. test:: Add item to a zone. :id: T_ARMI_ZONE0 :tests: R_ARMI_ZONE """ @@ -96,7 +96,7 @@ def test_addItems(self): """ Test adding items. - .. test:: Add multiple items to a zone + .. test:: Add multiple items to a zone. :id: T_ARMI_ZONE1 :tests: R_ARMI_ZONE """ @@ -115,7 +115,7 @@ def test_addLoc(self): """ Test adding a location. - .. test:: Add location to a zone + .. test:: Add location to a zone. :id: T_ARMI_ZONE2 :tests: R_ARMI_ZONE """ @@ -136,7 +136,7 @@ def test_addLocs(self): """ Test adding locations. - .. test:: Add multiple locations to a zone + .. test:: Add multiple locations to a zone. :id: T_ARMI_ZONE3 :tests: R_ARMI_ZONE """ @@ -210,7 +210,7 @@ def test_dictionaryInterface(self): """ Test creating and interacting with the Zones object. - .. test:: Create collection of Zones + .. test:: Create collection of Zones. :id: T_ARMI_ZONES :tests: R_ARMI_ZONES """ diff --git a/armi/reactor/zones.py b/armi/reactor/zones.py index c5b16fd50..8a487bea2 100644 --- a/armi/reactor/zones.py +++ b/armi/reactor/zones.py @@ -29,7 +29,7 @@ class Zone: A group of locations in the Core, used to divide it up for analysis. Each location represents an Assembly or a Block. - .. impl:: A collection of armi locations + .. impl:: A user can define a collection of armi locations. :id: I_ARMI_ZONE :implements: R_ARMI_ZONE """ @@ -202,7 +202,7 @@ def removeItems(self, items: List) -> None: class Zones: """Collection of Zone objects. - .. impl:: A collection of armi zones + .. impl:: A user can define a collection of armi zones. :id: I_ARMI_ZONES :implements: R_ARMI_ZONES """ diff --git a/armi/utils/densityTools.py b/armi/utils/densityTools.py index 91cb90f87..5f98e20f9 100644 --- a/armi/utils/densityTools.py +++ b/armi/utils/densityTools.py @@ -24,7 +24,7 @@ def getNDensFromMasses(rho, massFracs, normalize=False): """ Convert density (g/cc) and massFracs vector into a number densities vector (#/bn-cm). - .. impl:: Get number densities + .. impl:: Number densities are retrievable from masses. :id: I_ARMI_UTIL_MASS2N_DENS :implements: R_ARMI_UTIL_MASS2N_DENS @@ -172,7 +172,7 @@ def formatMaterialCard( """ Formats nuclides and densities into a MCNP material card. - .. impl:: Create MCNP material card + .. impl:: Create MCNP material card. :id: I_ARMI_UTIL_MCNP_MAT_CARD :implements: R_ARMI_UTIL_MCNP_MAT_CARD @@ -262,7 +262,7 @@ def normalizeNuclideList(nuclideVector, normalization=1.0): """ Normalize the nuclide vector. - .. impl:: Normalize nuclide vector + .. impl:: Normalize nuclide vector. :id: I_ARMI_UTIL_DENS_TOOLS :implements: R_ARMI_UTIL_DENS_TOOLS @@ -299,7 +299,7 @@ def expandElementalMassFracsToNuclides( ----- This indirectly updates number densities through mass fractions. - .. impl:: Expand mass fractions to nuclides + .. impl:: Expand mass fractions to nuclides. :id: I_ARMI_UTIL_EXP_MASS_FRACS :implements: R_ARMI_UTIL_EXP_MASS_FRACS diff --git a/armi/utils/flags.py b/armi/utils/flags.py index d89e2b65a..76ffd2597 100644 --- a/armi/utils/flags.py +++ b/armi/utils/flags.py @@ -123,7 +123,7 @@ class Flag(metaclass=_FlagMeta): after the class has been defined. Most docs for ``enum.Flag`` should be relevant here, but there are sure to be occasional differences. - .. impl:: No two flags have equivalence + .. impl:: No two flags have equivalence. :id: I_ARMI_FLAG_DEFINE :implements: R_ARMI_FLAG_DEFINE @@ -220,7 +220,7 @@ def extend(cls, fields: Dict[str, Union[int, auto]]): This alters the class that it is called upon! Existing instances should see the new data, since classes are mutable. - .. impl:: Set of flags are extensible without loss of uniqueness + .. impl:: Set of flags are extensible without loss of uniqueness. :id: I_ARMI_FLAG_EXTEND :implements: R_ARMI_FLAG_EXTEND diff --git a/armi/utils/tests/test_densityTools.py b/armi/utils/tests/test_densityTools.py index dbe89cc0d..0727d0949 100644 --- a/armi/utils/tests/test_densityTools.py +++ b/armi/utils/tests/test_densityTools.py @@ -24,7 +24,7 @@ def test_expandElementalMassFracsToNuclides(self): """ Expand mass fraction to nuclides. - .. test:: Expand mass fractions to nuclides + .. test:: Expand mass fractions to nuclides. :id: T_ARMI_UTIL_EXP_MASS_FRACS :tests: R_ARMI_UTIL_EXP_MASS_FRACS """ @@ -110,7 +110,7 @@ def test_getNDensFromMasses(self): """ Number densities from masses. - .. test:: Get number densities + .. test:: Number densities are retrievable from masses. :id: T_ARMI_UTIL_MASS2N_DENS :tests: R_ARMI_UTIL_MASS2N_DENS """ diff --git a/armi/utils/tests/test_flags.py b/armi/utils/tests/test_flags.py index 8054dcc13..a6b0238fe 100644 --- a/armi/utils/tests/test_flags.py +++ b/armi/utils/tests/test_flags.py @@ -72,7 +72,7 @@ class F(Flag): def test_collision_extension(self): """Ensure the set of flags cannot be programmatically extended if duplicate created. - .. test:: Set of flags are extensible without loss of uniqueness + .. test:: Set of flags are extensible without loss of uniqueness. :id: T_ARMI_FLAG_EXTEND :tests: R_ARMI_FLAG_EXTEND """ @@ -88,7 +88,7 @@ class F(Flag): def test_collision_creation(self): """Make sure that we catch value collisions upon creation. - .. test:: No two flags have equivalence + .. test:: No two flags have equivalence. :id: T_ARMI_FLAG_DEFINE :tests: R_ARMI_FLAG_DEFINE """ diff --git a/armi/utils/tests/test_hexagon.py b/armi/utils/tests/test_hexagon.py index 2e8a8fa1d..5d081aebb 100644 --- a/armi/utils/tests/test_hexagon.py +++ b/armi/utils/tests/test_hexagon.py @@ -23,7 +23,7 @@ def test_hexagon_area(self): """ Area of a hexagon. - .. test:: Compute hexagonal area + .. test:: Hexagonal area is retrievable. :id: T_ARMI_UTIL_HEXAGON0 :test: R_ARMI_UTIL_HEXAGON """ @@ -35,7 +35,7 @@ def test_numPositionsInRing(self): """ Calculate number of positions in a ring of hexagons. - .. test:: Compute number of positions in ring + .. test:: Compute number of positions in ring. :id: T_ARMI_UTIL_HEXAGON1 :tests: R_ARMI_UTIL_HEXAGON """ From f5681d7162b100fa74e290f9a0f7c5e807076a54 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Wed, 22 Nov 2023 11:18:39 -0800 Subject: [PATCH 054/176] Adding requirement crumbs for Materials and Composites (#1487) --- armi/materials/tests/test_materials.py | 49 ++----------- armi/materials/tests/test_uZr.py | 99 +++++++++++++++++++++++++- armi/reactor/components/component.py | 20 ++++++ armi/reactor/composites.py | 35 ++++++++- armi/reactor/grids/grid.py | 2 - armi/reactor/tests/test_blocks.py | 25 +++++-- armi/reactor/tests/test_components.py | 26 ++++++- armi/reactor/tests/test_composites.py | 57 ++++++++++++++- armi/reactor/tests/test_reactors.py | 6 +- 9 files changed, 260 insertions(+), 59 deletions(-) diff --git a/armi/materials/tests/test_materials.py b/armi/materials/tests/test_materials.py index e14683696..f597983bc 100644 --- a/armi/materials/tests/test_materials.py +++ b/armi/materials/tests/test_materials.py @@ -35,12 +35,7 @@ def setUp(self): self.mat = self.MAT_CLASS() def test_isPicklable(self): - """Test that all materials are picklable so we can do MPI communication of state. - - .. test:: Test the material base class. - :id: T_ARMI_MAT_PROPERTIES0 - :tests: R_ARMI_MAT_PROPERTIES - """ + """Test that all materials are picklable so we can do MPI communication of state.""" stream = pickle.dumps(self.mat) mat = pickle.loads(stream) @@ -50,21 +45,11 @@ def test_isPicklable(self): ) def test_density(self): - """Test that all materials produce a zero density from density. - - .. test:: Test the material base class. - :id: T_ARMI_MAT_PROPERTIES1 - :tests: R_ARMI_MAT_PROPERTIES - """ + """Test that all materials produce a zero density from density.""" self.assertNotEqual(self.mat.density(500), 0) def test_TD(self): - """Test the material density. - - .. test:: Test the material base class. - :id: T_ARMI_MAT_PROPERTIES2 - :tests: R_ARMI_MAT_PROPERTIES - """ + """Test the material density.""" self.assertEqual(self.mat.getTD(), self.mat.theoreticalDensityFrac) self.mat.clearCache() @@ -75,12 +60,7 @@ def test_TD(self): self.assertEqual(self.mat.cached, {}) def test_duplicate(self): - """Test the material duplication. - - .. test:: Test the material base class. - :id: T_ARMI_MAT_PROPERTIES3 - :tests: R_ARMI_MAT_PROPERTIES - """ + """Test the material duplication.""" mat = self.mat.duplicate() self.assertEqual(len(mat.massFrac), len(self.mat.massFrac)) @@ -92,12 +72,7 @@ def test_duplicate(self): self.assertEqual(mat.theoreticalDensityFrac, self.mat.theoreticalDensityFrac) def test_cache(self): - """Test the material cache. - - .. test:: Test the material base class. - :id: T_ARMI_MAT_PROPERTIES4 - :tests: R_ARMI_MAT_PROPERTIES - """ + """Test the material cache.""" self.mat.clearCache() self.assertEqual(len(self.mat.cached), 0) @@ -108,23 +83,13 @@ def test_cache(self): self.assertEqual(val, "Noether") def test_densityKgM3(self): - """Test the density for kg/m^3. - - .. test:: Test the material base class. - :id: T_ARMI_MAT_PROPERTIES5 - :tests: R_ARMI_MAT_PROPERTIES - """ + """Test the density for kg/m^3.""" dens = self.mat.density(500) densKgM3 = self.mat.densityKgM3(500) self.assertEqual(dens * 1000.0, densKgM3) def test_pseudoDensityKgM3(self): - """Test the pseudo density for kg/m^3. - - .. test:: Test the material base class. - :id: T_ARMI_MAT_PROPERTIES6 - :tests: R_ARMI_MAT_PROPERTIES - """ + """Test the pseudo density for kg/m^3.""" dens = self.mat.pseudoDensity(500) densKgM3 = self.mat.pseudoDensityKgM3(500) self.assertEqual(dens * 1000.0, densKgM3) diff --git a/armi/materials/tests/test_uZr.py b/armi/materials/tests/test_uZr.py index c1ecfaa79..d6072b4f5 100644 --- a/armi/materials/tests/test_uZr.py +++ b/armi/materials/tests/test_uZr.py @@ -13,16 +13,109 @@ # limitations under the License. """Tests for simplified UZr material.""" -import unittest +from unittest import TestCase +import pickle -from armi.materials.tests import test_materials from armi.materials.uZr import UZr -class UZR_TestCase(test_materials._Material_Test, unittest.TestCase): +class UZR_TestCase(TestCase): + MAT_CLASS = UZr + def setUp(self): + self.mat = self.MAT_CLASS() + + def test_isPicklable(self): + """Test that materials are picklable so we can do MPI communication of state. + + .. test:: Test the material base class has temp-dependent thermal conductivity curves. + :id: T_ARMI_MAT_PROPERTIES0 + :tests: R_ARMI_MAT_PROPERTIES + """ + stream = pickle.dumps(self.mat) + mat = pickle.loads(stream) + + # check a property that is sometimes interpolated. + self.assertEqual( + self.mat.thermalConductivity(500), mat.thermalConductivity(500) + ) + + def test_TD(self): + """Test the material theoretical density. + + .. test:: Test the material base class has temp-dependent TD curves. + :id: T_ARMI_MAT_PROPERTIES2 + :tests: R_ARMI_MAT_PROPERTIES + """ + self.assertEqual(self.mat.getTD(), self.mat.theoreticalDensityFrac) + + self.mat.clearCache() + self.mat._setCache("dummy", 666) + self.assertEqual(self.mat.cached, {"dummy": 666}) + self.mat.adjustTD(0.5) + self.assertEqual(0.5, self.mat.theoreticalDensityFrac) + self.assertEqual(self.mat.cached, {}) + + def test_duplicate(self): + """Test the material duplication. + + .. test:: Materials shall calc mass fracs at init. + :id: T_ARMI_MAT_FRACS + :tests: R_ARMI_MAT_FRACS + """ + mat = self.mat.duplicate() + + self.assertEqual(len(mat.massFrac), len(self.mat.massFrac)) + for key in self.mat.massFrac: + self.assertEqual(mat.massFrac[key], self.mat.massFrac[key]) + + self.assertEqual(mat.parent, self.mat.parent) + self.assertEqual(mat.refDens, self.mat.refDens) + self.assertEqual(mat.theoreticalDensityFrac, self.mat.theoreticalDensityFrac) + + def test_cache(self): + """Test the material cache.""" + self.mat.clearCache() + self.assertEqual(len(self.mat.cached), 0) + + self.mat._setCache("Emmy", "Noether") + self.assertEqual(len(self.mat.cached), 1) + + val = self.mat._getCached("Emmy") + self.assertEqual(val, "Noether") + + def test_densityKgM3(self): + """Test the density for kg/m^3. + + .. test:: Test the material base class has temp-dependent density. + :id: T_ARMI_MAT_PROPERTIES5 + :tests: R_ARMI_MAT_PROPERTIES + """ + dens = self.mat.density(500) + densKgM3 = self.mat.densityKgM3(500) + self.assertEqual(dens * 1000.0, densKgM3) + + def test_pseudoDensityKgM3(self): + """Test the pseudo density for kg/m^3. + + .. test:: Test the material base class has temp-dependent 2D density. + :id: T_ARMI_MAT_PROPERTIES6 + :tests: R_ARMI_MAT_PROPERTIES + """ + dens = self.mat.pseudoDensity(500) + densKgM3 = self.mat.pseudoDensityKgM3(500) + self.assertEqual(dens * 1000.0, densKgM3) + def test_density(self): + """Test that all materials produce a zero density from density. + + .. test:: Test the material base class has temp-dependent density. + :id: T_ARMI_MAT_PROPERTIES1 + :tests: R_ARMI_MAT_PROPERTIES + """ + self.assertNotEqual(self.mat.density(500), 0) + cur = self.mat.density(400) ref = 15.94 delta = ref * 0.01 diff --git a/armi/reactor/components/component.py b/armi/reactor/components/component.py index 49a1f5c4d..e8202a13c 100644 --- a/armi/reactor/components/component.py +++ b/armi/reactor/components/component.py @@ -173,6 +173,10 @@ class Component(composites.Composite, metaclass=ComponentType): :id: I_ARMI_COMP_DEF :implements: R_ARMI_COMP_DEF + .. impl:: Order components by there outermost diameter (using the < operator). + :id: I_ARMI_COMP_ORDER + :implements: R_ARMI_COMP_ORDER + Attributes ---------- temperatureInC : float @@ -387,6 +391,10 @@ def getProperties(self): .. impl:: Material properties are retrievable. :id: I_ARMI_COMP_MAT0 :implements: R_ARMI_COMP_MAT + + .. impl:: Components have one-and-only-one material. + :id: I_ARMI_COMP_1MAT + :implements: R_ARMI_COMP_1MAT """ return self.material @@ -658,6 +666,10 @@ def setNumberDensity(self, nucName, val): """ Set heterogeneous number density. + .. impl:: Setting nuclide fractions. + :id: I_ARMI_COMP_NUCLIDE_FRACS0 + :implements: R_ARMI_COMP_NUCLIDE_FRACS + Parameters ---------- nucName : str @@ -676,6 +688,10 @@ def setNumberDensities(self, numberDensities): """ Set one or more multiple number densities. Clears out any number density not listed. + .. impl:: Setting nuclide fractions. + :id: I_ARMI_COMP_NUCLIDE_FRACS1 + :implements: R_ARMI_COMP_NUCLIDE_FRACS + Parameters ---------- numberDensities : dict @@ -888,6 +904,10 @@ def getThermalExpansionFactor(self, Tc=None, T0=None): """ Retrieves the material thermal expansion fraction. + .. impl:: Calculates radial thermal expansion factor. + :id: I_ARMI_COMP_EXPANSION + :implements: R_ARMI_COMP_EXPANSION + Parameters ---------- Tc : float, optional diff --git a/armi/reactor/composites.py b/armi/reactor/composites.py index 40cfc9c19..480e4463a 100644 --- a/armi/reactor/composites.py +++ b/armi/reactor/composites.py @@ -624,6 +624,10 @@ def getParameterCollection(cls): created and associated. So, we use this top-level class method to dig dynamically down to the underlying parameter collection type. + .. impl:: Composites (and all ARMI objects) have parameter collections. + :id: I_ARMI_CMP_PARAMS + :implements: R_ARMI_CMP_PARAMS + See Also -------- :py:meth:`armi.reactor.parameters.parameterCollections.ParameterCollection.__reduce__` @@ -669,8 +673,8 @@ def hasFlags(self, typeID: TypeSpec, exact=False): Determine if this object is of a certain type. .. impl:: Flags can be queried. - :id: I_ARMI_CMP_HAS_FLAGS - :implements: R_ARMI_CMP_HAS_FLAGS + :id: I_ARMI_CMP_FLAG + :implements: R_ARMI_CMP_FLAG Parameters ---------- @@ -1224,6 +1228,10 @@ def getNumberDensity(self, nucName): """ Return the number density of a nuclide in atoms/barn-cm. + .. impl:: Get number density for a specific nuclide + :id: I_ARMI_CMP_NUC0 + :implements: R_ARMI_CMP_NUC + Notes ----- This can get called very frequently and has to do volume computations so should @@ -1239,7 +1247,12 @@ def getNumberDensity(self, nucName): return self.getNuclideNumberDensities([nucName])[0] def getNuclideNumberDensities(self, nucNames): - """Return a list of number densities in atoms/barn-cm for the nuc names requested.""" + """Return a list of number densities in atoms/barn-cm for the nuc names requested. + + .. impl:: Get number densities for specific nuclides. + :id: I_ARMI_CMP_NUC1 + :implements: R_ARMI_CMP_NUC + """ volumes = numpy.array( [ c.getVolume() / (c.parent.getSymmetryFactor() if c.parent else 1.0) @@ -2435,6 +2448,10 @@ def getComponentByName(self, name): """ Gets a particular component from this object, based on its name. + .. impl:: Get child component by name. + :id: I_ARMI_CMP_BY_NAME + :implements: R_ARMI_CMP_BY_NAME + Parameters ---------- name : str @@ -2646,6 +2663,10 @@ class Composite(ArmiObject): mixed with siblings in a grid. This allows mixing grid-representation with explicit representation, often useful in advanced assemblies and thermal reactors. + + .. impl:: Composites are a physical part of the reactor in a hierarchical data model. + :id: I_ARMI_CMP + :implements: R_ARMI_CMP """ def __init__(self, name): @@ -2751,6 +2772,10 @@ def getChildren( """ Return the children objects of this composite. + .. impl:: Composites have children, in the hierarchical data model. + :id: I_ARMI_CMP_CHILDREN + :implements: R_ARMI_CMP_CHILDREN + Parameters ---------- deep : boolean, optional @@ -2865,6 +2890,10 @@ def syncMpiState(self): In parallelized runs, if each process has its own copy of the entire reactor hierarchy, this method synchronizes the state of all parameters on all objects. + .. impl:: Composites can be synchronized across MPI threads. + :id: I_ARMI_CMP_MPI + :implements: R_ARMI_CMP_MPI + Returns ------- int diff --git a/armi/reactor/grids/grid.py b/armi/reactor/grids/grid.py index 7d509eda4..8e33bdec4 100644 --- a/armi/reactor/grids/grid.py +++ b/armi/reactor/grids/grid.py @@ -183,7 +183,6 @@ def overlapsWhichSymmetryLine(self, indices: IJType) -> Optional[int]: None if not line of symmetry goes through the object at the requested index. Otherwise, some grid constants like ``BOUNDARY_CENTER`` will be returned. - """ @abstractmethod @@ -244,5 +243,4 @@ def reduce(self) -> Tuple[Hashable, ...]: Notes ----- For consistency, the second to last argument **must** be the geomType - """ diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py index b24c0d42d..a540ce4b6 100644 --- a/armi/reactor/tests/test_blocks.py +++ b/armi/reactor/tests/test_blocks.py @@ -23,7 +23,6 @@ from armi import materials, runLog, settings, tests from armi.reactor import blueprints -from armi.reactor.blueprints.tests.test_blockBlueprints import FULL_BP from armi.reactor.components import basicShapes, complexShapes from armi.nucDirectory import nucDir, nuclideBases from armi.nuclearDataIO.cccc import isotxs @@ -306,7 +305,9 @@ def getComponentData(component): class TestDetailedNDensUpdate(unittest.TestCase): - def setUp(self): + def test_updateDetailedNdens(self): + from armi.reactor.blueprints.tests.test_blockBlueprints import FULL_BP + cs = settings.Settings() with io.StringIO(FULL_BP) as stream: bps = blueprints.Blueprints.load(stream) @@ -317,7 +318,6 @@ def setUp(self): a.add(buildSimpleFuelBlock()) self.r.core.add(a) - def test_updateDetailedNdens(self): # get first block in assembly with 'fuel' key block = self.r.core[0][0] # get nuclides in first component in block @@ -1237,13 +1237,24 @@ def test_getComponentsOfMaterial(self): ) def test_getComponentByName(self): + """Test children by name. + + .. test:: Get children by name. + :id: I_ARMI_CMP_BY_NAME0 + :tests: R_ARMI_CMP_BY_NAME + """ self.assertIsNone( self.block.getComponentByName("not the droid youre looking for") ) self.assertIsNotNone(self.block.getComponentByName("annular void")) def test_getSortedComponentsInsideOfComponent(self): - """Test that components can be sorted within a block and returned in the correct order.""" + """Test that components can be sorted within a block and returned in the correct order. + + .. test:: Get children by name. + :id: I_ARMI_CMP_BY_NAME1 + :tests: R_ARMI_CMP_BY_NAME + """ expected = [ self.block.getComponentByName(c) for c in [ @@ -1262,6 +1273,12 @@ def test_getSortedComponentsInsideOfComponent(self): self.assertListEqual(actual, expected) def test_getSortedComponentsInsideOfComponentSpecifiedTypes(self): + """Test that components can be sorted within a block and returned in the correct order. + + .. test:: Get children by name. + :id: I_ARMI_CMP_BY_NAME2 + :tests: R_ARMI_CMP_BY_NAME + """ expected = [ self.block.getComponentByName(c) for c in [ diff --git a/armi/reactor/tests/test_components.py b/armi/reactor/tests/test_components.py index 899deeea0..1c16b8cb6 100644 --- a/armi/reactor/tests/test_components.py +++ b/armi/reactor/tests/test_components.py @@ -174,6 +174,10 @@ def test_initializeComponentMaterial(self): .. test:: Components are made of one material. :id: T_ARMI_COMP_1MAT0 :tests: R_ARMI_COMP_1MAT + + .. test:: Define a component. + :id: T_ARMI_COMP_DEF0 + :tests: R_ARMI_COMP_DEF """ expectedName = "TestComponent" actualName = self.component.getName() @@ -225,6 +229,12 @@ class TestNullComponent(TestGeneralComponents): componentCls = NullComponent def test_cmp(self): + """Test null component. + + .. test:: Define a component. + :id: T_ARMI_COMP_DEF1 + :tests: R_ARMI_COMP_DEF + """ cur = self.component ref = DerivedShape("DerivedShape", "Material", 0, 0) self.assertLess(cur, ref) @@ -431,6 +441,10 @@ def test_getThermalExpansionFactorConservedMassByLinearExpansionPercent(self): .. test:: Circle shaped component :id: T_ARMI_COMP_SHAPES0 :tests: R_ARMI_COMP_SHAPES + + .. test:: Calculate thermal expansion. + :id: I_ARMI_COMP_EXPANSION0 + :tests: R_ARMI_COMP_EXPANSION """ hotTemp = 700.0 dLL = self.component.material.linearExpansionFactor( @@ -446,6 +460,10 @@ def test_getDimension(self): .. test:: Retrieve a dimension at a temperature. :id: T_ARMI_COMP_DIMS1 :tests: R_ARMI_COMP_DIMS + + .. test:: Calculate thermal expansion. + :id: I_ARMI_COMP_EXPANSION1 + :tests: R_ARMI_COMP_EXPANSION """ hotTemp = 700.0 ref = self._od * self.component.getThermalExpansionFactor(Tc=hotTemp) @@ -528,7 +546,7 @@ def test_badComponentName(self): _gap = Circle("gap", "Void", **gapDims) def test_componentInteractionsLinkingBySubtraction(self): - r"""Tests linking of components by subtraction.""" + """Tests linking of components by subtraction.""" nPins = 217 gapDims = {"Tinput": 25.0, "Thot": 430.0, "od": 1.0, "id": 0.9, "mult": nPins} gap = Circle("gap", "Void", **gapDims) @@ -889,6 +907,12 @@ class TestSolidRectangle(TestShapedComponent): } def test_getBoundingCircleOuterDiameter(self): + """Test get bounding circle of the outer diameter. + + .. test:: Define a component. + :id: T_ARMI_COMP_DEF2 + :tests: R_ARMI_COMP_DEF + """ ref = math.sqrt(50) cur = self.component.getBoundingCircleOuterDiameter(cold=True) self.assertAlmostEqual(ref, cur) diff --git a/armi/reactor/tests/test_composites.py b/armi/reactor/tests/test_composites.py index 31f8d3f54..6b127d1ac 100644 --- a/armi/reactor/tests/test_composites.py +++ b/armi/reactor/tests/test_composites.py @@ -110,7 +110,17 @@ def setUp(self): container.add(nested) self.container = container - def test_Composite(self): + def test_composite(self): + """Test basic Composite things. + + .. test:: Composites are a physical item. + :id: T_ARMI_CMP0 + :tests: R_ARMI_CMP + + .. test:: Composites are part of a hierarchical model. + :id: T_ARMI_CMP_CHILDREN0 + :tests: R_ARMI_CMP_CHILDREN + """ container = self.container children = container.getChildren() @@ -124,6 +134,16 @@ def test_iterComponents(self): self.assertIn(self.thirdGen, list(self.container.iterComponents())) def test_getChildren(self): + """Test the get children method. + + .. test:: Composites are a physical part of the reactor. + :id: T_ARMI_CMP1 + :tests: R_ARMI_CMP + + .. test:: Composites are part of a hierarchical model. + :id: T_ARMI_CMP_CHILDREN1 + :tests: R_ARMI_CMP_CHILDREN + """ # There are 5 leaves and 1 composite in container. The composite has one leaf. firstGen = self.container.getChildren() self.assertEqual(len(firstGen), 6) @@ -141,6 +161,18 @@ def test_getChildren(self): ) self.assertEqual(len(onlyLiner), 1) + def test_getName(self): + """Test the getName method. + + .. test:: Composites names should be accessible. + :id: T_ARMI_CMP_GET_NAME + :tests: R_ARMI_CMP_GET_NAME + """ + self.assertEqual(self.secondGen.getName(), "liner") + self.assertEqual(self.thirdGen.getName(), "pin 77") + self.assertEqual(self.secondGen.getName(), "liner") + self.assertEqual(self.container.getName(), "inner test fuel") + def test_sort(self): # in this case, the children should start sorted c0 = [c.name for c in self.container.getChildren()] @@ -221,8 +253,8 @@ def test_hasFlags(self): """Ensure flags are queryable. .. test:: Flags can be queried. - :id: T_ARMI_CMP_HAS_FLAGS - :tests: R_ARMI_CMP_HAS_FLAGS + :id: T_ARMI_CMP_FLAG + :tests: R_ARMI_CMP_FLAG """ self.container.setType("fuel") self.assertFalse(self.container.hasFlags(Flags.SHIELD | Flags.FUEL, exact=True)) @@ -522,6 +554,12 @@ def test_getFissileMass(self): self.assertAlmostEqual(cur, ref, places=places) def test_getMaxParam(self): + """Test getMaxParam(). + + .. test:: Composites have parameter collections. + :id: T_ARMI_CMP_PARAMS0 + :tests: R_ARMI_CMP_PARAMS + """ for ci, c in enumerate(self.Block): if isinstance(c, basicShapes.Circle): c.p.id = ci @@ -532,6 +570,12 @@ def test_getMaxParam(self): self.assertIs(comp, lastSeen) def test_getMinParam(self): + """Test getMinParam(). + + .. test:: Composites have parameter collections. + :id: T_ARMI_CMP_PARAMS1 + :tests: R_ARMI_CMP_PARAMS + """ for ci, c in reversed(list(enumerate(self.Block))): if isinstance(c, basicShapes.Circle): c.p.id = ci @@ -642,11 +686,18 @@ def test_getNumberDensities(self): .. test:: Number density of composite is retrievable. :id: T_ARMI_CMP_GET_NDENS0 :tests: R_ARMI_CMP_GET_NDENS + + .. test:: Get number densities. + :id: T_ARMI_CMP_NUC + :tests: R_ARMI_CMP_NUC """ ndens = self.obj.getNumberDensities() self.assertAlmostEqual(0.0001096, ndens["SI"], 7) self.assertAlmostEqual(0.0000368, ndens["W"], 7) + ndens = self.obj.getNumberDensity("SI") + self.assertAlmostEqual(0.0001096, ndens, 7) + def test_dimensionReport(self): report = self.obj.setComponentDimensionsReport() self.assertEqual(len(report), len(self.obj)) diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 9a3aabac9..2fa22a6f9 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -246,8 +246,12 @@ def test_coreSfp(self): :tests: R_ARMI_R .. test:: The reactor object includes a core and an SFP. - :id: T_ARMI_R_CHILDREN + :id: T_ARMI_R_CHILDREN1 :tests: R_ARMI_R_CHILDREN + + .. test:: Components are a physical part of the reactor. + :id: T_ARMI_CMP2 + :tests: R_ARMI_CMP """ self.assertTrue(isinstance(self.r.core, reactors.Core)) self.assertTrue(isinstance(self.r.sfp, SpentFuelPool)) From 2571216afadbd0b773555f3dfc3d77b66840fa8f Mon Sep 17 00:00:00 2001 From: bdlafleur Date: Wed, 22 Nov 2023 18:56:19 -0600 Subject: [PATCH 055/176] Moving test class crumbs to unit test crumbs (#1489) --- armi/cases/suiteBuilder.py | 4 +- armi/cases/tests/test_cases.py | 14 ++- armi/cases/tests/test_suiteBuilder.py | 111 ++++++++++++++++-- armi/materials/tests/test_materials.py | 12 +- armi/materials/tests/test_water.py | 11 +- .../tests/test_globalFluxInterface.py | 14 +-- .../blueprints/tests/test_blockBlueprints.py | 14 +-- armi/reactor/tests/test_reactors.py | 4 +- armi/utils/tests/test_hexagon.py | 2 +- 9 files changed, 140 insertions(+), 46 deletions(-) diff --git a/armi/cases/suiteBuilder.py b/armi/cases/suiteBuilder.py index 93ee41eae..12baddbf6 100644 --- a/armi/cases/suiteBuilder.py +++ b/armi/cases/suiteBuilder.py @@ -204,7 +204,7 @@ def __init__(self, settingName, value): self.value = value def __call__(self, cs, bp, geom): - cs = cs.modified(newSettings={settignName: value}) + cs = cs.modified(newSettings={self.settingName: self.value}) return cs, bp, geom builder = FullFactorialSuiteBuilder(someCase) @@ -290,7 +290,7 @@ def __init__(self, settingName, value): self.value = value def __call__(self, cs, bp, geom): - cs = cs.modified(newSettings={settignName: value}) + cs = cs.modified(newSettings={self.settignName: self.value}) return cs, bp, geom builder = SeparateEffectsSuiteBuilder(someCase) diff --git a/armi/cases/tests/test_cases.py b/armi/cases/tests/test_cases.py index 952321148..a62ba2441 100644 --- a/armi/cases/tests/test_cases.py +++ b/armi/cases/tests/test_cases.py @@ -269,12 +269,7 @@ def test_clone(self): class TestCaseSuiteDependencies(unittest.TestCase): - """CaseSuite tests. - - .. test:: Dependence allows for one case to start after the completion of another. - :id: T_ARMI_CASE_SUITE - :tests: R_ARMI_CASE_SUITE - """ + """CaseSuite tests.""" def setUp(self): self.suite = cases.CaseSuite(settings.Settings()) @@ -416,6 +411,13 @@ def test_dependencyFromExplictRepeatShuffles(self): self.assertIn(self.c1, self.c2.dependencies) def test_explicitDependency(self): + """ + Test dependencies for case suites. + + .. test:: Dependence allows for one case to start after the completion of another. + :id: T_ARMI_CASE_SUITE + :tests: R_ARMI_CASE_SUITE + """ self.c1.addExplicitDependency(self.c2) self.assertIn(self.c2, self.c1.dependencies) diff --git a/armi/cases/tests/test_suiteBuilder.py b/armi/cases/tests/test_suiteBuilder.py index 534245aa0..e1681091f 100644 --- a/armi/cases/tests/test_suiteBuilder.py +++ b/armi/cases/tests/test_suiteBuilder.py @@ -17,8 +17,15 @@ import unittest from armi import cases, settings -from armi.cases.inputModifiers.inputModifiers import SamplingInputModifier -from armi.cases.suiteBuilder import LatinHyperCubeSuiteBuilder +from armi.cases.inputModifiers.inputModifiers import ( + SamplingInputModifier, + InputModifier, +) +from armi.cases.suiteBuilder import ( + LatinHyperCubeSuiteBuilder, + FullFactorialSuiteBuilder, + SeparateEffectsSuiteBuilder, +) cs = settings.Settings( os.path.join( @@ -45,20 +52,31 @@ def __call__(self, cs, bp, geom): return cs, bp, geom -class TestLatinHyperCubeSuiteBuilder(unittest.TestCase): - """ - Class to test LatinHyperCubeSuiteBuilder. +class SettingModifier(InputModifier): + def __init__(self, settingName, value): + self.settingName = settingName + self.value = value - .. test:: A generic mechanism to allow users to modify user inputs in cases. - :id: T_ARMI_CASE_MOD0 - :tests: R_ARMI_CASE_MOD - """ + def __call__(self, cs, bp, geom): + cs = cs.modified(newSettings={self.settingName: self.value}) + return cs, bp, geom + + +class TestLatinHyperCubeSuiteBuilder(unittest.TestCase): + """Class to test LatinHyperCubeSuiteBuilder.""" def test_initialize(self): builder = LatinHyperCubeSuiteBuilder(case, size=20) assert builder.modifierSets == [] def test_buildSuite(self): + """ + Initialize an LHC suite. + + .. test:: A generic mechanism to allow users to modify user inputs in cases. + :id: T_ARMI_CASE_MOD0 + :tests: R_ARMI_CASE_MOD + """ builder = LatinHyperCubeSuiteBuilder(case, size=20) powerMod = LatinHyperCubeModifier("power", "continuous", [0, 1e6]) availabilityMod = LatinHyperCubeModifier( @@ -78,3 +96,78 @@ def test_addDegreeOfFreedom(self): with self.assertRaises(ValueError): builder.addDegreeOfFreedom([powerMod, morePowerMod]) + + +class TestFullFactorialSuiteBuilder(unittest.TestCase): + """Class to test FullFactorialSuiteBuilder.""" + + def test_buildSuite(self): + """Initialize a full factorial suite of cases. + + .. test:: A generic mechanism to allow users to modify user inputs in cases. + :id: T_ARMI_CASE_MOD1 + :tests: R_ARMI_CASE_MOD + """ + builder = FullFactorialSuiteBuilder(case) + builder.addDegreeOfFreedom( + SettingModifier("settingName1", value) for value in (1, 2) + ) + builder.addDegreeOfFreedom( + SettingModifier("settingName2", value) for value in (3, 4, 5) + ) + + self.assertEquals(builder.modifierSets[0][0].value, 1) + self.assertEquals(builder.modifierSets[0][1].value, 3) + + self.assertEquals(builder.modifierSets[1][0].value, 2) + self.assertEquals(builder.modifierSets[1][1].value, 3) + + self.assertEquals(builder.modifierSets[2][0].value, 1) + self.assertEquals(builder.modifierSets[2][1].value, 4) + + self.assertEquals(builder.modifierSets[3][0].value, 2) + self.assertEquals(builder.modifierSets[3][1].value, 4) + + self.assertEquals(builder.modifierSets[4][0].value, 1) + self.assertEquals(builder.modifierSets[4][1].value, 5) + + self.assertEquals(builder.modifierSets[5][0].value, 2) + self.assertEquals(builder.modifierSets[5][1].value, 5) + + self.assertEquals(len(builder.modifierSets), 6) + + +class TestSeparateEffectsBuilder(unittest.TestCase): + """Class to test separate effects builder.""" + + def test_buildSuite(self): + """Initialize a full factorial suite of cases. + + .. test:: A generic mechanism to allow users to modify user inputs in cases. + :id: T_ARMI_CASE_MOD2 + :tests: R_ARMI_CASE_MOD + """ + builder = SeparateEffectsSuiteBuilder(case) + builder.addDegreeOfFreedom( + SettingModifier("settingName1", value) for value in (1, 2) + ) + builder.addDegreeOfFreedom( + SettingModifier("settingName2", value) for value in (3, 4, 5) + ) + + self.assertEquals(builder.modifierSets[0][0].value, 1) + self.assertEquals(builder.modifierSets[0][0].settingName, "settingName1") + + self.assertEquals(builder.modifierSets[1][0].value, 2) + self.assertEquals(builder.modifierSets[1][0].settingName, "settingName1") + + self.assertEquals(builder.modifierSets[2][0].value, 3) + self.assertEquals(builder.modifierSets[2][0].settingName, "settingName2") + + self.assertEquals(builder.modifierSets[3][0].value, 4) + self.assertEquals(builder.modifierSets[3][0].settingName, "settingName2") + + self.assertEquals(builder.modifierSets[4][0].value, 5) + self.assertEquals(builder.modifierSets[4][0].settingName, "settingName2") + + self.assertEquals(len(builder.modifierSets), 5) diff --git a/armi/materials/tests/test_materials.py b/armi/materials/tests/test_materials.py index f597983bc..c74edec0f 100644 --- a/armi/materials/tests/test_materials.py +++ b/armi/materials/tests/test_materials.py @@ -900,12 +900,6 @@ def test_propertyValidTemperature(self): class Lead_TestCase(_Material_Test, unittest.TestCase): - """Unit tests for lead materials. - - .. test:: There is a base class for fluid materials. - :id: T_ARMI_MAT_FLUID2 - :tests: R_ARMI_MAT_FLUID - """ MAT_CLASS = materials.Lead @@ -928,6 +922,12 @@ def test_volumetricExpansion(self): ) def test_linearExpansion(self): + """Unit tests for lead materials linear expansion. + + .. test:: There is a base class for fluid materials. + :id: T_ARMI_MAT_FLUID2 + :tests: R_ARMI_MAT_FLUID + """ cur = self.mat.linearExpansion(400) ref = 0.0 self.assertEqual(cur, ref) diff --git a/armi/materials/tests/test_water.py b/armi/materials/tests/test_water.py index 56b671527..d62c190c4 100644 --- a/armi/materials/tests/test_water.py +++ b/armi/materials/tests/test_water.py @@ -19,18 +19,17 @@ class Test_Water(unittest.TestCase): - """Unit tests for water materials. - - .. test:: There is a base class for fluid materials. - :id: T_ARMI_MAT_FLUID0 - :tests: R_ARMI_MAT_FLUID - """ + """Unit tests for water materials.""" def test_water_at_freezing(self): """ Reproduce verification results from IAPWS-IF97 for water at 0C. http://www.iapws.org/relguide/supsat.pdf + + .. test:: There is a base class for fluid materials. + :id: T_ARMI_MAT_FLUID0 + :tests: R_ARMI_MAT_FLUID """ water = SaturatedWater() steam = SaturatedSteam() diff --git a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py index dbe6cf924..84837ff73 100644 --- a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py @@ -104,15 +104,15 @@ def getKeff(self): class TestGlobalFluxOptions(unittest.TestCase): - """ - Tests for GlobalFluxOptions. - - .. test:: Tests GlobalFluxOptions. - :id: T_ARMI_FLUX_OPTIONS - :tests: R_ARMI_FLUX_OPTIONS - """ + """Tests for GlobalFluxOptions.""" def test_readFromSettings(self): + """Test reading global flux options. + + .. test:: Tests GlobalFluxOptions. + :id: T_ARMI_FLUX_OPTIONS + :tests: R_ARMI_FLUX_OPTIONS + """ cs = settings.Settings() opts = globalFluxInterface.GlobalFluxOptions("neutronics-run") opts.fromUserSettings(cs) diff --git a/armi/reactor/blueprints/tests/test_blockBlueprints.py b/armi/reactor/blueprints/tests/test_blockBlueprints.py index 34a46e91f..cbab444fd 100644 --- a/armi/reactor/blueprints/tests/test_blockBlueprints.py +++ b/armi/reactor/blueprints/tests/test_blockBlueprints.py @@ -255,12 +255,7 @@ class TestGriddedBlock(unittest.TestCase): - """Tests for a block that has components in a lattice. - - .. test:: Create block with blueprint file. - :id: T_ARMI_BP_BLOCK - :tests: R_ARMI_BP_BLOCK - """ + """Tests for a block that has components in a lattice.""" def setUp(self): self.cs = settings.Settings() @@ -285,7 +280,12 @@ def test_getLocatorsAtLatticePositions(self): self.assertIs(grid[locators[0].getCompleteIndices()], locators[0]) def test_blockLattice(self): - """Make sure constructing a block with grid specifiers works as a whole.""" + """Make sure constructing a block with grid specifiers works as a whole. + + .. test:: Create block with blueprint file. + :id: T_ARMI_BP_BLOCK + :tests: R_ARMI_BP_BLOCK + """ aDesign = self.blueprints.assemDesigns.bySpecifier["IC"] a = aDesign.construct(self.cs, self.blueprints) fuelBlock = a.getFirstBlock(Flags.FUEL) diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 2fa22a6f9..578e6ed80 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -298,9 +298,9 @@ def test_getSetParameters(self): :id: T_ARMI_PARAM_PART :tests: R_ARMI_PARAM_PART - .. impl:: Ensure there is a setting for total core power. + .. test:: Ensure there is a setting for total core power. :id: T_ARMI_SETTINGS_POWER - :implements: R_ARMI_SETTINGS_POWER + :tests: R_ARMI_SETTINGS_POWER """ # Test at core level core = self.r.core diff --git a/armi/utils/tests/test_hexagon.py b/armi/utils/tests/test_hexagon.py index 5d081aebb..31ae3b9c6 100644 --- a/armi/utils/tests/test_hexagon.py +++ b/armi/utils/tests/test_hexagon.py @@ -25,7 +25,7 @@ def test_hexagon_area(self): .. test:: Hexagonal area is retrievable. :id: T_ARMI_UTIL_HEXAGON0 - :test: R_ARMI_UTIL_HEXAGON + :tests: R_ARMI_UTIL_HEXAGON """ # Calculate area given a pitch self.assertEqual(hexagon.area(1), math.sqrt(3.0) / 2) From 550c47fc1c43918d2a139ffd57929848a7b4176e Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Mon, 27 Nov 2023 07:33:59 -0800 Subject: [PATCH 056/176] Fixing import loop (#1495) --- armi/reactor/blueprints/tests/test_gridBlueprints.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/armi/reactor/blueprints/tests/test_gridBlueprints.py b/armi/reactor/blueprints/tests/test_gridBlueprints.py index 757e19084..df12aabac 100644 --- a/armi/reactor/blueprints/tests/test_gridBlueprints.py +++ b/armi/reactor/blueprints/tests/test_gridBlueprints.py @@ -23,7 +23,6 @@ from armi.reactor import systemLayoutInput from armi.reactor.blueprints import Blueprints from armi.reactor.blueprints.gridBlueprint import Grids, saveToStream -from armi.reactor.blueprints.tests.test_blockBlueprints import FULL_BP, FULL_BP_GRID from armi.utils.directoryChangers import TemporaryDirectoryChanger @@ -401,6 +400,8 @@ def test_simpleReadLatticeMap(self): :id: T_ARMI_BP_GRID0 :tests: R_ARMI_BP_GRID """ + from armi.reactor.blueprints.tests.test_blockBlueprints import FULL_BP + # Cartesian full, even/odd hybrid gridDesign4 = self.grids["sfp even"] _grid = gridDesign4.construct() @@ -432,6 +433,8 @@ def test_simpleReadLatticeMap(self): self.assertTrue(os.path.exists(filePath)) def test_simpleReadNoLatticeMap(self): + from armi.reactor.blueprints.tests.test_blockBlueprints import FULL_BP_GRID + # Cartesian full, even/odd hybrid gridDesign4 = self.grids["sfp even"] _grid = gridDesign4.construct() From 0a4bad779a5e2abad29653910db1885ea340418e Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Mon, 27 Nov 2023 07:58:15 -0800 Subject: [PATCH 057/176] Adding impl/test crumbs for the Database (#1493) --- armi/bookkeeping/db/__init__.py | 4 ---- armi/bookkeeping/db/database3.py | 17 ++++++++++++++ armi/bookkeeping/db/layout.py | 6 +++++ armi/bookkeeping/db/tests/test_database3.py | 25 +++++++++++++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/armi/bookkeeping/db/__init__.py b/armi/bookkeeping/db/__init__.py index 7edf2f9c7..336d4c85d 100644 --- a/armi/bookkeeping/db/__init__.py +++ b/armi/bookkeeping/db/__init__.py @@ -249,10 +249,6 @@ def _getH5File(db): All this being said, we are probably violating this already with genAuxiliaryData, but we have to start somewhere. - - .. impl:: The ARMI output file has a language-agnostic format. - :id: I_ARMI_DB_H5 - :implements: R_ARMI_DB_H5 """ if isinstance(db, Database3): return db.h5db diff --git a/armi/bookkeeping/db/database3.py b/armi/bookkeeping/db/database3.py index cba50dbba..c2a60d892 100644 --- a/armi/bookkeeping/db/database3.py +++ b/armi/bookkeeping/db/database3.py @@ -109,6 +109,10 @@ class Database3: handles the packing and unpacking of the structure of the objects, their relationships, and their non-parameter attributes. + .. impl:: The database files are H5, and thus language agnostic. + :id: I_ARMI_DB_H51 + :implements: R_ARMI_DB_H5 + See Also -------- `doc/user/outputs/database` for more details. @@ -423,6 +427,7 @@ def loadBlueprints(self): def loadGeometry(self): """ This is primarily just used for migrations. + The "geometry files" were replaced by ``systems:`` and ``grids:`` sections of ``Blueprints``. """ geom = systemLayoutInput.SystemLayoutInput() @@ -437,6 +442,14 @@ def writeInputsToDB(self, cs, csString=None, geomString=None, bpString=None): implementation should be very stable, so we dont want it to be easy to change one Database implementation's behavior when trying to change another's. + .. impl:: The run settings are saved the settings file. + :id: I_ARMI_DB_CS + :implements: R_ARMI_DB_CS + + .. impl:: The reactor blueprints are saved the settings file. + :id: I_ARMI_DB_BP + :implements: R_ARMI_DB_BP + Notes ----- This is hard-coded to read the entire file contents into memory and write that @@ -659,6 +672,10 @@ def load( continue with new settings (or if blueprints are not on the database). Geometry is read from the database itself. + .. test:: Users can load a reactor from a DB. + :id: I_ARMI_DB_R_LOAD + :tests: R_ARMI_DB_R_LOAD + Parameters ---------- cycle : int diff --git a/armi/bookkeeping/db/layout.py b/armi/bookkeeping/db/layout.py index a49877a4e..347e28155 100644 --- a/armi/bookkeeping/db/layout.py +++ b/armi/bookkeeping/db/layout.py @@ -381,6 +381,12 @@ def _initComps(self, caseTitle, bp): return comps, groupedComps def writeToDB(self, h5group): + """Write a chunk of data to the database. + + .. test:: Write data to the DB for a given time step. + :id: I_ARMI_DB_TIME + :tests: R_ARMI_DB_TIME + """ if "layout/type" in h5group: # It looks like we have already written the layout to DB, skip for now return diff --git a/armi/bookkeeping/db/tests/test_database3.py b/armi/bookkeeping/db/tests/test_database3.py index a9676692b..7ca30ca3f 100644 --- a/armi/bookkeeping/db/tests/test_database3.py +++ b/armi/bookkeeping/db/tests/test_database3.py @@ -55,6 +55,12 @@ def tearDown(self): self.td.__exit__(None, None, None) 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 + :tests: R_ARMI_DB_TIME + """ self.r.p.cycle = 0 self.r.p.timeNode = 0 self.r.p.cycleLength = 0 @@ -67,6 +73,7 @@ def test_writeToDB(self): self.db.writeToDB(self.r) self.assertEqual(sorted(self.db.h5db.keys()), ["c00n00", "inputs"]) + # check the keys for a single time step keys = [ "Circle", "Core", @@ -345,6 +352,12 @@ def test_computeParents(self): ) 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 + """ self.makeShuffleHistory() with self.assertRaises(KeyError): _r = self.db.load(0, 0) @@ -601,14 +614,26 @@ def test_fileName(self): self.assertEqual(str(self.db.fileName), "thing.h5") def test_readInputsFromDB(self): + """Test that we can read inputs from the database. + + .. test:: Save and retrieve settings from the database. + :id: T_ARMI_DB_CS + :tests: R_ARMI_DB_CS + + .. test:: Save and retrieve blueprints from the database. + :id: T_ARMI_DB_BP + :tests: R_ARMI_DB_BP + """ inputs = self.db.readInputsFromDB() self.assertEqual(len(inputs), 3) + # settings self.assertGreater(len(inputs[0]), 100) self.assertIn("settings:", inputs[0]) self.assertEqual(len(inputs[1]), 0) + # blueprints self.assertGreater(len(inputs[2]), 100) self.assertIn("custom isotopics:", inputs[2]) self.assertIn("blocks:", inputs[2]) From 4e5eba359f46afad65e462b6fed1f85f8b2f945e Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Mon, 27 Nov 2023 12:42:41 -0800 Subject: [PATCH 058/176] Adding impl/test crumbs for Grid (#1492) --- armi/materials/tests/test_materials.py | 10 +++ armi/materials/tests/test_uZr.py | 2 +- armi/physics/executers.py | 12 ++++ armi/reactor/components/component.py | 6 +- armi/reactor/composites.py | 2 +- armi/reactor/grids/grid.py | 11 +++- armi/reactor/grids/hexagonal.py | 9 ++- armi/reactor/grids/structuredgrid.py | 8 ++- armi/reactor/grids/tests/test_grids.py | 86 ++++++++++++++++++++++---- armi/reactor/tests/test_blocks.py | 6 +- armi/reactor/tests/test_components.py | 4 +- armi/reactor/tests/test_composites.py | 2 +- armi/reactor/tests/test_reactors.py | 17 ++++- 13 files changed, 144 insertions(+), 31 deletions(-) diff --git a/armi/materials/tests/test_materials.py b/armi/materials/tests/test_materials.py index c74edec0f..e04ac1cf8 100644 --- a/armi/materials/tests/test_materials.py +++ b/armi/materials/tests/test_materials.py @@ -716,6 +716,16 @@ def test_getTempChangeForDensityChange(self): self.assertAlmostEqual(expectedDeltaT, actualDeltaT) def test_duplicate(self): + """Test the material duplication. + + .. test:: Test the material base class is usable. + :id: T_ARMI_MAT_PROPERTIES3 + :tests: R_ARMI_MAT_PROPERTIES + + .. test:: Materials shall calc mass fracs at init. + :id: T_ARMI_MAT_FRACS4 + :tests: R_ARMI_MAT_FRACS + """ duplicateU = self.mat.duplicate() for key in self.mat.massFrac: diff --git a/armi/materials/tests/test_uZr.py b/armi/materials/tests/test_uZr.py index d6072b4f5..d684bd184 100644 --- a/armi/materials/tests/test_uZr.py +++ b/armi/materials/tests/test_uZr.py @@ -61,7 +61,7 @@ def test_duplicate(self): """Test the material duplication. .. test:: Materials shall calc mass fracs at init. - :id: T_ARMI_MAT_FRACS + :id: T_ARMI_MAT_FRACS5 :tests: R_ARMI_MAT_FRACS """ mat = self.mat.duplicate() diff --git a/armi/physics/executers.py b/armi/physics/executers.py index e9e755c74..73151dff3 100644 --- a/armi/physics/executers.py +++ b/armi/physics/executers.py @@ -29,6 +29,10 @@ class ExecutionOptions: """ A data structure representing all options needed for a physics kernel. + .. impl:: Options for executing external calculations. + :id: I_ARMI_EX0 + :implements: R_ARMI_EX + Attributes ---------- inputFile : str @@ -123,6 +127,10 @@ class Executer: """ Short-lived object that coordinates a calculation step and updates a reactor. + .. impl:: Tool for executing external calculations. + :id: I_ARMI_EX1 + :implements: R_ARMI_EX + Notes ----- This is deliberately **not** a :py:class:`~mpiActions.MpiAction`. Thus, Executers can run as @@ -154,6 +162,10 @@ class DefaultExecuter(Executer): externally-executed physics codes. It is here for convenience but is not required. The sequence look like: + .. impl:: Default tool for executing external calculations. + :id: I_ARMI_EX2 + :implements: R_ARMI_EX + * Choose modeling options (either from the global run settings input or dictated programmatically) * Apply geometry transformations to the ARMI Reactor as needed * Build run-specific working directory diff --git a/armi/reactor/components/component.py b/armi/reactor/components/component.py index e8202a13c..6ce144ba5 100644 --- a/armi/reactor/components/component.py +++ b/armi/reactor/components/component.py @@ -173,9 +173,9 @@ class Component(composites.Composite, metaclass=ComponentType): :id: I_ARMI_COMP_DEF :implements: R_ARMI_COMP_DEF - .. impl:: Order components by there outermost diameter (using the < operator). - :id: I_ARMI_COMP_ORDER - :implements: R_ARMI_COMP_ORDER + .. impl:: Order components by their outermost diameter (using the < operator). + :id: I_ARMI_COMP_ORDER + :implements: R_ARMI_COMP_ORDER Attributes ---------- diff --git a/armi/reactor/composites.py b/armi/reactor/composites.py index 480e4463a..de3831104 100644 --- a/armi/reactor/composites.py +++ b/armi/reactor/composites.py @@ -672,7 +672,7 @@ def hasFlags(self, typeID: TypeSpec, exact=False): """ Determine if this object is of a certain type. - .. impl:: Flags can be queried. + .. impl:: Composites have queriable flags. :id: I_ARMI_CMP_FLAG :implements: R_ARMI_CMP_FLAG diff --git a/armi/reactor/grids/grid.py b/armi/reactor/grids/grid.py index 8e33bdec4..8f57fad12 100644 --- a/armi/reactor/grids/grid.py +++ b/armi/reactor/grids/grid.py @@ -35,6 +35,10 @@ class Grid(ABC): So here, we define an interface so things that rely on grids can worry less about how the location data are stored. + .. impl:: Grids can nest. + :id: I_ARMI_GRID_NEST + :implements: R_ARMI_GRID_NEST + Parameters ---------- geomType : str or armi.reactor.geometry.GeomType @@ -81,7 +85,12 @@ def geomType(self, geomType: Union[str, geometry.GeomType]): @property def symmetry(self) -> str: - """Symmetry applied to the grid.""" + """Symmetry applied to the grid. + + .. impl:: Grids shall be able to repesent 1/3 and full core symmetries. + :id: I_ARMI_GRID_SYMMETRY + :implements: R_ARMI_GRID_SYMMETRY + """ return geometry.SymmetryType.fromStr(self._symmetry) @symmetry.setter diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py index 745d1f432..000a8c765 100644 --- a/armi/reactor/grids/hexagonal.py +++ b/armi/reactor/grids/hexagonal.py @@ -74,6 +74,14 @@ def fromPitch(pitch, numRings=25, armiObject=None, pointedEndUp=False, symmetry= """ Build a finite step-based 2-D hex grid from a hex pitch in cm. + .. impl:: Construct a hexagonal lattice. + :id: I_ARMI_GRID_HEX + :implements: R_ARMI_GRID_HEX + + .. impl:: Hexagonal grids can be points-up or flats-up. + :id: I_ARMI_GRID_HEX_TYPE + :implements: R_ARMI_GRID_HEX_TYPE + Parameters ---------- pitch : float @@ -290,7 +298,6 @@ def overlapsWhichSymmetryLine(self, indices: IJType) -> Optional[int]: ----- - Only the 1/3 core view geometry is actually coded in here right now. - Being "on" a symmetry line means the line goes through the middle of you. - """ i, j = indices[:2] diff --git a/armi/reactor/grids/structuredgrid.py b/armi/reactor/grids/structuredgrid.py index 07ec5f2af..7b74eaadd 100644 --- a/armi/reactor/grids/structuredgrid.py +++ b/armi/reactor/grids/structuredgrid.py @@ -290,7 +290,12 @@ def restoreBackup(self): self._unitSteps, self._bounds, self._offset = self._backup def getCoordinates(self, indices, nativeCoords=False) -> numpy.ndarray: - """Return the coordinates of the center of the mesh cell at the given given indices in cm.""" + """Return the coordinates of the center of the mesh cell at the given indices in cm. + + .. test:: Get the coordinates from a location in a grid. + :id: I_ARMI_GRID_GLOBAL_POS + :tests: R_ARMI_GRID_GLOBAL_POS + """ indices = numpy.array(indices) return self._evaluateMesh( indices, self._centroidBySteps, self._centroidByBounds @@ -495,7 +500,6 @@ def pitch(self) -> Union[float, Tuple[float, float]]: ------- float or tuple of (float, float) Grid spacing in cm - """ diff --git a/armi/reactor/grids/tests/test_grids.py b/armi/reactor/grids/tests/test_grids.py index 7311a9b50..728f87bb2 100644 --- a/armi/reactor/grids/tests/test_grids.py +++ b/armi/reactor/grids/tests/test_grids.py @@ -308,26 +308,71 @@ def test_getSymmetricIdenticalsThird(self): :id: T_ARMI_GRID_EQUIVALENTS :tests: R_ARMI_GRID_EQUIVALENTS """ - grid = grids.HexGrid.fromPitch(1.0) - grid.symmetry = str( + g = grids.HexGrid.fromPitch(1.0) + g.symmetry = str( geometry.SymmetryType( geometry.DomainType.THIRD_CORE, geometry.BoundaryType.PERIODIC ) ) - self.assertEqual(grid.getSymmetricEquivalents((3, -2)), [(-1, 3), (-2, -1)]) - self.assertEqual(grid.getSymmetricEquivalents((2, 1)), [(-3, 2), (1, -3)]) + self.assertEqual(g.getSymmetricEquivalents((3, -2)), [(-1, 3), (-2, -1)]) + self.assertEqual(g.getSymmetricEquivalents((2, 1)), [(-3, 2), (1, -3)]) - symmetrics = grid.getSymmetricEquivalents(grid.getIndicesFromRingAndPos(5, 3)) + symmetrics = g.getSymmetricEquivalents(g.getIndicesFromRingAndPos(5, 3)) self.assertEqual( - [(5, 11), (5, 19)], [grid.getRingPos(indices) for indices in symmetrics] + [(5, 11), (5, 19)], [g.getRingPos(indices) for indices in symmetrics] + ) + + def test_thirdAndFullSymmetry(self): + """Test that we can construct a full and a 1/3 core grid. + + .. test:: Test 1/3 and full cores have the correct positions and rings. + :id: T_ARMI_GRID_SYMMETRY + :tests: R_ARMI_GRID_SYMMETRY + """ + full = grids.HexGrid.fromPitch(1.0) + full.symmetry = str( + geometry.SymmetryType( + geometry.DomainType.FULL_CORE, geometry.BoundaryType.NO_SYMMETRY + ) + ) + third = grids.HexGrid.fromPitch(1.0) + third.symmetry = str( + geometry.SymmetryType( + geometry.DomainType.THIRD_CORE, geometry.BoundaryType.PERIODIC + ) ) + # check full core + self.assertEqual(full.getMinimumRings(2), 2) + self.assertEqual(full.getIndicesFromRingAndPos(2, 2), (0, 1)) + self.assertEqual(full.getPositionsInRing(3), 12) + self.assertEqual(full.getSymmetricEquivalents((3, -2)), []) + + # check 1/3 core + self.assertEqual(third.getMinimumRings(2), 2) + self.assertEqual(third.getIndicesFromRingAndPos(2, 2), (0, 1)) + self.assertEqual(third.getPositionsInRing(3), 12) + self.assertEqual(third.getSymmetricEquivalents((3, -2)), [(-1, 3), (-2, -1)]) + + def test_pointsUpFlatsUp(self): + """Test the pointedEndUp attribute of the fromPitch method. + + .. test:: Build a points-up and a flats-up hexagonal grids. + :id: T_ARMI_GRID_HEX_TYPE + :tests: R_ARMI_GRID_HEX_TYPE + """ + tipsUp = grids.HexGrid.fromPitch(1.0, pointedEndUp=True) + flatsUp = grids.HexGrid.fromPitch(1.0, pointedEndUp=False) + + self.assertEqual(tipsUp._unitSteps[0][0], 0.5) + self.assertAlmostEqual(flatsUp._unitSteps[0][0], 0.8660254037844388) + def test_triangleCoords(self): - grid = grids.HexGrid.fromPitch(8.15) - indices1 = grid.getIndicesFromRingAndPos(5, 3) + (0,) - indices2 = grid.getIndicesFromRingAndPos(5, 23) + (0,) - indices3 = grid.getIndicesFromRingAndPos(3, 4) + (0,) - cur = grid.triangleCoords(indices1) + g = grids.HexGrid.fromPitch(8.15) + indices1 = g.getIndicesFromRingAndPos(5, 3) + (0,) + indices2 = g.getIndicesFromRingAndPos(5, 23) + (0,) + indices3 = g.getIndicesFromRingAndPos(3, 4) + (0,) + cur = g.triangleCoords(indices1) ref = [ (16.468_916_428_634_078, 25.808_333_333_333_337), (14.116_214_081_686_351, 27.166_666_666_666_67), @@ -362,8 +407,8 @@ def test_triangleCoords(self): def test_getIndexBounds(self): numRings = 5 - grid = grids.HexGrid.fromPitch(1.0, numRings=numRings) - boundsIJK = grid.getIndexBounds() + g = grids.HexGrid.fromPitch(1.0, numRings=numRings) + boundsIJK = g.getIndexBounds() self.assertEqual( boundsIJK, ((-numRings, numRings), (-numRings, numRings), (0, 1)) ) @@ -390,11 +435,26 @@ def test_is_pickleable(self): assert_allclose(loc.indices, newLoc.indices) def test_adjustPitch(self): + """Adjust the pich of a hexagonal lattice. + + .. test:: Construct a hexagonal lattice with three rings. + :id: T_ARMI_GRID_HEX + :tests: R_ARMI_GRID_HEX + + .. test:: Return the grid coordinates of different locations. + :id: T_ARMI_GRID_GLOBAL_POS0 + :tests: R_ARMI_GRID_GLOBAL_POS + """ grid = grids.HexGrid.fromPitch(1.0, numRings=3) v1 = grid.getCoordinates((1, 0, 0)) grid.changePitch(2.0) v2 = grid.getCoordinates((1, 0, 0)) assert_allclose(2 * v1, v2) + self.assertEqual(grid.pitch, 2.0) + + # test number of rings + numRings = 3 + self.assertEqual(grid._unitStepLimits[0][1], numRings) def test_badIndices(self): grid = grids.HexGrid.fromPitch(1.0, numRings=3) diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py index a540ce4b6..810a72bcc 100644 --- a/armi/reactor/tests/test_blocks.py +++ b/armi/reactor/tests/test_blocks.py @@ -1240,7 +1240,7 @@ def test_getComponentByName(self): """Test children by name. .. test:: Get children by name. - :id: I_ARMI_CMP_BY_NAME0 + :id: T_ARMI_CMP_BY_NAME0 :tests: R_ARMI_CMP_BY_NAME """ self.assertIsNone( @@ -1252,7 +1252,7 @@ def test_getSortedComponentsInsideOfComponent(self): """Test that components can be sorted within a block and returned in the correct order. .. test:: Get children by name. - :id: I_ARMI_CMP_BY_NAME1 + :id: T_ARMI_CMP_BY_NAME1 :tests: R_ARMI_CMP_BY_NAME """ expected = [ @@ -1276,7 +1276,7 @@ def test_getSortedComponentsInsideOfComponentSpecifiedTypes(self): """Test that components can be sorted within a block and returned in the correct order. .. test:: Get children by name. - :id: I_ARMI_CMP_BY_NAME2 + :id: T_ARMI_CMP_BY_NAME2 :tests: R_ARMI_CMP_BY_NAME """ expected = [ diff --git a/armi/reactor/tests/test_components.py b/armi/reactor/tests/test_components.py index 1c16b8cb6..8337896a3 100644 --- a/armi/reactor/tests/test_components.py +++ b/armi/reactor/tests/test_components.py @@ -443,7 +443,7 @@ def test_getThermalExpansionFactorConservedMassByLinearExpansionPercent(self): :tests: R_ARMI_COMP_SHAPES .. test:: Calculate thermal expansion. - :id: I_ARMI_COMP_EXPANSION0 + :id: T_ARMI_COMP_EXPANSION0 :tests: R_ARMI_COMP_EXPANSION """ hotTemp = 700.0 @@ -462,7 +462,7 @@ def test_getDimension(self): :tests: R_ARMI_COMP_DIMS .. test:: Calculate thermal expansion. - :id: I_ARMI_COMP_EXPANSION1 + :id: T_ARMI_COMP_EXPANSION1 :tests: R_ARMI_COMP_EXPANSION """ hotTemp = 700.0 diff --git a/armi/reactor/tests/test_composites.py b/armi/reactor/tests/test_composites.py index 6b127d1ac..26fccc9a8 100644 --- a/armi/reactor/tests/test_composites.py +++ b/armi/reactor/tests/test_composites.py @@ -113,7 +113,7 @@ def setUp(self): def test_composite(self): """Test basic Composite things. - .. test:: Composites are a physical item. + .. test:: Composites are a physical part of the reactor. :id: T_ARMI_CMP0 :tests: R_ARMI_CMP diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 578e6ed80..673791a9a 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -846,9 +846,20 @@ def test_getFuelBottomHeight(self): self.assertEqual(fuelBottomHeightInCm, fuelBottomHeightRef) def test_getGridBounds(self): - (_minI, maxI), (_minJ, maxJ), (minK, maxK) = self.r.core.getBoundingIndices() - self.assertEqual((maxI, maxJ), (8, 8)) - self.assertEqual((minK, maxK), (0, 0)) + """Test getGridBounds() works on different scales. + + .. test:: Test that assembly grids nest inside core grids. + :id: T_ARMI_GRID_NEST + :tests: R_ARMI_GRID_NEST + """ + (minI, maxI), (minJ, maxJ), (_minK, _maxK) = self.r.core.getBoundingIndices() + self.assertEqual((minI, maxI), (-3, 8)) + self.assertEqual((minJ, maxJ), (-4, 8)) + + randomBlock = self.r.core.getFirstAssembly() + (minI, maxI), (minJ, maxJ), (_minK, _maxK) = randomBlock.getBoundingIndices() + self.assertEqual((minI, maxI), (8, 8)) + self.assertEqual((minJ, maxJ), (-4, -4)) def test_locations(self): loc = self.r.core.spatialGrid.getLocatorFromRingAndPos(3, 2) From bbe9d01d260d7719b0aec1ca58106ba956c1d079 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Mon, 27 Nov 2023 13:05:41 -0800 Subject: [PATCH 059/176] Adding crumbs for CLI (#1496) --- armi/cli/__init__.py | 6 +++++- armi/cli/entryPoint.py | 4 ++++ armi/cli/tests/test_runEntryPoint.py | 19 +++++++++++++++++- armi/cli/tests/test_runSuite.py | 29 ++++++++++++++++++++++++++-- 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/armi/cli/__init__.py b/armi/cli/__init__.py index 30a789324..19a7384c7 100644 --- a/armi/cli/__init__.py +++ b/armi/cli/__init__.py @@ -104,6 +104,10 @@ class ArmiCLI: available, to get help for the individual commands, run again with ` --help`. Generically, the CLI implements functions that already exists within ARMI. + + .. impl:: The basic ARMI CLI, for running a simulation. + :id: I_ARMI_CLI_CS + :implements: R_ARMI_CLI_CS """ def __init__(self): @@ -198,7 +202,7 @@ def run(self) -> Optional[int]: return self.executeCommand(args.command, args.args) def executeCommand(self, command, args) -> Optional[int]: - r"""Execute `command` with arguments `args`, return optional exit code.""" + """Execute `command` with arguments `args`, return optional exit code.""" command = command.lower() if command not in self._entryPoints: print( diff --git a/armi/cli/entryPoint.py b/armi/cli/entryPoint.py index 2a9c2fc82..e8554ef41 100644 --- a/armi/cli/entryPoint.py +++ b/armi/cli/entryPoint.py @@ -53,6 +53,10 @@ class EntryPoint: A valid subclass must provide at least a ``name`` class attribute, and may also specify the other class attributes described below. + + .. impl:: Generic CLI base class for developers to use. + :id: I_ARMI_CLI_GEN + :implements: R_ARMI_CLI_GEN """ #: The that is used to call the command from the command line diff --git a/armi/cli/tests/test_runEntryPoint.py b/armi/cli/tests/test_runEntryPoint.py index 069ad20e3..ca83ad2bf 100644 --- a/armi/cli/tests/test_runEntryPoint.py +++ b/armi/cli/tests/test_runEntryPoint.py @@ -37,7 +37,12 @@ class TestInitializationEntryPoints(unittest.TestCase): def test_entryPointInitialization(self): - """Tests the initialization of all subclasses of `EntryPoint`.""" + """Tests the initialization of all subclasses of `EntryPoint`. + + .. test:: Test initialization of many basic CLIs. + :id: T_ARMI_CLI_GEN0 + :tests: R_ARMI_CLI_GEN + """ entryPoints = getEntireFamilyTree(EntryPoint) # Comparing to a minimum number of entry points, in case more are added. @@ -74,6 +79,12 @@ def test_checkInputEntryPointBasics(self): self.assertEqual(ci.args.generate_design_summary, False) def test_checkInputEntryPointInvoke(self): + """Test the "check inputs" entry point. + + .. test:: A working CLI child class, to validate inputs. + :id: T_ARMI_CLI_GEN1 + :tests: R_ARMI_CLI_GEN + """ ci = CheckInputEntryPoint() ci.addOptions() ci.parse_args([ARMI_RUN_PATH]) @@ -124,6 +135,12 @@ def test_cloneArmiRunCommandBatchInvokeShort(self): self.assertNotIn("availabilityFactor", txt) def test_cloneArmiRunCommandBatchInvokeMedium(self): + """Test the "clone armi run" batch entry point, on medium detail. + + .. test:: A working CLI child class, to clone a run. + :id: T_ARMI_CLI_GEN2 + :tests: R_ARMI_CLI_GEN + """ # Test medium write style ca = CloneArmiRunCommandBatch() ca.addOptions() diff --git a/armi/cli/tests/test_runSuite.py b/armi/cli/tests/test_runSuite.py index 32bf55915..8c663b8c1 100644 --- a/armi/cli/tests/test_runSuite.py +++ b/armi/cli/tests/test_runSuite.py @@ -15,6 +15,7 @@ import io import sys import unittest +from unittest.mock import patch from armi import meta from armi.cli import ArmiCLI @@ -22,7 +23,12 @@ class TestRunSuiteSuite(unittest.TestCase): def test_listCommand(self): - """Ensure run-suite entry point is registered.""" + """Ensure run-suite entry point is registered. + + .. test:: The ARMI CLI can be correctly initialized. + :id: T_ARMI_CLI_CS0 + :tests: R_ARMI_CLI_CS + """ acli = ArmiCLI() origout = sys.stdout @@ -36,7 +42,12 @@ def test_listCommand(self): self.assertIn("run-suite", out.getvalue()) def test_showVersion(self): - """Test the ArmiCLI.showVersion method.""" + """Test the ArmiCLI.showVersion method. + + .. test:: The ARMI CLI's basic "--version" functionality works. + :id: T_ARMI_CLI_CS1 + :tests: R_ARMI_CLI_CS + """ origout = sys.stdout try: out = io.StringIO() @@ -47,3 +58,17 @@ def test_showVersion(self): self.assertIn("armi", out.getvalue()) self.assertIn(meta.__version__, out.getvalue()) + + @patch("armi.cli.ArmiCLI.executeCommand") + def test_run(self, mockExeCmd): + """Test the ArmiCLI.run method. + + .. test:: The ARMI CLI's import run() method works. + :id: T_ARMI_CLI_CS2 + :tests: R_ARMI_CLI_CS + """ + correct = 0 + acli = ArmiCLI() + mockExeCmd.return_value = correct + ret = acli.run() + self.assertEqual(ret, correct) From 9442e14636c153fb49726e18d6dd4dfaff5350fa Mon Sep 17 00:00:00 2001 From: Nick Touran Date: Tue, 28 Nov 2023 10:09:44 -0800 Subject: [PATCH 060/176] Add armi-example-app submodule back in. (#1502) It is still used in documentation builds. It was removed in dac69790bf325f9701595f020cd388dc817ea30f probably accidentally as part of a cleanup. --- doc/tutorials/armi-example-app | 1 + 1 file changed, 1 insertion(+) create mode 160000 doc/tutorials/armi-example-app diff --git a/doc/tutorials/armi-example-app b/doc/tutorials/armi-example-app new file mode 160000 index 000000000..60becb513 --- /dev/null +++ b/doc/tutorials/armi-example-app @@ -0,0 +1 @@ +Subproject commit 60becb5137cf3c3671ebd44b06b894287611c181 From f9f456fc16494be1dcccd37d429b38e8eeb7c895 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 28 Nov 2023 11:25:24 -0800 Subject: [PATCH 061/176] Adding crumbs for XSGM (#1497) --- .../neutronics/crossSectionGroupManager.py | 42 +++++++-- .../tests/test_crossSectionManager.py | 85 +++++++++++++------ 2 files changed, 94 insertions(+), 33 deletions(-) diff --git a/armi/physics/neutronics/crossSectionGroupManager.py b/armi/physics/neutronics/crossSectionGroupManager.py index b9ee666a5..4de167e58 100644 --- a/armi/physics/neutronics/crossSectionGroupManager.py +++ b/armi/physics/neutronics/crossSectionGroupManager.py @@ -315,6 +315,10 @@ class AverageBlockCollection(BlockCollection): Averages number densities, fission product yields, and fission gas removal fractions. + + .. impl:: Create representative blocks using volume-weighted averaging. + :id: I_ARMI_XSGM_CREATE_REPR_BLOCKS0 + :implements: R_ARMI_XSGM_CREATE_REPR_BLOCKS """ def _makeRepresentativeBlock(self): @@ -474,9 +478,7 @@ def getBlockNuclideTemperatureAvgTerms(block, allNucNames): """ def getNumberDensitiesWithTrace(component, allNucNames): - """ - Needed to make sure temperature of 0-density nuclides in fuel get fuel temperature - """ + """Needed to make sure temperature of 0-density nuclides in fuel get fuel temperature.""" return [ component.p.numberDensities[nucName] or TRACE_NUMBER_DENSITY if nucName in component.p.numberDensities @@ -506,6 +508,10 @@ class CylindricalComponentsAverageBlockCollection(BlockCollection): Creates a representative block for the purpose of cross section generation with a one-dimensional cylindrical model. + .. impl:: Create representative blocks using custom cylindrical averaging. + :id: I_ARMI_XSGM_CREATE_REPR_BLOCKS1 + :implements: R_ARMI_XSGM_CREATE_REPR_BLOCKS + Notes ----- When generating the representative block within this collection, the geometry is checked @@ -832,6 +838,12 @@ def __init__(self, r, cs): self._unrepresentedXSIDs = [] def interactBOL(self): + """Called at the Beginning-of-Life of a run, before any cycles start. + + .. impl:: The lattice physics interface and XSGM are connected at BOL. + :id: I_ARMI_XSGM_FREQ0 + :implements: R_ARMI_XSGM_FREQ + """ # now that all cs settings are loaded, apply defaults to compound XS settings from armi.physics.neutronics.settings import CONF_XS_BLOCK_REPRESENTATION from armi.physics.neutronics.settings import ( @@ -853,6 +865,10 @@ def interactBOC(self, cycle=None): """ Update representative blocks and block burnup groups. + .. impl:: The lattice physics interface and XSGM are connected at BOC. + :id: I_ARMI_XSGM_FREQ1 + :implements: R_ARMI_XSGM_FREQ + Notes ----- The block list each each block collection cannot be emptied since it is used to derive nuclide temperatures. @@ -861,20 +877,29 @@ def interactBOC(self, cycle=None): self.createRepresentativeBlocks() def interactEOC(self, cycle=None): - """ - EOC interaction. + """EOC interaction. Clear out big dictionary of all blocks to avoid memory issues and out-of-date representers. """ self.clearRepresentativeBlocks() def interactEveryNode(self, cycle=None, tn=None): + """Interactino at every time now. + + .. impl:: The lattice physics interface and XSGM are connected at every time node. + :id: I_ARMI_XSGM_FREQ2 + :implements: R_ARMI_XSGM_FREQ + """ if self._latticePhysicsFrequency >= LatticePhysicsFrequency.everyNode: self.createRepresentativeBlocks() def interactCoupled(self, iteration): """Update XS groups on each physics coupling iteration to get latest temperatures. + .. impl:: The lattice physics interface and XSGM are connected during coupling. + :id: I_ARMI_XSGM_FREQ3 + :implements: R_ARMI_XSGM_FREQ + Notes ----- Updating the XS on only the first (i.e., iteration == 0) timenode can be a reasonable approximation to @@ -1052,7 +1077,12 @@ def _getPregeneratedFluxFileLocationData(self, xsID): return (filePath, fileName) def createRepresentativeBlocks(self): - """Get a representative block from each cross section ID managed here.""" + """Get a representative block from each cross section ID managed here. + + .. impl:: Create collections of blocks based on XS type and burn-up group. + :id: I_ARMI_XSGM_CREATE_XS_GROUPS + :implements: R_ARMI_XSGM_CREATE_XS_GROUPS + """ representativeBlocks = {} self.avgNucTemperatures = {} self._unrepresentedXSIDs = [] diff --git a/armi/physics/neutronics/tests/test_crossSectionManager.py b/armi/physics/neutronics/tests/test_crossSectionManager.py index 995100896..66f2dcc13 100644 --- a/armi/physics/neutronics/tests/test_crossSectionManager.py +++ b/armi/physics/neutronics/tests/test_crossSectionManager.py @@ -93,7 +93,6 @@ def setUp(self): self.bc.extend(self.blockList) def test_createRepresentativeBlock(self): - avgB = self.bc.createRepresentativeBlock() self.assertAlmostEqual(avgB.p.percentBu, 50.0) @@ -132,25 +131,24 @@ def setUp(self): self.bc.averageByComponent = True def test_performAverageByComponent(self): - """ - Check the averageByComponent attribute - """ + """Check the averageByComponent attribute.""" self.bc._checkBlockSimilarity = MagicMock(return_value=True) self.assertTrue(self.bc._performAverageByComponent()) self.bc.averageByComponent = False self.assertFalse(self.bc._performAverageByComponent()) def test_checkBlockSimilarity(self): - """ - Check the block similarity test - """ + """Check the block similarity test.""" self.assertTrue(self.bc._checkBlockSimilarity()) self.bc.append(test_blocks.loadTestBlock()) self.assertFalse(self.bc._checkBlockSimilarity()) def test_createRepresentativeBlock(self): - """ - Test creation of a representative block + """Test creation of a representative block. + + .. test:: Create representative blocks using a volume-weighted averaging. + :id: T_ARMI_XSGM_CREATE_REPR_BLOCKS0 + :tests: R_ARMI_XSGM_CREATE_REPR_BLOCKS """ avgB = self.bc.createRepresentativeBlock() self.assertNotIn(avgB, self.bc) @@ -174,7 +172,6 @@ def test_createRepresentativeBlockDissimilar(self): """ Test creation of a representative block from a collection with dissimilar blocks """ - uniqueBlock = test_blocks.loadTestBlock() uniqueBlock.p.percentBu = 50.0 fpFactory = test_lumpedFissionProduct.getDummyLFPFile() @@ -379,10 +376,10 @@ def setUp(self): self.expectedAreas = [[1, 6, 1], [1, 2, 1, 4]] def test_ComponentAverageRepBlock(self): - r""" - tests that the XS group manager calculates the expected component atom density - and component area correctly. Order of components is also checked since in - 1D cases the order of the components matters. + """Tests that the XS group manager calculates the expected component atom density + and component area correctly. + + Order of components is also checked since in 1D cases the order of the components matters. """ xsgm = self.o.getInterface("xsGroups") @@ -421,11 +418,11 @@ def test_ComponentAverageRepBlock(self): class TestBlockCollectionComponentAverage1DCylinder(unittest.TestCase): - r"""tests for 1D cylinder XS gen cases.""" + """tests for 1D cylinder XS gen cases.""" def setUp(self): - r""" - First part of setup same as test_Cartesian. + """First part of setup same as test_Cartesian. + Second part of setup builds lists/dictionaries of expected values to compare to. has expected values for component isotopic atom density and component area. """ @@ -498,10 +495,14 @@ def setUp(self): ] def test_ComponentAverage1DCylinder(self): - r""" - tests that the XS group manager calculates the expected component atom density - and component area correctly. Order of components is also checked since in - 1D cases the order of the components matters. + """Tests that the XS group manager calculates the expected component atom density + and component area correctly. + + Order of components is also checked since in 1D cases the order of the components matters. + + .. test:: Create representative blocks using custom cylindrical averaging. + :id: T_ARMI_XSGM_CREATE_REPR_BLOCKS1 + :tests: R_ARMI_XSGM_CREATE_REPR_BLOCKS """ xsgm = self.o.getInterface("xsGroups") @@ -546,7 +547,6 @@ def test_ComponentAverage1DCylinder(self): ) def test_checkComponentConsistency(self): - xsgm = self.o.getInterface("xsGroups") xsgm.interactBOL() blockCollectionsByXsGroup = xsgm.makeCrossSectionGroups() @@ -775,6 +775,12 @@ def test_getNextAvailableXsType(self): self.assertEqual("D", xsType3) def test_getRepresentativeBlocks(self): + """Test that we can create the representative blocks for a reactor. + + .. test:: Build representative blocks for a reactor. + :id: T_ARMI_XSGM_CREATE_XS_GROUPS + :tests: R_ARMI_XSGM_CREATE_XS_GROUPS + """ _o, r = test_reactors.loadTestReactor(TEST_ROOT) self.csm.r = r @@ -844,14 +850,24 @@ def test_createRepresentativeBlocksUsingExistingBlocks(self): self.assertEqual(origXSIDsFromNew["BA"], "AA") def test_interactBOL(self): - """Test `BOL` lattice physics update frequency.""" + """Test `BOL` lattice physics update frequency. + + .. test:: The XSGM frequency depends on the LPI frequency at BOL. + :id: T_ARMI_XSGM_FREQ0 + :tests: R_ARMI_XSGM_FREQ + """ self.blockList[0].r.p.timeNode = 0 self.csm.cs[CONF_LATTICE_PHYSICS_FREQUENCY] = "BOL" self.csm.interactBOL() self.assertTrue(self.csm.representativeBlocks) def test_interactBOC(self): - """Test `BOC` lattice physics update frequency.""" + """Test `BOC` lattice physics update frequency. + + .. test:: The XSGM frequency depends on the LPI frequency at BOC. + :id: T_ARMI_XSGM_FREQ1 + :tests: R_ARMI_XSGM_FREQ + """ self.blockList[0].r.p.timeNode = 0 self.csm.cs[CONF_LATTICE_PHYSICS_FREQUENCY] = "BOC" self.csm.interactBOL() @@ -859,7 +875,12 @@ def test_interactBOC(self): self.assertTrue(self.csm.representativeBlocks) def test_interactEveryNode(self): - """Test `everyNode` lattice physics update frequency.""" + """Test `everyNode` lattice physics update frequency. + + .. test:: The XSGM frequency depends on the LPI frequency at every time node. + :id: T_ARMI_XSGM_FREQ2 + :tests: R_ARMI_XSGM_FREQ + """ self.csm.cs[CONF_LATTICE_PHYSICS_FREQUENCY] = "BOC" self.csm.interactBOL() self.csm.interactEveryNode() @@ -870,7 +891,12 @@ def test_interactEveryNode(self): self.assertTrue(self.csm.representativeBlocks) def test_interactFirstCoupledIteration(self): - """Test `firstCoupledIteration` lattice physics update frequency.""" + """Test `firstCoupledIteration` lattice physics update frequency. + + .. test:: The XSGM frequency depends on the LPI frequency during first coupled iteration. + :id: T_ARMI_XSGM_FREQ3 + :tests: R_ARMI_XSGM_FREQ + """ self.csm.cs[CONF_LATTICE_PHYSICS_FREQUENCY] = "everyNode" self.csm.interactBOL() self.csm.interactCoupled(iteration=0) @@ -881,7 +907,12 @@ def test_interactFirstCoupledIteration(self): self.assertTrue(self.csm.representativeBlocks) def test_interactAllCoupled(self): - """Test `all` lattice physics update frequency.""" + """Test `all` lattice physics update frequency. + + .. test:: The XSGM frequency depends on the LPI frequency during coupling. + :id: T_ARMI_XSGM_FREQ4 + :tests: R_ARMI_XSGM_FREQ + """ self.csm.cs[CONF_LATTICE_PHYSICS_FREQUENCY] = "firstCoupledIteration" self.csm.interactBOL() self.csm.interactCoupled(iteration=1) From f2909b5588682ffc3592ad3d7ac6802e59611611 Mon Sep 17 00:00:00 2001 From: Chris Keckler Date: Tue, 28 Nov 2023 13:53:36 -0600 Subject: [PATCH 062/176] Fix units on various block params (#1498) --- armi/reactor/blockParameters.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/armi/reactor/blockParameters.py b/armi/reactor/blockParameters.py index 478c2cda8..54ba57ba5 100644 --- a/armi/reactor/blockParameters.py +++ b/armi/reactor/blockParameters.py @@ -449,7 +449,7 @@ def xsTypeNum(self, value): pb.defParam( "fuelWorth", - units=f"{units.REACTIVITY}/{units.KG})", + units=f"{units.REACTIVITY}/{units.KG}", description="Reactivity worth of fuel material per unit mass", ) @@ -491,13 +491,13 @@ def xsTypeNum(self, value): pb.defParam( "coolantWorth", - units=f"{units.REACTIVITY}/{units.KG})", + units=f"{units.REACTIVITY}/{units.KG}", description="Reactivity worth of coolant material per unit mass", ) pb.defParam( "cladWorth", - units=f"{units.REACTIVITY}/{units.KG})", + units=f"{units.REACTIVITY}/{units.KG}", description="Reactivity worth of clad material per unit mass", ) @@ -623,7 +623,7 @@ def xsTypeNum(self, value): # FUEL COEFFICIENTS pb.defParam( "rxFuelDensityCoeffPerMass", - units=f"{units.REACTIVITY}/{units.KG})", + units=f"{units.REACTIVITY}/{units.KG}", description="Fuel Density Coefficient", ) @@ -641,20 +641,20 @@ def xsTypeNum(self, value): pb.defParam( "rxFuelTemperatureCoeffPerMass", - units=f"{units.REACTIVITY}/{units.KG})", + units=f"{units.REACTIVITY}/{units.KG}", description="Fuel Temperature Coefficient", ) pb.defParam( "rxFuelVoidedTemperatureCoeffPerMass", - units=f"{units.REACTIVITY}/{units.KG})", + units=f"{units.REACTIVITY}/{units.KG}", description="Fuel Voided-Coolant Temperature Coefficient", ) # CLAD COEFFICIENTS pb.defParam( "rxCladDensityCoeffPerMass", - units=f"{units.REACTIVITY}/{units.KG})", + units=f"{units.REACTIVITY}/{units.KG}", description="Clad Density Coefficient", ) @@ -666,14 +666,14 @@ def xsTypeNum(self, value): pb.defParam( "rxCladTemperatureCoeffPerMass", - units=f"{units.REACTIVITY}/{units.KG})", + units=f"{units.REACTIVITY}/{units.KG}", description="Clad Temperature Coefficient", ) # STRUCTURE COEFFICIENTS pb.defParam( "rxStructureDensityCoeffPerMass", - units=f"{units.REACTIVITY}/{units.KG})", + units=f"{units.REACTIVITY}/{units.KG}", description="Structure Density Coefficient", ) @@ -685,20 +685,20 @@ def xsTypeNum(self, value): pb.defParam( "rxStructureTemperatureCoeffPerMass", - units=f"{units.REACTIVITY}/{units.KG})", + units=f"{units.REACTIVITY}/{units.KG}", description="Structure Temperature Coefficient", ) # COOLANT COEFFICIENTS pb.defParam( "rxCoolantDensityCoeffPerMass", - units=f"{units.REACTIVITY}/{units.KG})", + units=f"{units.REACTIVITY}/{units.KG}", description="Coolant Density Coefficient", ) pb.defParam( "rxCoolantTemperatureCoeffPerMass", - units=f"{units.REACTIVITY}/{units.KG})", + units=f"{units.REACTIVITY}/{units.KG}", description="Coolant Temperature Coefficient", ) From 037afbee09893f4c73311698a7e1668dcbf4ffb0 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 28 Nov 2023 16:02:01 -0800 Subject: [PATCH 063/176] Adding tests and crumbs for high-level framework concepts (#1503) --- armi/nuclearDataIO/tests/test_xsLibraries.py | 2 +- armi/operators/operator.py | 4 - armi/operators/tests/test_operators.py | 148 ++++++++++++++++-- .../tests/test_crossSectionManager.py | 2 +- .../tests/test_crossSectionSettings.py | 2 +- .../neutronics/tests/test_neutronicsPlugin.py | 2 +- .../parameters/parameterCollections.py | 2 +- .../parameters/parameterDefinitions.py | 10 +- armi/reactor/tests/test_blocks.py | 2 +- armi/reactor/tests/test_rz_reactors.py | 17 +- armi/settings/tests/test_settings.py | 32 ++-- armi/tests/test_apps.py | 17 +- armi/tests/test_interfaces.py | 4 + armi/tests/test_mpiFeatures.py | 21 ++- armi/tests/test_plugins.py | 62 +++++++- armi/tests/test_tests.py | 2 +- armi/utils/tests/test_densityTools.py | 2 +- armi/utils/tests/test_hexagon.py | 2 +- 18 files changed, 277 insertions(+), 56 deletions(-) diff --git a/armi/nuclearDataIO/tests/test_xsLibraries.py b/armi/nuclearDataIO/tests/test_xsLibraries.py index bbe29afe2..29c843619 100644 --- a/armi/nuclearDataIO/tests/test_xsLibraries.py +++ b/armi/nuclearDataIO/tests/test_xsLibraries.py @@ -235,7 +235,7 @@ def _canWritefromCombined(self, writer, refFile): self.assertTrue(filecmp.cmp(refFile, self.testFileName)) -class Test_GetISOTXSFilesInWorkingDirectory(unittest.TestCase): +class TestGetISOTXSFilesInWorkingDirectory(unittest.TestCase): def test_getISOTXSFilesWithoutLibrarySuffix(self): shouldBeThere = ["ISOAA", "ISOBA", os.path.join("file-path", "ISOCA")] shouldNotBeThere = [ diff --git a/armi/operators/operator.py b/armi/operators/operator.py index 663aca42a..4cf08a1d7 100644 --- a/armi/operators/operator.py +++ b/armi/operators/operator.py @@ -402,10 +402,6 @@ def _timeNodeLoop(self, cycle, timeNode): def _performTightCoupling(self, cycle: int, timeNode: int, writeDB: bool = True): """If requested, perform tight coupling and write out database. - .. impl:: The operator shall allow tight coupling between physics systems. - :id: I_ARMI_OPERATOR_PHYSICS - :implements: R_ARMI_OPERATOR_PHYSICS - Notes ----- writeDB is False for OperatorSnapshots as the DB gets written at EOL. diff --git a/armi/operators/tests/test_operators.py b/armi/operators/tests/test_operators.py index 55c795e8b..b1a7274e3 100644 --- a/armi/operators/tests/test_operators.py +++ b/armi/operators/tests/test_operators.py @@ -13,30 +13,32 @@ # limitations under the License. """Tests for operators.""" -import os -import unittest from unittest.mock import patch import collections +import io +import os +import sys +import unittest from armi import settings +from armi.bookkeeping.db.databaseInterface import DatabaseInterface from armi.interfaces import Interface, TightCoupler from armi.operators.operator import Operator -from armi.reactor.tests import test_reactors -from armi.settings.caseSettings import Settings -from armi.utils.directoryChangers import TemporaryDirectoryChanger from armi.physics.neutronics.globalFlux.globalFluxInterface import ( GlobalFluxInterfaceUsingExecuters, ) -from armi.utils import directoryChangers -from armi.bookkeeping.db.databaseInterface import DatabaseInterface -from armi.tests import mockRunLogs from armi.reactor.reactors import Reactor, Core +from armi.reactor.tests import test_reactors +from armi.settings.caseSettings import Settings from armi.settings.fwSettings.globalSettings import ( CONF_RUN_TYPE, CONF_TIGHT_COUPLING, CONF_CYCLES_SKIP_TIGHT_COUPLING_INTERACTION, CONF_TIGHT_COUPLING_SETTINGS, ) +from armi.tests import mockRunLogs +from armi.utils import directoryChangers +from armi.utils.directoryChangers import TemporaryDirectoryChanger class InterfaceA(Interface): @@ -56,12 +58,75 @@ class InterfaceC(Interface): name = "Third" -# TODO: Add a test that shows time evolution of Reactor (R_EVOLVING_STATE) class OperatorTests(unittest.TestCase): def setUp(self): self.o, self.r = test_reactors.loadTestReactor() self.activeInterfaces = [ii for ii in self.o.interfaces if ii.enabled()] + def test_operatorData(self): + """Test that the operator has input data, a reactor model. + + .. test:: The Operator includes input data and the reactor data model. + :id: T_ARMI_OPERATOR_COMM + :tests: R_ARMI_OPERATOR_COMM + """ + self.assertEqual(self.o.r, self.r) + self.assertEqual(type(self.o.cs), settings.Settings) + + @patch("armi.operators.Operator._interactAll") + def test_orderedInterfaces(self, interactAll): + """Test the default interfaces are in an ordered list, looped over at each time step. + + .. test:: An ordered list of interfaces are run at each time step. + :id: T_ARMI_OPERATOR_INTERFACES + :tests: R_ARMI_OPERATOR_INTERFACES + + .. test:: Interfaces are run at BOC, EOC, and at time points between. + :id: T_ARMI_INTERFACE + :tests: R_ARMI_INTERFACE + """ + # an ordered list of interfaces + self.assertGreater(len(self.o.interfaces), 0) + for i in self.o.interfaces: + self.assertTrue(isinstance(i, Interface)) + + # make sure we only iterate one time step + self.o.cs = self.o.cs.modified(newSettings={"nCycles": 1}) + self.r.p.cycle = 1 + + # mock some stdout logging of what's happening when + def sideEffect(node, activeInts): + print(node) + print(activeInts) + + interactAll.side_effect = sideEffect + + # run the operator through one cycle + origout = sys.stdout + try: + out = io.StringIO() + sys.stdout = out + self.o.operate() + finally: + sys.stdout = origout + + # check the outputs + log = out.getvalue() + # the BOL timestep comes before the EOL + self.assertIn("BOL", log) + self.assertIn("EOL", log.split("BOL")[-1]) + # we have some common interfaces listed + self.assertIn("main", log) + self.assertIn("fuelHandler", log) + self.assertIn("fissionProducts", log) + self.assertIn("history", log) + self.assertIn("snapshot", log) + # At the first time step, we get one ordered list of interfaces + interfaces = log.split("BOL")[1].split("EOL")[0].split(",") + self.assertGreater(len(interfaces), 0) + for i in interfaces: + self.assertIn("Interface", i) + def test_addInterfaceSubclassCollision(self): cs = settings.Settings() @@ -156,6 +221,34 @@ def test_snapshotRequest(self, fakeDirList, fakeCopy): self.assertTrue(os.path.exists("snapShot0_2")) +class TestCreateOperator(unittest.TestCase): + def test_createOperator(self): + """Test that an operator can be created from settings. + + .. test:: Create an operator from settings. + :id: T_ARMI_OPERATOR_SETTINGS + :tests: R_ARMI_OPERATOR_SETTINGS + """ + cs = settings.Settings() + o = Operator(cs) + # high-level items + self.assertTrue(isinstance(o, Operator)) + self.assertTrue(isinstance(o.cs, settings.Settings)) + + # validate some more nitty-gritty operator details come from settings + burnStepsSetting = cs["burnSteps"] + if not type(burnStepsSetting) == list: + burnStepsSetting = [burnStepsSetting] + self.assertEqual(o.burnSteps, burnStepsSetting) + self.assertEqual(o.maxBurnSteps, max(burnStepsSetting)) + + powerFracsSetting = cs["powerFractions"] + if powerFracsSetting: + self.assertEqual(o.powerFractions, powerFracsSetting) + else: + self.assertEqual(o.powerFractions, [[1] * cs["burnSteps"]]) + + class TestTightCoupling(unittest.TestCase): def setUp(self): self.cs = settings.Settings() @@ -164,6 +257,20 @@ def setUp(self): self.o.r = Reactor("empty", None) self.o.r.core = Core("empty") + def test_getStepLengths(self): + """Test the step lengths are correctly calculated, based on settings. + + .. test:: Users can control time discretization of the simulation through settings. + :id: T_ARMI_FW_HISTORY0 + :tests: R_ARMI_FW_HISTORY + """ + self.assertEqual(self.cs["nCycles"], 1) + self.assertAlmostEqual(self.cs["cycleLength"], 365.242199) + self.assertEqual(self.cs["burnSteps"], 4) + + self.assertEqual(len(self.o.stepLengths), 1) + self.assertEqual(len(self.o.stepLengths[0]), 4) + def test_couplingIsActive(self): """Ensure that ``cs[CONF_TIGHT_COUPLING]`` controls ``couplingIsActive``.""" self.assertTrue(self.o.couplingIsActive()) @@ -185,7 +292,12 @@ def test_performTightCoupling_skip(self): self.assertEqual(self.o.r.core.p.coupledIteration, 0) def test_performTightCoupling_notConverged(self): - """Ensure that the appropriate ``runLog.warning`` is addressed in tight coupling reaches max num of iters.""" + """Ensure that the appropriate ``runLog.warning`` is addressed in tight coupling reaches max num of iters. + + .. test:: The tight coupling logic can fail if there is no convergence. + :id: T_ARMI_OPERATOR_PHYSICS0 + :tests: R_ARMI_OPERATOR_PHYSICS + """ class NoConverge(TightCoupler): def isConverged(self, _val: TightCoupler._SUPPORTED_TYPES) -> bool: @@ -345,11 +457,25 @@ def test_getAvailabilityFactors(self): ) def test_getStepLengths(self): - self.assertEqual(self.detailedOperator.stepLengths, self.stepLengthsSolution) + """Test that the manually-set, detailed time steps are retrievable. + .. test:: Users can manually control time discretization of the simulation. + :id: T_ARMI_FW_HISTORY1 + :tests: R_ARMI_FW_HISTORY + """ + # detailed step lengths can be set manually + self.assertEqual(self.detailedOperator.stepLengths, self.stepLengthsSolution) self.detailedOperator._stepLength = None self.assertEqual(self.detailedOperator.stepLengths, self.stepLengthsSolution) + # when doing detailed step information, we don't get step information from settings + cs = self.detailedOperator.cs + self.assertEqual(cs["nCycles"], 3) + with self.assertRaises(ValueError): + cs["cycleLength"] + with self.assertRaises(ValueError): + cs["burnSteps"] + def test_getCycleLengths(self): self.assertEqual(self.detailedOperator.cycleLengths, self.cycleLengthsSolution) diff --git a/armi/physics/neutronics/tests/test_crossSectionManager.py b/armi/physics/neutronics/tests/test_crossSectionManager.py index 66f2dcc13..1b1e99b35 100644 --- a/armi/physics/neutronics/tests/test_crossSectionManager.py +++ b/armi/physics/neutronics/tests/test_crossSectionManager.py @@ -701,7 +701,7 @@ def test_invalidWeights(self): self.bc.createRepresentativeBlock() -class Test_CrossSectionGroupManager(unittest.TestCase): +class TestCrossSectionGroupManager(unittest.TestCase): def setUp(self): cs = settings.Settings() self.blockList = makeBlocks(20) diff --git a/armi/physics/neutronics/tests/test_crossSectionSettings.py b/armi/physics/neutronics/tests/test_crossSectionSettings.py index 4f82b9566..631af79c1 100644 --- a/armi/physics/neutronics/tests/test_crossSectionSettings.py +++ b/armi/physics/neutronics/tests/test_crossSectionSettings.py @@ -207,7 +207,7 @@ def test_badCrossSections(self): xsSettingsValidator({"AAA": {CONF_BLOCK_REPRESENTATION: "Average"}}) -class Test_XSSettings(unittest.TestCase): +class TestXSSettings(unittest.TestCase): def test_yamlIO(self): """Ensure we can read/write this custom setting object to yaml.""" yaml = YAML() diff --git a/armi/physics/neutronics/tests/test_neutronicsPlugin.py b/armi/physics/neutronics/tests/test_neutronicsPlugin.py index dabeb0c2d..d10c510af 100644 --- a/armi/physics/neutronics/tests/test_neutronicsPlugin.py +++ b/armi/physics/neutronics/tests/test_neutronicsPlugin.py @@ -53,7 +53,7 @@ """ -class Test_NeutronicsPlugin(TestPlugin): +class TestNeutronicsPlugin(TestPlugin): plugin = neutronics.NeutronicsPlugin def setUp(self): diff --git a/armi/reactor/parameters/parameterCollections.py b/armi/reactor/parameters/parameterCollections.py index be380617b..0163a416e 100644 --- a/armi/reactor/parameters/parameterCollections.py +++ b/armi/reactor/parameters/parameterCollections.py @@ -83,7 +83,7 @@ def __new__(mcl, name, bases, attrs): class ParameterCollection(metaclass=_ParameterCollectionType): - r"""An empty class for holding state information in the ARMI data structure. + """An empty class for holding state information in the ARMI data structure. A parameter collection stores one or more formally-defined values ("parameters"). Until a given ParameterCollection subclass has been instantiated, new parameters may diff --git a/armi/reactor/parameters/parameterDefinitions.py b/armi/reactor/parameters/parameterDefinitions.py index 2844dce0b..daa51afc9 100644 --- a/armi/reactor/parameters/parameterDefinitions.py +++ b/armi/reactor/parameters/parameterDefinitions.py @@ -465,7 +465,7 @@ def __getitem__(self, name): return matches[0] def add(self, paramDef): - r"""Add a :py:class:`Parameter` to this collection.""" + """Add a :py:class:`Parameter` to this collection.""" assert not self._locked, "This ParameterDefinitionCollection has been locked." self._paramDefs.append(paramDef) self._paramDefDict[paramDef.name, paramDef.collectionType] = paramDef @@ -527,7 +527,7 @@ def unchanged_since(self, mask): return self._filter(lambda pd: not (pd.assigned & mask)) def forType(self, compositeType): - r""" + """ Create a :py:class:`ParameterDefinitionCollection` that contains definitions for a specific composite type. """ @@ -553,16 +553,16 @@ def setAssignmentFlag(self, mask): pd.assigned |= mask def byNameAndType(self, name, compositeType): - r"""Get a :py:class:`Parameter` by compositeType and name.""" + """Get a :py:class:`Parameter` by compositeType and name.""" return self._paramDefDict[name, compositeType.paramCollectionType] def byNameAndCollectionType(self, name, collectionType): - r"""Get a :py:class:`Parameter` by collectionType and name.""" + """Get a :py:class:`Parameter` by collectionType and name.""" return self._paramDefDict[name, collectionType] @property def categories(self): - r"""Get the categories of all the :py:class:`~Parameter` instances within this collection.""" + """Get the categories of all the :py:class:`~Parameter` instances within this collection.""" categories = set() for paramDef in self: categories |= paramDef.categories diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py index 810a72bcc..99bc74342 100644 --- a/armi/reactor/tests/test_blocks.py +++ b/armi/reactor/tests/test_blocks.py @@ -1771,7 +1771,7 @@ def test_getReactionRates(self): ) -class Test_NegativeVolume(unittest.TestCase): +class TestNegativeVolume(unittest.TestCase): def test_negativeVolume(self): """Build a block with WAY too many fuel pins and show that the derived volume is negative.""" block = blocks.HexBlock("TestHexBlock") diff --git a/armi/reactor/tests/test_rz_reactors.py b/armi/reactor/tests/test_rz_reactors.py index 261817aac..b99b6c6fb 100644 --- a/armi/reactor/tests/test_rz_reactors.py +++ b/armi/reactor/tests/test_rz_reactors.py @@ -22,7 +22,7 @@ from armi.reactor import reactors -class Test_RZT_Reactor(unittest.TestCase): +class TestRZTReactor(unittest.TestCase): """Tests for RZT reactors.""" @classmethod @@ -38,8 +38,7 @@ def test_loadRZT(self): self.assertTrue(all(aziMesh == 8 for aziMesh in aziMeshes)) def test_findAllMeshPoints(self): - """ - Test findAllMeshPoints(). + """Test findAllMeshPoints(). .. test:: Test that the reactor can calculate its core block mesh. :id: T_ARMI_R_MESH @@ -49,14 +48,14 @@ def test_findAllMeshPoints(self): self.assertLess(i[-1], 2 * math.pi) -class Test_RZT_Reactor_modern(unittest.TestCase): +class TestRZTReactorModern(unittest.TestCase): def test_loadRZT_reactor(self): """ The Godiva benchmark model is a HEU sphere with a radius of 8.74 cm. This unit tests loading and verifies the reactor is loaded correctly by comparing volumes against expected volumes for full core (including - void boundary conditions) and just the fuel + void boundary conditions) and just the fuel. """ cs = settings.Settings( fName=os.path.join(TEST_ROOT, "Godiva.armi.unittest.yaml") @@ -77,15 +76,11 @@ def test_loadRZT_reactor(self): for c in b: if "Godiva" in c.name: fuelVolumes.append(c.getVolume()) - """ - verify the total reactor volume is as expected - """ + # verify the total reactor volume is as expected tolerance = 1e-3 error = math.fabs((refReactorVolume - sum(reactorVolumes)) / refReactorVolume) self.assertLess(error, tolerance) - """ - verify the total fuel volume is as expected - """ + # verify the total fuel volume is as expected error = math.fabs((refFuelVolume - sum(fuelVolumes)) / refFuelVolume) self.assertLess(error, tolerance) diff --git a/armi/settings/tests/test_settings.py b/armi/settings/tests/test_settings.py index 460f290cd..bb90a5277 100644 --- a/armi/settings/tests/test_settings.py +++ b/armi/settings/tests/test_settings.py @@ -41,7 +41,7 @@ THIS_DIR = os.path.dirname(__file__) -class DummyPlugin1(plugins.ArmiPlugin): +class DummySettingPlugin1(plugins.ArmiPlugin): @staticmethod @plugins.HOOKIMPL def defineSettings(): @@ -54,11 +54,17 @@ def defineSettings(): description="The neutronics / depletion solver for global flux solve.", enforcedOptions=True, options=["DEFAULT", "OTHER"], - ) + ), + setting.Setting( + "avocado", + default=0, + label="Avocados", + description="Avocados are delicious.", + ), ] -class DummyPlugin2(plugins.ArmiPlugin): +class DummySettingPlugin2(plugins.ArmiPlugin): @staticmethod @plugins.HOOKIMPL def defineSettings(): @@ -234,34 +240,42 @@ def test_pluginValidatorsAreDiscovered(self): ) def test_pluginSettings(self): + """Test settings change depending on what plugins are registered. + + .. test:: Registering a plugin can change what settings exist. + :id: T_ARMI_PLUGIN_SETTINGS + :tests: R_ARMI_PLUGIN_SETTINGS + """ pm = getPluginManagerOrFail() - pm.register(DummyPlugin1) + pm.register(DummySettingPlugin1) # We have a setting; this should be fine cs = caseSettings.Settings() self.assertEqual(cs["extendableOption"], "DEFAULT") + self.assertEqual(cs["avocado"], 0) # We shouldn't have any settings from the other plugin, so this should be an # error. with self.assertRaises(vol.error.MultipleInvalid): newSettings = {"extendableOption": "PLUGIN"} cs = cs.modified(newSettings=newSettings) - pm.register(DummyPlugin2) + pm.register(DummySettingPlugin2) cs = caseSettings.Settings() self.assertEqual(cs["extendableOption"], "PLUGIN") # Now we should have the option from plugin 2; make sure that works cs = cs.modified(newSettings=newSettings) cs["extendableOption"] = "PLUGIN" self.assertIn("extendableOption", cs.keys()) - pm.unregister(DummyPlugin2) - pm.unregister(DummyPlugin1) + pm.unregister(DummySettingPlugin2) + pm.unregister(DummySettingPlugin1) # Now try the same, but adding the plugins in a different order. This is to make # sure that it doesnt matter if the Setting or its Options come first - pm.register(DummyPlugin2) - pm.register(DummyPlugin1) + pm.register(DummySettingPlugin2) + pm.register(DummySettingPlugin1) cs = caseSettings.Settings() self.assertEqual(cs["extendableOption"], "PLUGIN") + self.assertEqual(cs["avocado"], 0) def test_default(self): """ diff --git a/armi/tests/test_apps.py b/armi/tests/test_apps.py index 6b534b545..f092324e5 100644 --- a/armi/tests/test_apps.py +++ b/armi/tests/test_apps.py @@ -212,15 +212,26 @@ def test_disableFutureConfigures(self): armi._ignoreConfigures = old -class TestArmi(unittest.TestCase): +class TestArmiHighLevel(unittest.TestCase): """Tests for functions in the ARMI __init__ module.""" - def test_getDefaultPlugMan(self): + def test_getDefaultPluginManager(self): + """Test the default plugin manager. + + .. test:: The default application consists of a list of default plugins. + :id: T_ARMI_APP_PLUGINS + :tests: R_ARMI_APP_PLUGINS + """ pm = getDefaultPluginManager() pm2 = getDefaultPluginManager() self.assertNotEqual(pm, pm2) - self.assertIn(cli.EntryPointsPlugin, pm.get_plugins()) + pluginsList = "".join([str(p) for p in pm.get_plugins()]) + + self.assertIn("BookkeepingPlugin", pluginsList) + self.assertIn("EntryPointsPlugin", pluginsList) + self.assertIn("NeutronicsPlugin", pluginsList) + self.assertIn("ReactorPlugin", pluginsList) def test_overConfigured(self): with self.assertRaises(RuntimeError): diff --git a/armi/tests/test_interfaces.py b/armi/tests/test_interfaces.py index b5924af5e..21395c6ff 100644 --- a/armi/tests/test_interfaces.py +++ b/armi/tests/test_interfaces.py @@ -127,6 +127,10 @@ def test_isConvergedValueError(self): def test_isConverged(self): """Ensure TightCoupler.isConverged() works with float, 1D list, and ragged 2D list. + .. test:: The tight coupling logic is based around a convergence criteria. + :id: T_ARMI_OPERATOR_PHYSICS1 + :tests: R_ARMI_OPERATOR_PHYSICS + Notes ----- 2D lists can end up being ragged as assemblies can have different number of blocks. diff --git a/armi/tests/test_mpiFeatures.py b/armi/tests/test_mpiFeatures.py index 4ee46b494..abf16fa10 100644 --- a/armi/tests/test_mpiFeatures.py +++ b/armi/tests/test_mpiFeatures.py @@ -25,6 +25,7 @@ mpiexec.exe -n 2 python -m pytest armi/tests/test_mpiFeatures.py """ from distutils.spawn import find_executable +from unittest.mock import patch import os import unittest @@ -40,6 +41,7 @@ from armi.reactor.parameters import parameterDefinitions from armi.reactor.tests import test_reactors from armi.tests import ARMI_RUN_PATH, TEST_ROOT +from armi.tests import mockRunLogs from armi.utils import pathTools from armi.utils.directoryChangers import TemporaryDirectoryChanger @@ -97,12 +99,27 @@ def setUp(self): self.o = OperatorMPI(cs=self.old_op.cs) self.o.r = self.r + @patch("armi.operators.Operator.operate") @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") - def test_basicOperatorMPI(self): - self.o.operate() + def test_basicOperatorMPI(self, mockOpMpi): + """Test we can drive a parallel operator. + + .. test:: Run a parallel operator. + :id: T_ARMI_OPERATOR_MPI0 + :tests: R_ARMI_OPERATOR_MPI + """ + with mockRunLogs.BufferLog() as mock: + self.o.operate() + self.assertIn("OperatorMPI.operate", mock.getStdout()) @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") def test_primaryException(self): + """Test a custom interface that only fails on the main process. + + .. test:: Run a parallel operator that fails online on the main process. + :id: T_ARMI_OPERATOR_MPI1 + :tests: R_ARMI_OPERATOR_MPI + """ self.o.removeAllInterfaces() failer = FailingInterface1(self.o.r, self.o.cs) self.o.addInterface(failer) diff --git a/armi/tests/test_plugins.py b/armi/tests/test_plugins.py index ede37c965..6c6d53a5d 100644 --- a/armi/tests/test_plugins.py +++ b/armi/tests/test_plugins.py @@ -18,9 +18,68 @@ import yamlize +from armi import getPluginManagerOrFail from armi import interfaces from armi import plugins from armi import settings +from armi.physics.neutronics import NeutronicsPlugin +from armi.reactor import parameters +from armi.reactor.blocks import Block, HexBlock +from armi.reactor.parameters import ParamLocation +from armi.reactor.parameters.parameterCollections import collectPluginParameters +from armi.utils import units + + +class TestPluginBasics(unittest.TestCase): + def test_defineParameters(self): + """Test that the default ARMI plugins are correctly defining parameters. + + .. test:: ARMI plugins define parameters, which appear on a new Block. + :id: T_ARMI_PLUGIN_PARAMS + :tests: R_ARMI_PLUGIN_PARAMS + """ + # create a block + b = Block("fuel", height=10.0) + + # unless a plugin has registerd a param, it doesn't exist + with self.assertRaises(AttributeError): + b.p.fakeParam + + # Check the default values of parameters defined by the neutronics plugin + self.assertIsNone(b.p.axMesh) + self.assertEqual(b.p.flux, 0) + self.assertEqual(b.p.power, 0) + self.assertEqual(b.p.pdens, 0) + + # Check the default values of parameters defined by the fuel peformance plugin + self.assertEqual(b.p.gasPorosity, 0) + self.assertEqual(b.p.liquidPorosity, 0) + + def test_exposeInterfaces(self): + """Make sure that the exposeInterfaces hook is properly implemented. + + .. test:: Plugins can add interfaces to the interface stack. + :id: T_ARMI_PLUGIN_INTERFACES + :tests: R_ARMI_PLUGIN_INTERFACES + """ + plugin = NeutronicsPlugin() + + cs = settings.Settings() + results = plugin.exposeInterfaces(cs) + + # each plugin should return a list + self.assertIsInstance(results, list) + self.assertGreater(len(results), 0) + for result in results: + # Make sure all elements in the list satisfy the constraints of the hookspec + self.assertIsInstance(result, tuple) + self.assertEqual(len(result), 3) + + order, interface, kwargs = result + + self.assertIsInstance(order, (int, float)) + self.assertTrue(issubclass(interface, interfaces.Interface)) + self.assertIsInstance(kwargs, dict) class TestPlugin(unittest.TestCase): @@ -62,8 +121,7 @@ def test_exposeInterfaces(self): # each plugin should return a list self.assertIsInstance(results, list) for result in results: - # Make sure that all elements in the list satisfy the constraints of the - # hookspec + # Make sure all elements in the list satisfy the constraints of the hookspec self.assertIsInstance(result, tuple) self.assertEqual(len(result), 3) diff --git a/armi/tests/test_tests.py b/armi/tests/test_tests.py index 938cd03a8..25f471bf4 100644 --- a/armi/tests/test_tests.py +++ b/armi/tests/test_tests.py @@ -18,7 +18,7 @@ from armi import tests -class Test_CompareFiles(unittest.TestCase): +class TestCompareFiles(unittest.TestCase): def test_compareFileLine(self): expected = "oh look, a number! 3.14 and some text and another number 1.5" diff --git a/armi/utils/tests/test_densityTools.py b/armi/utils/tests/test_densityTools.py index 0727d0949..75b2d2160 100644 --- a/armi/utils/tests/test_densityTools.py +++ b/armi/utils/tests/test_densityTools.py @@ -19,7 +19,7 @@ from armi.utils import densityTools -class Test_densityTools(unittest.TestCase): +class TestDensityTools(unittest.TestCase): def test_expandElementalMassFracsToNuclides(self): """ Expand mass fraction to nuclides. diff --git a/armi/utils/tests/test_hexagon.py b/armi/utils/tests/test_hexagon.py index 31ae3b9c6..ea0873f87 100644 --- a/armi/utils/tests/test_hexagon.py +++ b/armi/utils/tests/test_hexagon.py @@ -18,7 +18,7 @@ from armi.utils import hexagon -class Test_hexagon(unittest.TestCase): +class TestHexagon(unittest.TestCase): def test_hexagon_area(self): """ Area of a hexagon. From 75e024697c1c6f422db31abf23e1cd8cf7f5f41d Mon Sep 17 00:00:00 2001 From: bdlafleur Date: Tue, 28 Nov 2023 21:41:40 -0600 Subject: [PATCH 064/176] Adding crumbs for shuffle logic (#1499) --- armi/physics/fuelCycle/assemblyRotationAlgorithms.py | 4 ++++ armi/physics/fuelCycle/fuelHandlerInterface.py | 5 +++++ armi/physics/fuelCycle/fuelHandlers.py | 12 ++++++++++++ .../tests/test_assemblyRotationAlgorithms.py | 6 ++++++ armi/physics/fuelCycle/tests/test_fuelHandlers.py | 11 ++++++++++- 5 files changed, 37 insertions(+), 1 deletion(-) diff --git a/armi/physics/fuelCycle/assemblyRotationAlgorithms.py b/armi/physics/fuelCycle/assemblyRotationAlgorithms.py index a56d30827..2bf3bd0d4 100644 --- a/armi/physics/fuelCycle/assemblyRotationAlgorithms.py +++ b/armi/physics/fuelCycle/assemblyRotationAlgorithms.py @@ -77,6 +77,10 @@ def simpleAssemblyRotation(fh): """ Rotate all pin-detail assemblies that were just shuffled by 60 degrees. + .. impl:: An assembly can be rotated about its z-axis. + :id: I_ARMI_SHUFFLE_ROTATE + :implements: R_ARMI_SHUFFLE_ROTATE + Parameters ---------- fh : FuelHandler object diff --git a/armi/physics/fuelCycle/fuelHandlerInterface.py b/armi/physics/fuelCycle/fuelHandlerInterface.py index aa7098bbd..31d17917f 100644 --- a/armi/physics/fuelCycle/fuelHandlerInterface.py +++ b/armi/physics/fuelCycle/fuelHandlerInterface.py @@ -31,6 +31,11 @@ class FuelHandlerInterface(interfaces.Interface): power or temperatures have been updated. This allows pre-run fuel management steps for highly customized fuel loadings. In typical runs, no fuel management occurs at the beginning of the first cycle and the as-input state is left as is. + + .. impl:: ARMI provides a shuffle logic interface. + :id: I_ARMI_SHUFFLE + :implements: R_ARMI_SHUFFLE + """ name = "fuelHandler" diff --git a/armi/physics/fuelCycle/fuelHandlers.py b/armi/physics/fuelCycle/fuelHandlers.py index 2c05fa9d4..2fe524263 100644 --- a/armi/physics/fuelCycle/fuelHandlers.py +++ b/armi/physics/fuelCycle/fuelHandlers.py @@ -714,6 +714,14 @@ def swapAssemblies(self, a1, a2): r""" 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 + + .. impl:: User-specified blocks can be left in place and not moved. + :id: I_ARMI_SHUFFLE_STATIONARY0 + :implements: R_ARMI_SHUFFLE_STATIONARY + Parameters ---------- a1 : Assembly @@ -803,6 +811,10 @@ def dischargeSwap(self, incoming, outgoing): r""" Removes one assembly from the core and replace it with another assembly. + .. impl:: User-specified blocks can be left in place and not moved. + :id: I_ARMI_SHUFFLE_STATIONARY1 + :implements: R_ARMI_SHUFFLE_STATIONARY + See Also -------- swapAssemblies : swaps assemblies that are already in the core diff --git a/armi/physics/fuelCycle/tests/test_assemblyRotationAlgorithms.py b/armi/physics/fuelCycle/tests/test_assemblyRotationAlgorithms.py index ccf7929ca..f1bbe5667 100644 --- a/armi/physics/fuelCycle/tests/test_assemblyRotationAlgorithms.py +++ b/armi/physics/fuelCycle/tests/test_assemblyRotationAlgorithms.py @@ -49,6 +49,12 @@ def test_buReducingAssemblyRotation(self): self.assertNotEqual(b.getRotationNum(), rotNum) def test_simpleAssemblyRotation(self): + """Test rotating an assembly. + + .. test:: An assembly can be rotated about its z-axis. + :id: T_ARMI_SHUFFLE_ROTATE + :tests: R_ARMI_SHUFFLE_ROTATE + """ fh = fuelHandlers.FuelHandler(self.o) newSettings = {CONF_ASSEM_ROTATION_STATIONARY: True} self.o.cs = self.o.cs.modified(newSettings=newSettings) diff --git a/armi/physics/fuelCycle/tests/test_fuelHandlers.py b/armi/physics/fuelCycle/tests/test_fuelHandlers.py index 764ef22cb..797024acc 100644 --- a/armi/physics/fuelCycle/tests/test_fuelHandlers.py +++ b/armi/physics/fuelCycle/tests/test_fuelHandlers.py @@ -390,6 +390,10 @@ def test_repeatShuffles(self): Checks some other things in the meantime + .. test:: There is a user friendly shuffle logic interface. + :id: T_ARMI_SHUFFLE + :tests: R_ARMI_SHUFFLE + See Also -------- runShuffling : creates the shuffling file to be read in. @@ -522,7 +526,12 @@ def test_linPowByPinGamma(self): self.assertEqual(type(b.p.linPowByPinGamma), np.ndarray) def test_transferStationaryBlocks(self): - """Test the _transferStationaryBlocks method.""" + """Test the _transferStationaryBlocks method. + + .. test:: User-specified blocks can remain in place during shuffling + :id: T_ARMI_SHUFFLE_STATIONARY + :tests: R_ARMI_SHUFFLE_STATIONARY + """ # grab stationary block flags sBFList = self.r.core.stationaryBlockFlagsList From cc59b0069412671f92a0bdb16f703e40f6b18084 Mon Sep 17 00:00:00 2001 From: bdlafleur Date: Wed, 29 Nov 2023 08:39:32 -0600 Subject: [PATCH 065/176] Adding miscellaneous crumbs (#1505) --- .../neutronics/globalFlux/globalFluxInterface.py | 4 ++++ .../globalFlux/tests/test_globalFluxInterface.py | 13 +++++++++++++ armi/reactor/converters/geometryConverters.py | 11 ++++++++++- .../converters/tests/test_geometryConverters.py | 7 ++++++- armi/reactor/tests/test_reactors.py | 6 ++++++ 5 files changed, 39 insertions(+), 2 deletions(-) diff --git a/armi/physics/neutronics/globalFlux/globalFluxInterface.py b/armi/physics/neutronics/globalFlux/globalFluxInterface.py index 752ffdfe7..d4ace8ff9 100644 --- a/armi/physics/neutronics/globalFlux/globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/globalFluxInterface.py @@ -1174,6 +1174,10 @@ def computeDpaRate(mgFlux, dpaXs): r""" Compute the DPA rate incurred by exposure of a certain flux spectrum. + .. impl:: Compute DPA and DPA rates. + :id: I_ARMI_FLUX_DPA + :implements: R_ARMI_FLUX_DPA + Parameters ---------- mgFlux : list diff --git a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py index 84837ff73..a7726ba26 100644 --- a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py @@ -141,6 +141,19 @@ def test_savePhysicsFiles(self): class TestGlobalFluxInterface(unittest.TestCase): + def test_computeDpaRate(self): + """ + Compute DPA and DPA rates from multi-group neutron flux and cross sections. + + .. test:: Compute DPA and DPA rates. + :id: T_ARMI_FLUX_DPA + :tests: R_ARMI_FLUX_DPA + """ + xs = [1, 2, 3] + flx = [0.5, 0.75, 2] + res = globalFluxInterface.computeDpaRate(flx, xs) + self.assertEqual(res, 10**-24 * (0.5 + 1.5 + 6)) + def test_interaction(self): """ Ensure the basic interaction hooks work. diff --git a/armi/reactor/converters/geometryConverters.py b/armi/reactor/converters/geometryConverters.py index 0f1d5ef4e..e9b8777c6 100644 --- a/armi/reactor/converters/geometryConverters.py +++ b/armi/reactor/converters/geometryConverters.py @@ -1243,6 +1243,10 @@ def convert(self, r): """ Run the conversion. + .. impl:: Convert a one-third-core geometry to a full-core geometry. + :id: I_ARMI_THIRD_TO_FULL_CORE0 + :implements: R_ARMI_THIRD_TO_FULL_CORE + Parameters ---------- sourceReactor : Reactor object @@ -1329,7 +1333,12 @@ def convert(self, r): ) def restorePreviousGeometry(self, r=None): - """Undo the changes made by convert by going back to 1/3 core.""" + """Undo the changes made by convert by going back to 1/3 core. + + .. impl:: Restore a one-third-core geometry to a full-core geometry. + :id: I_ARMI_THIRD_TO_FULL_CORE1 + :implements: R_ARMI_THIRD_TO_FULL_CORE + """ r = r or self._sourceReactor # remove the assemblies that were added when the conversion happened. diff --git a/armi/reactor/converters/tests/test_geometryConverters.py b/armi/reactor/converters/tests/test_geometryConverters.py index a3021ee15..f704262b7 100644 --- a/armi/reactor/converters/tests/test_geometryConverters.py +++ b/armi/reactor/converters/tests/test_geometryConverters.py @@ -332,7 +332,12 @@ def tearDown(self): del self.r def test_growToFullCoreFromThirdCore(self): - """Test that a hex core can be converted from a third core to a full core geometry.""" + """Test that a hex core can be converted from a third core to a full core geometry. + + .. test:: Convert a third-core to a full-core geometry and then restore it. + :id: T_ARMI_THIRD_TO_FULL_CORE0 + :tests: R_ARMI_THIRD_TO_FULL_CORE + """ # Check the initialization of the third core model self.assertFalse(self.r.core.isFullCore) self.assertEqual( diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 673791a9a..53fb0ef77 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -743,6 +743,12 @@ def test_getAssemblyWithName(self): self.assertEqual(a1, a2) def test_restoreReactor(self): + """Restore a reactor after growing it from third to full core. + + .. test:: Convert a third-core to a full-core geometry and then restore it. + :id: T_ARMI_THIRD_TO_FULL_CORE1 + :tests: R_ARMI_THIRD_TO_FULL_CORE + """ aListLength = len(self.r.core.getAssemblies()) converter = self.r.core.growToFullCore(self.o.cs) converter.restorePreviousGeometry(self.r) From f566dbf9da9adaf2ec9a77961edd448df759d5ae Mon Sep 17 00:00:00 2001 From: bdlafleur Date: Wed, 29 Nov 2023 10:07:49 -0600 Subject: [PATCH 066/176] Adding crumbs for istopic depletion (#1500) --- .../tests/test_fissionProductModel.py | 7 ++++++- .../isotopicDepletion/crossSectionTable.py | 8 ++++++++ .../isotopicDepletion/isotopicDepletionInterface.py | 10 +++++++++- .../neutronics/tests/test_crossSectionTable.py | 13 +++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/armi/physics/neutronics/fissionProductModel/tests/test_fissionProductModel.py b/armi/physics/neutronics/fissionProductModel/tests/test_fissionProductModel.py index 3be98d29e..48408e49e 100644 --- a/armi/physics/neutronics/fissionProductModel/tests/test_fissionProductModel.py +++ b/armi/physics/neutronics/fissionProductModel/tests/test_fissionProductModel.py @@ -126,7 +126,12 @@ def test_nuclidesInModelFuel(self): self.assertIn(nb.name, nuclideList) def test_nuclidesInModelAllDepletableBlocks(self): - """Test that the depletable blocks contain all the MC2-3 modeled nuclides.""" + """Test that the depletable blocks contain all the MC2-3 modeled nuclides. + + .. test:: Determine if any component is depletable. + :id: T_ARMI_DEPL_DEPLETABLE + :tests: R_ARMI_DEPL_DEPLETABLE + """ # Check that there are some fuel and control blocks in the core model. fuelBlocks = self.r.core.getBlocks(Flags.FUEL) controlBlocks = self.r.core.getBlocks(Flags.CONTROL) diff --git a/armi/physics/neutronics/isotopicDepletion/crossSectionTable.py b/armi/physics/neutronics/isotopicDepletion/crossSectionTable.py index fa719a9db..3af26af80 100644 --- a/armi/physics/neutronics/isotopicDepletion/crossSectionTable.py +++ b/armi/physics/neutronics/isotopicDepletion/crossSectionTable.py @@ -39,6 +39,10 @@ class CrossSectionTable(collections.OrderedDict): XStable is indexed by nucNames (nG), (nF), (n2n), (nA), (nP) and (n3n) are expected the cross sections are returned in barns + + .. impl:: Generate cross section table. + :id: I_ARMI_DEPL_TABLES0 + :implements: R_ARMI_DEPL_TABLES """ rateTypes = ("nG", "nF", "n2n", "nA", "nP", "n3n") @@ -163,6 +167,10 @@ def makeReactionRateTable(obj, nuclides: List = None): Often useful in support of depletion. + .. impl:: Generate cross section table. + :id: I_ARMI_DEPL_TABLES1 + :implements: R_ARMI_DEPL_TABLES + Parameters ---------- nuclides : list, optional diff --git a/armi/physics/neutronics/isotopicDepletion/isotopicDepletionInterface.py b/armi/physics/neutronics/isotopicDepletion/isotopicDepletionInterface.py index 4e641c744..185eca2e5 100644 --- a/armi/physics/neutronics/isotopicDepletion/isotopicDepletionInterface.py +++ b/armi/physics/neutronics/isotopicDepletion/isotopicDepletionInterface.py @@ -41,6 +41,10 @@ def isDepletable(obj: composites.ArmiObject): to figure out how often to replace them. But in conceptual design, they may want to just leave them as they are as an approximation. + .. impl:: Determine if any component is depletable. + :id: I_ARMI_DEPL_DEPLETABLE + :implements: R_ARMI_DEPL_DEPLETABLE + .. warning:: The ``DEPLETABLE`` flag is automatically added to compositions that have active nuclides. If you explicitly define any flags at all, you must also manually include ``DEPLETABLE`` or else the objects will silently not deplete. @@ -68,12 +72,16 @@ class AbstractIsotopicDepleter: interface The depletion in this analysis only depends on the flux, material vectors, - nuclear data and countinuous source and loss objects. + nuclear data and continuous source and loss objects. The depleters derived from this abstract class use all the fission products armi can handle -- i.e. do not form lumped fission products. _depleteByName contains a ARMI objects to deplete keyed by name. + + .. impl:: ARMI provides a base class to deplete isotopes. + :id: I_ARMI_DEPL_ABC + :implements: R_ARMI_DEPL_ABC """ name = None diff --git a/armi/physics/neutronics/tests/test_crossSectionTable.py b/armi/physics/neutronics/tests/test_crossSectionTable.py index 159d19cf0..b77387606 100644 --- a/armi/physics/neutronics/tests/test_crossSectionTable.py +++ b/armi/physics/neutronics/tests/test_crossSectionTable.py @@ -29,6 +29,12 @@ class TestCrossSectionTable(unittest.TestCase): def test_makeTable(self): + """Test making a cross section table. + + .. test:: Generate cross section table. + :id: T_ARMI_DEPL_TABLES + :tests: R_ARMI_DEPL_TABLES + """ obj = loadTestBlock() obj.p.mgFlux = range(33) core = obj.getAncestorWithFlags(Flags.CORE) @@ -47,6 +53,13 @@ def test_makeTable(self): self.assertIn("mcnpId", xSecTable[-1]) def test_isotopicDepletionInterface(self): + """ + Test isotopic depletion interface. + + .. test:: ARMI provides a base class to deplete isotopes. + :id: T_ARMI_DEPL_ABC + :tests: R_ARMI_DEPL_ABC + """ _o, r = loadTestReactor() cs = Settings() From 0c4d5f4c4353f670ce6096b239af360addcc6fde Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Wed, 29 Nov 2023 09:38:06 -0800 Subject: [PATCH 067/176] Adding impl/test crumbs (#1506) --- armi/bookkeeping/db/database3.py | 4 +-- armi/bookkeeping/db/layout.py | 4 +-- armi/materials/mixture.py | 2 +- armi/reactor/blocks.py | 11 ++++-- armi/reactor/components/__init__.py | 8 +++-- armi/reactor/components/component.py | 1 - .../converters/axialExpansionChanger.py | 4 +++ .../tests/test_axialExpansionChanger.py | 29 ++++++++++----- armi/reactor/converters/uniformMesh.py | 2 +- armi/reactor/tests/test_blocks.py | 10 ++++-- armi/reactor/tests/test_components.py | 36 +++++++++++++++++++ 11 files changed, 89 insertions(+), 22 deletions(-) diff --git a/armi/bookkeeping/db/database3.py b/armi/bookkeeping/db/database3.py index c2a60d892..3b9a30767 100644 --- a/armi/bookkeeping/db/database3.py +++ b/armi/bookkeeping/db/database3.py @@ -672,9 +672,9 @@ def load( continue with new settings (or if blueprints are not on the database). Geometry is read from the database itself. - .. test:: Users can load a reactor from a DB. + .. impl:: Users can load a reactor from a DB. :id: I_ARMI_DB_R_LOAD - :tests: R_ARMI_DB_R_LOAD + :implements: R_ARMI_DB_R_LOAD Parameters ---------- diff --git a/armi/bookkeeping/db/layout.py b/armi/bookkeeping/db/layout.py index 347e28155..c7c5c1c7c 100644 --- a/armi/bookkeeping/db/layout.py +++ b/armi/bookkeeping/db/layout.py @@ -383,9 +383,9 @@ def _initComps(self, caseTitle, bp): def writeToDB(self, h5group): """Write a chunk of data to the database. - .. test:: Write data to the DB for a given time step. + .. impl:: Write data to the DB for a given time step. :id: I_ARMI_DB_TIME - :tests: R_ARMI_DB_TIME + :implements: R_ARMI_DB_TIME """ if "layout/type" in h5group: # It looks like we have already written the layout to DB, skip for now diff --git a/armi/materials/mixture.py b/armi/materials/mixture.py index b2f3d7d44..b3a7c7c50 100644 --- a/armi/materials/mixture.py +++ b/armi/materials/mixture.py @@ -33,5 +33,5 @@ class _Mixture(materials.Material): See Also -------- - armi.reactor.blocks.HexBlock._createHomogenizedCopy + armi.reactor.blocks.HexBlock.createHomogenizedCopy """ diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py index 43fbaa078..dbab8b45f 100644 --- a/armi/reactor/blocks.py +++ b/armi/reactor/blocks.py @@ -157,7 +157,7 @@ def __deepcopy__(self, memo): return b - def _createHomogenizedCopy(self, pinSpatialLocators=False): + def createHomogenizedCopy(self, pinSpatialLocators=False): """ Create a copy of a block. @@ -1081,7 +1081,12 @@ def getSortedComponentsInsideOfComponent(self, component): return sortedComponents def getNumPins(self): - """Return the number of pins in this block.""" + """Return the number of pins in this block. + + .. impl:: Get the number of pins in a block; potentially zero. + :id: I_ARMI_BLOCK_NPINS + :implements: R_ARMI_BLOCK_NPINS + """ nPins = [ sum( [ @@ -1624,7 +1629,7 @@ def coords(self, rotationDegreesCCW=0.0): round(y, units.FLOAT_DIMENSION_DECIMALS), ) - def _createHomogenizedCopy(self, pinSpatialLocators=False): + def createHomogenizedCopy(self, pinSpatialLocators=False): """ Create a new homogenized copy of a block that is less expensive than a full deepcopy. diff --git a/armi/reactor/components/__init__.py b/armi/reactor/components/__init__.py index 9d3e940d2..a7fef9038 100644 --- a/armi/reactor/components/__init__.py +++ b/armi/reactor/components/__init__.py @@ -323,7 +323,12 @@ def getBoundingCircleOuterDiameter(self, Tc=None, cold=False): return math.sqrt(4.0 * self.getComponentArea() / math.pi) def computeVolume(self): - """Cannot compute volume until it is derived.""" + """Cannot compute volume until it is derived. + + .. impl:: The volume of a DerivedShape depends on the solid shapes surrounding them. + :id: I_ARMI_COMP_FLUID + :implements: R_ARMI_COMP_FLUID + """ return self._deriveVolumeAndArea() def _deriveVolumeAndArea(self): @@ -412,7 +417,6 @@ def getVolume(self): ------- float volume of component in cm^3. - """ if self.parent.derivedMustUpdate: # tell _updateVolume to update it during the below getVolume call diff --git a/armi/reactor/components/component.py b/armi/reactor/components/component.py index 6ce144ba5..2ce5b6e18 100644 --- a/armi/reactor/components/component.py +++ b/armi/reactor/components/component.py @@ -519,7 +519,6 @@ def _checkNegativeArea(self, area, cold): Overlapping is allowed to maintain conservation of atoms while sticking close to the as-built geometry. Modules that need true geometries will have to handle this themselves. - """ if numpy.isnan(area): return diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index 5f0818df6..aed698541 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -110,6 +110,10 @@ class AxialExpansionChanger: """ Axially expand or contract assemblies or an entire core. + .. impl:: Performing axial expansion on solid components within a compatible ARMI assembly. + :id: I_ARMI_AXIAL_EXP + :implements: R_ARMI_AXIAL_EXP + Attributes ---------- linked : :py:class:`AssemblyAxialLinkage` diff --git a/armi/reactor/converters/tests/test_axialExpansionChanger.py b/armi/reactor/converters/tests/test_axialExpansionChanger.py index f40503b4b..0e296c601 100644 --- a/armi/reactor/converters/tests/test_axialExpansionChanger.py +++ b/armi/reactor/converters/tests/test_axialExpansionChanger.py @@ -249,8 +249,12 @@ def expandAssemForMassConservationTest(self): ) self._getConservationMetrics(self.a) - def test_ThermalExpansionContractionConservation_Simple(self): - r"""Thermally expand and then contract to ensure original state is recovered. + def test_thermalExpansionContractionConservation_simple(self): + """Thermally expand and then contract to ensure original state is recovered. + + .. test:: Thermally expand and then contract to ensure original assembly is recovered. + :id: T_ARMI_AXIAL_EXP_THERM0 + :tests: R_ARMI_AXIAL_EXP_THERM Notes ----- @@ -285,8 +289,8 @@ def test_ThermalExpansionContractionConservation_Simple(self): self._checkMass(origMasses, newMasses) self._checkNDens(origNDens, newNDens, 1.0) - def test_ThermalExpansionContractionConservation_Complex(self): - r"""Thermally expand and then contract to ensure original state is recovered. + def test_thermalExpansionContractionConservation_complex(self): + """Thermally expand and then contract to ensure original state is recovered. Notes ----- @@ -351,9 +355,13 @@ def _getMass(a): newMass = a.getMass("B10") return newMass - def test_PrescribedExpansionContractionConservation(self): + def test_prescribedExpansionContractionConservation(self): """Expand all components and then contract back to original state. + .. test:: Expand all components and then contract back to original state. + :id: T_ARMI_AXIAL_EXP_PRESC0 + :tests: R_ARMI_AXIAL_EXP_PRESC + Notes ----- - uniform expansion over all components within the assembly @@ -412,7 +420,7 @@ def _getComponentMassAndNDens(a): nDens[c] = c.getNumberDensities() return masses, nDens - def test_TargetComponentMassConservation(self): + def test_targetComponentMassConservation(self): """Tests mass conservation for target components.""" self.expandAssemForMassConservationTest() for cName, masses in self.componentMass.items(): @@ -434,8 +442,13 @@ def test_TargetComponentMassConservation(self): msg="Total assembly steel mass is not conserved.", ) - def test_NoMovementACLP(self): - """Ensures that above core load pad (ACLP) does not move during fuel-only expansion.""" + def test_noMovementACLP(self): + """Ensures the above core load pad (ACLP) does not move during fuel-only expansion. + + .. test:: Ensure the ACLP does not move during fuel-only expansion. + :id: T_ARMI_AXIAL_EXP_PRESC1 + :tests: R_ARMI_AXIAL_EXP_PRESC + """ # build test assembly with ACLP assembly = HexAssembly("testAssemblyType") assembly.spatialGrid = grids.axialUnitGrid(numCells=1) diff --git a/armi/reactor/converters/uniformMesh.py b/armi/reactor/converters/uniformMesh.py index 57fc2cd87..e8b945bea 100644 --- a/armi/reactor/converters/uniformMesh.py +++ b/armi/reactor/converters/uniformMesh.py @@ -720,7 +720,7 @@ def checkPriorityFlags(b): heightFrac = h / totalHeight runLog.debug(f"XSType {xs}: {heightFrac:.4f}") - block = sourceBlock._createHomogenizedCopy(includePinCoordinates) + block = sourceBlock.createHomogenizedCopy(includePinCoordinates) block.p.xsType = xsType block.setHeight(topMeshPoint - bottom) block.p.axMesh = 1 diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py index 99bc74342..b5f8c4957 100644 --- a/armi/reactor/tests/test_blocks.py +++ b/armi/reactor/tests/test_blocks.py @@ -436,7 +436,7 @@ def test_setType(self): self.assertFalse(self.block.hasFlags(Flags.IGNITER | Flags.FUEL)) def test_duplicate(self): - Block2 = blocks.Block._createHomogenizedCopy(self.block) + Block2 = blocks.Block.createHomogenizedCopy(self.block) originalComponents = self.block.getComponents() newComponents = Block2.getComponents() for c1, c2 in zip(originalComponents, newComponents): @@ -487,7 +487,7 @@ def test_homogenizedMixture(self): ] for arg, shapes in zip(args, expectedShapes): - homogBlock = self.block._createHomogenizedCopy(pinSpatialLocators=arg) + homogBlock = self.block.createHomogenizedCopy(pinSpatialLocators=arg) for shapeType in shapes: for c in homogBlock.getComponents(): if isinstance(c, shapeType): @@ -1306,6 +1306,12 @@ def test_getNumComponents(self): self.assertEqual(1, self.block.getNumComponents(Flags.DUCT)) def test_getNumPins(self): + """Test that we can get the number of pins from various blocks. + + .. test:: Retrieve the number of pins from various blocks. + :id: T_ARMI_BLOCK_NPINS + :tests: R_ARMI_BLOCK_NPINS + """ cur = self.block.getNumPins() ref = self.block.getDim(Flags.FUEL, "mult") self.assertEqual(cur, ref) diff --git a/armi/reactor/tests/test_components.py b/armi/reactor/tests/test_components.py index 8337896a3..f67e5f019 100644 --- a/armi/reactor/tests/test_components.py +++ b/armi/reactor/tests/test_components.py @@ -217,13 +217,25 @@ def test_solid_material(self): .. test:: Determine if material is solid. :id: T_ARMI_COMP_SOLID :tests: R_ARMI_COMP_SOLID + + .. test:: Components have material properties. + :id: T_ARMI_COMP_MAT + :tests: R_ARMI_COMP_MAT """ + self.assertTrue(isinstance(self.component.getProperties(), Material)) + self.assertTrue(hasattr(self.component.material, "density")) + self.assertIn("HT9", str(self.component.getProperties())) + self.component.material = air.Air() self.assertFalse(self.component.containsSolidMaterial()) self.component.material = alloy200.Alloy200() self.assertTrue(self.component.containsSolidMaterial()) + self.assertTrue(isinstance(self.component.getProperties(), Material)) + self.assertTrue(hasattr(self.component.material, "density")) + self.assertIn("Alloy200", str(self.component.getProperties())) + class TestNullComponent(TestGeneralComponents): componentCls = NullComponent @@ -419,6 +431,30 @@ def test_getBoundingCircleOuterDiameter(self): self.component.getBoundingCircleOuterDiameter(cold=True), 0.0 ) + def test_computeVolume(self): + """Test the computeVolume method on a number of components in a block. + + .. test:: Compute the volume of a DerivedShape inside solid shapes. + :id: T_ARMI_COMP_FLUID + :tests: R_ARMI_COMP_FLUID + """ + from armi.reactor.tests.test_blocks import buildSimpleFuelBlock + + # Calculate the total volume of the block + b = buildSimpleFuelBlock() + totalVolume = b.getVolume() + + # calculate the total volume by adding up all the components + c = b.getComponent(flags.Flags.COOLANT) + totalByParts = 0 + for co in b.getComponents(): + totalByParts += co.computeVolume() + + self.assertAlmostEqual(totalByParts, totalVolume) + + # test the computeVolume method on the one DerivedShape in thi block + self.assertAlmostEqual(c.computeVolume(), 1386.5232044586771) + class TestCircle(TestShapedComponent): """Test circle shaped component.""" From 7de33eb4ba87846764e73895044ab1cbd3ae8539 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Wed, 29 Nov 2023 15:38:05 -0800 Subject: [PATCH 068/176] Adding impl/test crumbs for requirements (#1507) --- armi/physics/neutronics/energyGroups.py | 11 ++++++++++- .../neutronics/globalFlux/globalFluxInterface.py | 9 +++++++-- .../globalFlux/tests/test_globalFluxInterface.py | 5 ++--- armi/physics/neutronics/tests/test_energyGroups.py | 11 ++++++++++- armi/reactor/assemblies.py | 9 +++++++-- armi/reactor/blueprints/componentBlueprint.py | 4 ++++ armi/reactor/blueprints/isotopicOptions.py | 4 ++++ armi/reactor/blueprints/tests/test_blockBlueprints.py | 2 +- armi/reactor/blueprints/tests/test_blueprints.py | 7 ++++++- armi/reactor/converters/axialExpansionChanger.py | 4 ++++ armi/reactor/converters/geometryConverters.py | 10 +++++++--- .../converters/tests/test_geometryConverters.py | 10 ++++++++-- armi/reactor/grids/structuredgrid.py | 4 ++-- 13 files changed, 72 insertions(+), 18 deletions(-) diff --git a/armi/physics/neutronics/energyGroups.py b/armi/physics/neutronics/energyGroups.py index 5ecda047d..3f430326a 100644 --- a/armi/physics/neutronics/energyGroups.py +++ b/armi/physics/neutronics/energyGroups.py @@ -30,7 +30,12 @@ def getFastFluxGroupCutoff(eGrpStruc): - """Given a constant "fast" energy threshold, return which ARMI energy group index contains this threshold.""" + """Given a constant "fast" energy threshold, return which ARMI energy group index contains this threshold. + + .. impl:: Return the energy group index which contains a given energy threshold. + :id: I_ARMI_EG_FE + :implements: R_ARMI_EG_FE + """ gThres = -1 for g, eV in enumerate(eGrpStruc): if eV < FAST_FLUX_THRESHOLD_EV: @@ -67,6 +72,10 @@ def getGroupStructure(name): """ Return descending neutron energy group upper bounds in eV for a given structure name. + .. impl:: Provide the neutron energy group bounds for a given group structure. + :id: I_ARMI_EG_NE + :implements: R_ARMI_EG_NE + Notes ----- Copy of the group structure is return so that modifications of the energy bounds does diff --git a/armi/physics/neutronics/globalFlux/globalFluxInterface.py b/armi/physics/neutronics/globalFlux/globalFluxInterface.py index d4ace8ff9..8f4a293dc 100644 --- a/armi/physics/neutronics/globalFlux/globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/globalFluxInterface.py @@ -119,8 +119,13 @@ def interactEOC(self, cycle=None): * units.ABS_REACTIVITY_TO_PCM ) - def _checkEnergyBalance(self): - """Check that there is energy balance between the power generated and the specified power is the system.""" + def checkEnergyBalance(self): + """Check that there is energy balance between the power generated and the specified power. + + .. impl:: Validate the energy generate matches user specifications. + :id: I_ARMI_FLUX_CHECK_POWER + :implements: R_ARMI_FLUX_CHECK_POWER + """ powerGenerated = ( self.r.core.calcTotalParam( "power", calcBasedOnFullObj=False, generationNum=2 diff --git a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py index a7726ba26..2fd627a34 100644 --- a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py @@ -180,8 +180,7 @@ def test_getHistoryParams(self): self.assertIn("detailedDpa", params) def test_checkEnergyBalance(self): - """ - Test energy balance check. + """Test energy balance check. .. test:: Block-wise power is consistent with reactor data model power. :id: T_ARMI_FLUX_CHECK_POWER @@ -190,7 +189,7 @@ def test_checkEnergyBalance(self): cs = settings.Settings() _o, r = test_reactors.loadTestReactor() gfi = MockGlobalFluxInterface(r, cs) - gfi._checkEnergyBalance() + gfi.checkEnergyBalance() class TestGlobalFluxInterfaceWithExecuters(unittest.TestCase): diff --git a/armi/physics/neutronics/tests/test_energyGroups.py b/armi/physics/neutronics/tests/test_energyGroups.py index 23f7d903f..6cb2b9673 100644 --- a/armi/physics/neutronics/tests/test_energyGroups.py +++ b/armi/physics/neutronics/tests/test_energyGroups.py @@ -20,7 +20,12 @@ class TestEnergyGroups(unittest.TestCase): def test_invalidGroupStructureType(self): - """Test that the reverse lookup fails on non-existent energy group bounds.""" + """Test that the reverse lookup fails on non-existent energy group bounds. + + .. test:: Check the neutron energy group bounds logic fails correctly for the wrong structure. + :id: T_ARMI_EG_NE0 + :tests: R_ARMI_EG_NE + """ modifier = 1e-5 for groupStructureType in energyGroups.GROUP_STRUCTURE: energyBounds = energyGroups.getGroupStructure(groupStructureType) @@ -32,6 +37,10 @@ def test_consistenciesBetweenGroupStructureAndGroupStructureType(self): """ Test that the reverse lookup of the energy group structures work. + .. test:: Check the neutron energy group bounds for a given group structure. + :id: T_ARMI_EG_NE1 + :tests: R_ARMI_EG_NE + Notes ----- Several group structures point to the same energy group structure so the reverse lookup will fail to diff --git a/armi/reactor/assemblies.py b/armi/reactor/assemblies.py index bff1866e9..66e3c1459 100644 --- a/armi/reactor/assemblies.py +++ b/armi/reactor/assemblies.py @@ -180,7 +180,6 @@ def add(self, obj: blocks.Block): .. impl:: Assemblies are made up of type Block. :id: I_ARMI_ASSEM_BLOCKS :implements: R_ARMI_ASSEM_BLOCKS - """ composites.Composite.add(self, obj) obj.spatialLocator = self.spatialGrid[0, 0, len(self) - 1] @@ -227,7 +226,6 @@ def getLocation(self): .. impl:: Assembly location is retrievable. :id: I_ARMI_ASSEM_POSI0 :implements: R_ARMI_ASSEM_POSI - """ # just use ring and position, not axial (which is 0) if not self.parent: @@ -1229,6 +1227,13 @@ def rotate(self, rad): class HexAssembly(Assembly): + """Placeholder, so users can explicitly define a hex-based assembly. + + .. impl:: Assembly of hex blocks. + :id: I_ARMI_ASSEM_HEX + :implements: R_ARMI_ASSEM_HEX + """ + pass diff --git a/armi/reactor/blueprints/componentBlueprint.py b/armi/reactor/blueprints/componentBlueprint.py index 244229517..f37e2f8a3 100644 --- a/armi/reactor/blueprints/componentBlueprint.py +++ b/armi/reactor/blueprints/componentBlueprint.py @@ -291,6 +291,10 @@ def insertDepletableNuclideKeys(c, blueprint): """ Auto update number density keys on all DEPLETABLE components. + .. impl:: Insert any depletable blueprint flags onto this component. + :id: I_ARMI_BP_NUC_FLAGS0 + :implements: R_ARMI_BP_NUC_FLAGS + Notes ----- This should be moved to a neutronics/depletion plugin hook but requires some diff --git a/armi/reactor/blueprints/isotopicOptions.py b/armi/reactor/blueprints/isotopicOptions.py index 0e37b4cfb..ead85253f 100644 --- a/armi/reactor/blueprints/isotopicOptions.py +++ b/armi/reactor/blueprints/isotopicOptions.py @@ -61,6 +61,10 @@ class NuclideFlag(yamlize.Object): physics option. However, restarting from that case with different cross section needs is challenging. + .. impl:: The blueprint object that represents a nuclide flag. + :id: I_ARMI_BP_NUC_FLAGS1 + :implements: R_ARMI_BP_NUC_FLAGS + Attributes ---------- nuclideName : str diff --git a/armi/reactor/blueprints/tests/test_blockBlueprints.py b/armi/reactor/blueprints/tests/test_blockBlueprints.py index cbab444fd..a06964b59 100644 --- a/armi/reactor/blueprints/tests/test_blockBlueprints.py +++ b/armi/reactor/blueprints/tests/test_blockBlueprints.py @@ -310,7 +310,7 @@ def test_explicitFlags(self): Test flags are created from blueprint file. .. test:: Nuc flags can define depletable objects. - :id: T_ARMI_BP_NUC_FLAGS + :id: T_ARMI_BP_NUC_FLAGS0 :tests: R_ARMI_BP_NUC_FLAGS """ a1 = self.blueprints.assemDesigns.bySpecifier["IC"].construct( diff --git a/armi/reactor/blueprints/tests/test_blueprints.py b/armi/reactor/blueprints/tests/test_blueprints.py index 9661ad820..a96fceadd 100644 --- a/armi/reactor/blueprints/tests/test_blueprints.py +++ b/armi/reactor/blueprints/tests/test_blueprints.py @@ -102,7 +102,12 @@ def test_componentDimensions(self): self.assertAlmostEqual(fuel.getDimension("mult"), 169) def test_traceNuclides(self): - """Ensure that armi.reactor.blueprints.componentBlueprint.insertDepletableNuclideKeys runs.""" + """Ensure that armi.reactor.blueprints.componentBlueprint.insertDepletableNuclideKeys runs. + + .. test:: Users marking components as depletable will affect number densities. + :id: T_ARMI_BP_NUC_FLAGS1 + :tests: R_ARMI_BP_NUC_FLAGS + """ fuel = ( self.blueprints.constructAssem(self.cs, "igniter fuel") .getFirstBlock(Flags.FUEL) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index aed698541..3b5f391bc 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -114,6 +114,10 @@ class AxialExpansionChanger: :id: I_ARMI_AXIAL_EXP :implements: R_ARMI_AXIAL_EXP + .. impl:: Preserve the total height of an ARMI assembly, during expansion. + :id: I_ARMI_ASSEM_HEIGHT_PRES + :implements: R_ARMI_ASSEM_HEIGHT_PRES + Attributes ---------- linked : :py:class:`AssemblyAxialLinkage` diff --git a/armi/reactor/converters/geometryConverters.py b/armi/reactor/converters/geometryConverters.py index e9b8777c6..418f320a9 100644 --- a/armi/reactor/converters/geometryConverters.py +++ b/armi/reactor/converters/geometryConverters.py @@ -1194,11 +1194,15 @@ def reset(self): class HexToRZConverter(HexToRZThetaConverter): - r""" + """ Create a new reactor with R-Z coordinates from the Hexagonal-Z reactor. - This is a subclass of the HexToRZThetaConverter. See the HexToRZThetaConverter for explanation and setup of - the converterSettings. + This is a subclass of the HexToRZThetaConverter. See the HexToRZThetaConverter for + explanation and setup of the converterSettings. + + .. impl:: Tool to convert a hex core to an RZTheta core. + :id: I_ARMI_CONV_3DHEX_TO_2DRZ + :implements: R_ARMI_CONV_3DHEX_TO_2DRZ """ _GEOMETRY_TYPE = geometry.GeomType.RZ diff --git a/armi/reactor/converters/tests/test_geometryConverters.py b/armi/reactor/converters/tests/test_geometryConverters.py index f704262b7..5a1b52c97 100644 --- a/armi/reactor/converters/tests/test_geometryConverters.py +++ b/armi/reactor/converters/tests/test_geometryConverters.py @@ -40,7 +40,7 @@ def setUp(self): self.cs = self.o.cs def test_addRing(self): - r"""Tests that the addRing method adds the correct number of fuel assemblies to the test reactor.""" + """Tests that the addRing method adds the correct number of fuel assemblies to the test reactor.""" converter = geometryConverters.FuelAssemNumModifier(self.cs) converter.numFuelAssems = 7 converter.ringsToAdd = 1 * ["radial shield"] @@ -65,7 +65,7 @@ def test_addRing(self): ) # should wind up with 11 reflector assemblies per 1/3rd core def test_setNumberOfFuelAssems(self): - r"""Tests that the setNumberOfFuelAssems method properly changes the number of fuel assemblies.""" + """Tests that the setNumberOfFuelAssems method properly changes the number of fuel assemblies.""" # tests ability to add fuel assemblies converter = geometryConverters.FuelAssemNumModifier(self.cs) converter.numFuelAssems = 60 @@ -142,6 +142,12 @@ def tearDown(self): del self.r def test_convert(self): + """Test the HexToRZConverter. + + .. test:: Convert a 3D hex reactor core to an RZ-Theta core. + :id: T_ARMI_CONV_3DHEX_TO_2DRZ + :tests: R_ARMI_CONV_3DHEX_TO_2DRZ + """ # make the reactor smaller, because of a test parallelization edge case for ring in [9, 8, 7, 6, 5, 4, 3]: self.r.core.removeAssembliesInRing(ring, self.o.cs) diff --git a/armi/reactor/grids/structuredgrid.py b/armi/reactor/grids/structuredgrid.py index 7b74eaadd..f37c95433 100644 --- a/armi/reactor/grids/structuredgrid.py +++ b/armi/reactor/grids/structuredgrid.py @@ -292,9 +292,9 @@ def restoreBackup(self): def getCoordinates(self, indices, nativeCoords=False) -> numpy.ndarray: """Return the coordinates of the center of the mesh cell at the given indices in cm. - .. test:: Get the coordinates from a location in a grid. + .. impl:: Get the coordinates from a location in a grid. :id: I_ARMI_GRID_GLOBAL_POS - :tests: R_ARMI_GRID_GLOBAL_POS + :implements: R_ARMI_GRID_GLOBAL_POS """ indices = numpy.array(indices) return self._evaluateMesh( From 956c51d45df02178c20c0e41dc64de6d3790e07f Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Wed, 29 Nov 2023 16:02:56 -0800 Subject: [PATCH 069/176] Adding some impl crumbs to docstrings (#1508) --- armi/nuclearDataIO/xsCollections.py | 5 +++++ armi/physics/neutronics/macroXSGenerationInterface.py | 4 ++++ armi/reactor/blocks.py | 4 ++++ 3 files changed, 13 insertions(+) diff --git a/armi/nuclearDataIO/xsCollections.py b/armi/nuclearDataIO/xsCollections.py index 3f1357568..e453a016b 100644 --- a/armi/nuclearDataIO/xsCollections.py +++ b/armi/nuclearDataIO/xsCollections.py @@ -381,6 +381,7 @@ def __init__( def createMacrosOnBlocklist( self, microLibrary, blockList, nucNames=None, libType="micros" ): + """Create macroscopic cross sections for a list of blocks.""" for block in blockList: block.macros = self.createMacrosFromMicros( microLibrary, block, nucNames, libType=libType @@ -788,6 +789,10 @@ def computeMacroscopicGroupConstants( """ Compute any macroscopic group constants given number densities and a microscopic library. + .. impl:: Compute macroscopic cross sections from microscopic cross sections and number densities. + :id: I_ARMI_NUCDATA_MACRO + :implements: R_ARMI_NUCDATA_MACRO + Parameters ---------- constantName : str diff --git a/armi/physics/neutronics/macroXSGenerationInterface.py b/armi/physics/neutronics/macroXSGenerationInterface.py index 0303cb92f..1a69f55c0 100644 --- a/armi/physics/neutronics/macroXSGenerationInterface.py +++ b/armi/physics/neutronics/macroXSGenerationInterface.py @@ -144,6 +144,10 @@ def buildMacros( Builds G-vectors of the basic XS ('nGamma','fission','nalph','np','n2n','nd','nt') Builds GxG matrices for scatter matrices + .. impl:: Build macroscopic cross sections for blocks. + :id: I_ARMI_MACRO_XS + :implements: R_ARMI_MACRO_XS + Parameters ---------- lib : library object , optional diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py index dbab8b45f..4d83e827d 100644 --- a/armi/reactor/blocks.py +++ b/armi/reactor/blocks.py @@ -1561,6 +1561,10 @@ def rotate(self, rad): def setAxialExpTargetComp(self, targetComponent): """Sets the targetComponent for the axial expansion changer. + .. impl:: Set the target axial expansion components on a given block. + :id: I_ARMI_MANUAL_TARG_COMP + :implements: R_ARMI_MANUAL_TARG_COMP + Parameter --------- targetComponent: :py:class:`Component ` object From 1330ad15704d590dcd883eae0bdb47fb2d356ce7 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 30 Nov 2023 14:15:18 -0800 Subject: [PATCH 070/176] Adding impl/test crumbs for 5 reqs (#1509) --- .../globalFlux/globalFluxInterface.py | 4 +++ .../tests/test_globalFluxInterface.py | 15 +++++++++++ .../tests/test_latticeInterface.py | 27 ++++++++++++++++--- armi/physics/neutronics/settings.py | 7 ++++- armi/reactor/blueprints/assemblyBlueprint.py | 4 +++ armi/reactor/blueprints/componentBlueprint.py | 7 ++++- armi/reactor/blueprints/isotopicOptions.py | 4 +++ .../tests/test_componentBlueprint.py | 4 +-- .../tests/test_materialModifications.py | 21 +++++++++++++++ armi/reactor/grids/locations.py | 8 ++++++ armi/reactor/grids/tests/test_grids.py | 8 ++++++ armi/reactor/tests/test_blocks.py | 7 +++++ 12 files changed, 109 insertions(+), 7 deletions(-) diff --git a/armi/physics/neutronics/globalFlux/globalFluxInterface.py b/armi/physics/neutronics/globalFlux/globalFluxInterface.py index 8f4a293dc..9aa8e1c2c 100644 --- a/armi/physics/neutronics/globalFlux/globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/globalFluxInterface.py @@ -519,6 +519,10 @@ class GlobalFluxExecuter(executers.DefaultExecuter): and copying certain user-defined files back to the working directory on error or completion. Given all these options and possible needs for information from global flux, this class provides a unified interface to everything. + + .. impl:: Ensure the mesh in the reactor model is appropriate for neutronics solver execution. + :id: I_ARMI_FLUX_GEOM_TRANSFORM + :implements: R_ARMI_FLUX_GEOM_TRANSFORM """ def __init__(self, options: GlobalFluxOptions, reactor): diff --git a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py index 2fd627a34..3d152ab6a 100644 --- a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py @@ -205,6 +205,12 @@ def setUp(self): self.gfi = MockGlobalFluxWithExecuters(self.r, self.cs) def test_executerInteraction(self): + """Run the global flux interface and executer though one time now. + + .. test:: Run the global flux interface to prove the mesh in the reactor is suffience for the neutronics solver. + :id: T_ARMI_FLUX_GEOM_TRANSFORM1 + :tests: R_ARMI_FLUX_GEOM_TRANSFORM + """ gfi, r = self.gfi, self.r gfi.interactBOC() gfi.interactEveryNode(0, 0) @@ -262,6 +268,15 @@ def setUpClass(cls): cls.gfi = MockGlobalFluxWithExecutersNonUniform(cls.r, cs) def test_executerInteractionNonUniformAssems(self): + """Run the global flux interface with non-uniform assemblies. + + This will serve as a broad end-to-end test of the interface, and also + stress test the mesh issues with non-uniform assemblies. + + .. test:: Run the global flux interface to prove the mesh in the reactor is suffience for the neutronics solver. + :id: T_ARMI_FLUX_GEOM_TRANSFORM0 + :tests: R_ARMI_FLUX_GEOM_TRANSFORM + """ gfi, r = self.gfi, self.r gfi.interactBOC() gfi.interactEveryNode(0, 0) diff --git a/armi/physics/neutronics/latticePhysics/tests/test_latticeInterface.py b/armi/physics/neutronics/latticePhysics/tests/test_latticeInterface.py index 8b79b3536..b88c8cf58 100644 --- a/armi/physics/neutronics/latticePhysics/tests/test_latticeInterface.py +++ b/armi/physics/neutronics/latticePhysics/tests/test_latticeInterface.py @@ -20,9 +20,12 @@ LatticePhysicsInterface, ) from armi import settings +from armi.nuclearDataIO.cccc import isotxs from armi.operators.operator import Operator +from armi.physics.neutronics import LatticePhysicsFrequency from armi.physics.neutronics.crossSectionGroupManager import CrossSectionGroupManager from armi.physics.neutronics.settings import CONF_GEN_XS +from armi.physics.neutronics.settings import CONF_GLOBAL_FLUX_ACTIVE from armi.reactor.reactors import Reactor, Core from armi.reactor.tests.test_blocks import buildSimpleFuelBlock from armi.tests import mockRunLogs @@ -30,8 +33,6 @@ HexAssembly, grids, ) -from armi.nuclearDataIO.cccc import isotxs -from armi.physics.neutronics import LatticePhysicsFrequency from armi.tests import ISOAA_PATH # As an interface, LatticePhysicsInterface must be subclassed to be used @@ -86,7 +87,27 @@ def setUp(self): self.o.r.core.lib = "Nonsense" self.latticeInterface.testVerification = False - def test_LatticePhysicsInterface(self): + def test_includeGammaXS(self): + """Test that we can correctly flip the switch to calculate gamma XS. + + .. test:: Users can flip a setting to determine if gamma XS are generated. + :id: T_ARMI_GAMMA_XS + :tests: R_ARMI_GAMMA_XS + """ + # The default operator here turns off Gamma XS generation + self.assertFalse(self.latticeInterface.includeGammaXS) + self.assertEqual(self.o.cs[CONF_GLOBAL_FLUX_ACTIVE], "Neutron") + + # but we can create an operator that turns on Gamma XS generation + cs = settings.Settings().modified( + newSettings={CONF_GLOBAL_FLUX_ACTIVE: "Neutron and Gamma"} + ) + newOperator = Operator(cs) + newLatticeInterface = LatticeInterfaceTesterLibFalse(newOperator.r, cs) + self.assertTrue(newLatticeInterface.includeGammaXS) + self.assertEqual(cs[CONF_GLOBAL_FLUX_ACTIVE], "Neutron and Gamma") + + def test_latticePhysicsInterface(self): """Super basic test of the LatticePhysicsInterface.""" self.assertEqual(self.latticeInterface._updateBlockNeutronVelocities, True) self.assertEqual(self.latticeInterface.executablePath, "/tmp/fake_path") diff --git a/armi/physics/neutronics/settings.py b/armi/physics/neutronics/settings.py index 46139d961..f00e72700 100644 --- a/armi/physics/neutronics/settings.py +++ b/armi/physics/neutronics/settings.py @@ -83,7 +83,12 @@ def defineSettings(): - """Standard function to define settings - for neutronics.""" + """Standard function to define settings - for neutronics. + + .. impl:: Users to select if gamma cross sections are generated. + :id: I_ARMI_GAMMA_XS + :implements: R_ARMI_GAMMA_XS + """ settings = [ setting.Setting( CONF_GROUP_STRUCTURE, diff --git a/armi/reactor/blueprints/assemblyBlueprint.py b/armi/reactor/blueprints/assemblyBlueprint.py index 5d5b75246..0ef15b43a 100644 --- a/armi/reactor/blueprints/assemblyBlueprint.py +++ b/armi/reactor/blueprints/assemblyBlueprint.py @@ -75,6 +75,10 @@ class MaterialModifications(yamlize.Map): If the user wishes to specify material modifications specific to a component within the block, they should use the `by component` attribute, specifying the keys/values underneath the name of a specific component in the block. + + .. impl:: User-impact on material definitions. + :id: I_ARMI_MAT_USER_INPUT0 + :implements: R_ARMI_MAT_USER_INPUT """ key_type = yamlize.Typed(str) diff --git a/armi/reactor/blueprints/componentBlueprint.py b/armi/reactor/blueprints/componentBlueprint.py index f37e2f8a3..4994eca18 100644 --- a/armi/reactor/blueprints/componentBlueprint.py +++ b/armi/reactor/blueprints/componentBlueprint.py @@ -162,7 +162,12 @@ def shape(self, shape): area = yamlize.Attribute(type=float, default=None) def construct(self, blueprint, matMods): - """Construct a component or group.""" + """Construct a component or group. + + .. impl:: User-defined on material alterations are applied here. + :id: I_ARMI_MAT_USER_INPUT1 + :implements: R_ARMI_MAT_USER_INPUT + """ runLog.debug("Constructing component {}".format(self.name)) kwargs = self._conformKwargs(blueprint, matMods) shape = self.shape.lower().strip() diff --git a/armi/reactor/blueprints/isotopicOptions.py b/armi/reactor/blueprints/isotopicOptions.py index ead85253f..f39f5c9ea 100644 --- a/armi/reactor/blueprints/isotopicOptions.py +++ b/armi/reactor/blueprints/isotopicOptions.py @@ -150,6 +150,10 @@ class CustomIsotopic(yamlize.Map): """ User specified, custom isotopics input defined by a name (such as MOX), and key/pairs of nuclide names and numeric values consistent with the ``input format``. + + .. impl:: Certain material modifications will be applied using this code. + :id: I_ARMI_MAT_USER_INPUT2 + :implements: R_ARMI_MAT_USER_INPUT """ key_type = yamlize.Typed(str) diff --git a/armi/reactor/blueprints/tests/test_componentBlueprint.py b/armi/reactor/blueprints/tests/test_componentBlueprint.py index 729e4cbc4..b96e7c88e 100644 --- a/armi/reactor/blueprints/tests/test_componentBlueprint.py +++ b/armi/reactor/blueprints/tests/test_componentBlueprint.py @@ -68,7 +68,7 @@ def test_componentInitializationIncompleteBurnChain(self): def test_componentInitializationControlCustomIsotopics(self): nuclideFlags = ( inspect.cleandoc( - r""" + """ nuclide flags: U234: {burn: true, xs: true} U235: {burn: true, xs: true} @@ -99,7 +99,7 @@ def test_componentInitializationControlCustomIsotopics(self): def test_autoDepletable(self): nuclideFlags = ( inspect.cleandoc( - r""" + """ nuclide flags: U234: {burn: true, xs: true} U235: {burn: true, xs: true} diff --git a/armi/reactor/blueprints/tests/test_materialModifications.py b/armi/reactor/blueprints/tests/test_materialModifications.py index 9a5b63ab4..bd7b458cb 100644 --- a/armi/reactor/blueprints/tests/test_materialModifications.py +++ b/armi/reactor/blueprints/tests/test_materialModifications.py @@ -73,6 +73,13 @@ def test_noMaterialModifications(self): assert_allclose(uzr.massFrac[nucName], massFrac) def test_u235_wt_frac_modification(self): + """Test constructing a component where the blueprints specify a material + modification for one nuclide. + + .. test:: A material modification can be applied to all the components in an assembly. + :id: T_ARMI_MAT_USER_INPUT0 + :tests: R_ARMI_MAT_USER_INPUT + """ a = self.loadUZrAssembly( """ material modifications: @@ -90,6 +97,13 @@ def test_u235_wt_frac_modification(self): assert_allclose(0.20, u235 / u) def test_u235_wt_frac_byComponent_modification1(self): + """Test constructing a component where the blueprints specify a material + modification for one nuclide, for just one component. + + .. test:: A material modification can be applied to one component in an assembly. + :id: T_ARMI_MAT_USER_INPUT1 + :tests: R_ARMI_MAT_USER_INPUT + """ a = self.loadUZrAssembly( """ material modifications: @@ -110,6 +124,13 @@ def test_u235_wt_frac_byComponent_modification1(self): assert_allclose(0.30, u235 / u) def test_u235_wt_frac_byComponent_modification2(self): + """Test constructing a component where the blueprints specify a material + modification for one nuclide, for multiple components. + + .. test:: A material modification can be applied to multiple components in an assembly. + :id: T_ARMI_MAT_USER_INPUT2 + :tests: R_ARMI_MAT_USER_INPUT + """ a = self.loadUZrAssembly( """ material modifications: diff --git a/armi/reactor/grids/locations.py b/armi/reactor/grids/locations.py index 5622f4d7b..126e2e42e 100644 --- a/armi/reactor/grids/locations.py +++ b/armi/reactor/grids/locations.py @@ -359,6 +359,10 @@ class MultiIndexLocation(IndexLocation): This class contains an implementation that allows a multi-index location to be used in the ARMI data model similar to a individual IndexLocation. + + .. impl:: Store components with multiplicity greater than 1 + :id: I_ARMI_GRID_MULT + :implements: R_ARMI_GRID_MULT """ # MIL's cannot be hashed, so we need to scrape off the implementation from @@ -428,6 +432,10 @@ def indices(self) -> List[numpy.ndarray]: """ Return indices for all locations. + .. impl:: Return the location of all instances of grid components with multiplicity greater than 1. + :id: I_ARMI_GRID_ELEM_LOC + :implements: R_ARMI_GRID_ELEM_LOC + Notes ----- Notice that this returns a list of all of the indices, unlike the ``indices()`` diff --git a/armi/reactor/grids/tests/test_grids.py b/armi/reactor/grids/tests/test_grids.py index 728f87bb2..4cdb73ab7 100644 --- a/armi/reactor/grids/tests/test_grids.py +++ b/armi/reactor/grids/tests/test_grids.py @@ -201,6 +201,10 @@ def test_getitem(self): """ Test that locations are created on demand, and the multi-index locations are returned when necessary. + + .. test:: Return the locations of grid items with multiplicity greater than one. + :id: T_ARMI_GRID_ELEM_LOC + :tests: R_ARMI_GRID_ELEM_LOC """ grid = grids.HexGrid.fromPitch(1.0, numRings=0) self.assertNotIn((0, 0, 0), grid._locations) @@ -211,6 +215,10 @@ def test_getitem(self): self.assertIsInstance(multiLoc, grids.MultiIndexLocation) self.assertIn((1, 0, 0), grid._locations) + i = multiLoc.indices + i = [ii.tolist() for ii in i] + self.assertEqual(i, [[0, 0, 0], [1, 0, 0], [0, 1, 0]]) + def test_ringPosFromIndicesIncorrect(self): """Test the getRingPos fails if there is no armiObect or parent.""" grid = MockStructuredGrid( diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py index b5f8c4957..d420f245f 100644 --- a/armi/reactor/tests/test_blocks.py +++ b/armi/reactor/tests/test_blocks.py @@ -2060,6 +2060,13 @@ def test_getPinCenterFlatToFlat(self): self.assertAlmostEqual(pinCenterFlatToFlat, f2f) def test_gridCreation(self): + """Create a grid for a block, and show that it can handle components with + multiplicity > 1. + + .. test:: Grids can handle components with multiplicity > 1. + :id: T_ARMI_GRID_MULT + :tests: R_ARMI_GRID_MULT + """ b = self.HexBlock # The block should have a spatial grid at construction, # since it has mults = 1 or 169 from setup From d3b5bd5d5a044172c797f41b0bb20dfefd1a9ca7 Mon Sep 17 00:00:00 2001 From: bdlafleur Date: Thu, 30 Nov 2023 17:43:38 -0600 Subject: [PATCH 071/176] Adding test crumbs for requirements (#1511) --- .../neutronics/tests/test_energyGroups.py | 13 +++++++++++ armi/reactor/tests/test_components.py | 22 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/armi/physics/neutronics/tests/test_energyGroups.py b/armi/physics/neutronics/tests/test_energyGroups.py index 6cb2b9673..cb68011ae 100644 --- a/armi/physics/neutronics/tests/test_energyGroups.py +++ b/armi/physics/neutronics/tests/test_energyGroups.py @@ -53,3 +53,16 @@ def test_consistenciesBetweenGroupStructureAndGroupStructureType(self): energyGroups.getGroupStructure(groupStructureType) ), ) + + def test_getFastFluxGroupCutoff(self): + """Test ability to get the ARMI energy group index contained in energy threshold. + + .. test:: Return the energy group index which contains a given energy threshold. + :id: T_ARMI_EG_FE + :tests: R_ARMI_EG_FE + """ + group, frac = energyGroups.getFastFluxGroupCutoff( + [100002, 100001, 100000, 99999, 0] + ) + + self.assertListEqual([group, frac], [2, 0]) diff --git a/armi/reactor/tests/test_components.py b/armi/reactor/tests/test_components.py index f67e5f019..d89bd4da5 100644 --- a/armi/reactor/tests/test_components.py +++ b/armi/reactor/tests/test_components.py @@ -320,6 +320,28 @@ def test_getBoundingCircleOuterDiameter(self): * self.component.getThermalExpansionFactor(self.component.temperatureInC), ) + def test_component_less_than(self): + """Ensure that comparisons between components properly reference bounding circle outer diameter. + + .. test:: Order components by their outermost diameter + :id: T_ARMI_COMP_ORDER + :tests: R_ARMI_COMP_ORDER + """ + componentCls = UnshapedComponent + componentMaterial = "HT9" + + smallDims = {"Tinput": 25.0, "Thot": 430.0, "area": 0.5 * math.pi} + sameDims = {"Tinput": 25.0, "Thot": 430.0, "area": 1.0 * math.pi} + bigDims = {"Tinput": 25.0, "Thot": 430.0, "area": 2.0 * math.pi} + + smallComponent = componentCls("TestComponent", componentMaterial, **smallDims) + sameComponent = componentCls("TestComponent", componentMaterial, **sameDims) + bigComponent = componentCls("TestComponent", componentMaterial, **bigDims) + + self.assertTrue(smallComponent < self.component) + self.assertFalse(bigComponent < self.component) + self.assertFalse(sameComponent < self.component) + def test_fromComponent(self): circle = components.Circle("testCircle", "HT9", 25, 500, 1.0) unshaped = components.UnshapedComponent.fromComponent(circle) From fffb48e61d6e8c5a50df7c78f1b5bcece613b1be Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Fri, 1 Dec 2023 09:58:31 -0800 Subject: [PATCH 072/176] Adding test crumbs to nuclearDataIO (#1512) --- armi/nuclearDataIO/cccc/tests/test_isotxs.py | 35 +++++++++++++++++++ armi/nuclearDataIO/cccc/tests/test_pmatrx.py | 7 +++- .../nuclearDataIO/tests/test_xsCollections.py | 6 ++++ armi/nuclearDataIO/xsCollections.py | 1 - .../tests/test_axialExpansionChanger.py | 4 +++ 5 files changed, 51 insertions(+), 2 deletions(-) diff --git a/armi/nuclearDataIO/cccc/tests/test_isotxs.py b/armi/nuclearDataIO/cccc/tests/test_isotxs.py index f4bb35db5..484f7aa4a 100644 --- a/armi/nuclearDataIO/cccc/tests/test_isotxs.py +++ b/armi/nuclearDataIO/cccc/tests/test_isotxs.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Tests the workings of the library wrappers.""" +import filecmp import unittest from armi import nuclearDataIO @@ -19,6 +20,7 @@ from armi.nuclearDataIO import xsLibraries from armi.nuclearDataIO.cccc import isotxs from armi.tests import ISOAA_PATH +from armi.utils.directoryChangers import TemporaryDirectoryChanger class TestIsotxs(unittest.TestCase): @@ -30,6 +32,33 @@ def setUpClass(cls): # be a small library with LFPs, Actinides, structure, and coolant cls.lib = isotxs.readBinary(ISOAA_PATH) + def test_writeBinary(self): + """Test reading in an ISOTXS file, and then writing it back out again. + + Now, the library here can't guarantee the output will be the same as the + input. But we can guarantee the written file is still valid, by reading + it again. + + .. test:: Write ISOTSX binary files. + :id: T_ARMI_NUCDATA_ISOTXS0 + :tests: R_ARMI_NUCDATA_ISOTXS + """ + with TemporaryDirectoryChanger(): + origLib = isotxs.readBinary(ISOAA_PATH) + + fname = self._testMethodName + "temp-aa.isotxs" + isotxs.writeBinary(origLib, fname) + lib = isotxs.readBinary(fname) + + # validate the written file is still valid + nucs = lib.nuclides + self.assertTrue(nucs) + self.assertIn("AA", lib.xsIDs) + nuc = lib["U235AA"] + self.assertIsNotNone(nuc) + with self.assertRaises(KeyError): + lib.getNuclide("nonexistent", "zz") + def test_isotxsGeneralData(self): nucs = self.lib.nuclides self.assertTrue(nucs) @@ -164,6 +193,12 @@ def test_getGAMISOFileName(self): class Isotxs_merge_Tests(unittest.TestCase): def test_mergeMccV2FilesRemovesTheFileWideChi(self): + """Test merging ISOTXS files. + + .. test:: Read ISOTXS files. + :id: T_ARMI_NUCDATA_ISOTXS1 + :tests: R_ARMI_NUCDATA_ISOTXS + """ isoaa = isotxs.readBinary(ISOAA_PATH) self.assertAlmostEqual(1.0, sum(isoaa.isotxsMetadata["chi"]), 5) self.assertAlmostEqual(1, isoaa.isotxsMetadata["fileWideChiFlag"]) diff --git a/armi/nuclearDataIO/cccc/tests/test_pmatrx.py b/armi/nuclearDataIO/cccc/tests/test_pmatrx.py index 4d8e90e3f..75b33f70a 100644 --- a/armi/nuclearDataIO/cccc/tests/test_pmatrx.py +++ b/armi/nuclearDataIO/cccc/tests/test_pmatrx.py @@ -201,7 +201,12 @@ class TestProductionMatrix_FromWritten(TestPmatrx): """ def test_writtenIsIdenticalToOriginal(self): - """Make sure our writer produces something identical to the original.""" + """Make sure our writer produces something identical to the original. + + .. test:: Test reading and writing PMATRIX files. + :id: T_ARMI_NUCDATA_PMATRX + :tests: R_ARMI_NUCDATA_PMATRX + """ origLib = pmatrx.readBinary(test_xsLibraries.PMATRX_AA) fname = self._testMethodName + "temp-aa.pmatrx" diff --git a/armi/nuclearDataIO/tests/test_xsCollections.py b/armi/nuclearDataIO/tests/test_xsCollections.py index f8b1990f0..6a85e1734 100644 --- a/armi/nuclearDataIO/tests/test_xsCollections.py +++ b/armi/nuclearDataIO/tests/test_xsCollections.py @@ -78,6 +78,12 @@ def test_plotNucXs(self): self.assertTrue(os.path.exists(fName)) def test_createMacrosFromMicros(self): + """Test calculating macroscopic cross sections from microscopic cross sections. + + .. test:: Compute macroscopic cross sections from microscopic cross sections and number densities. + :id: T_ARMI_NUCDATA_MACRO + :tests: R_ARMI_NUCDATA_MACRO + """ self.assertEqual(self.mc.minimumNuclideDensity, 1e-13) self.mc.createMacrosFromMicros(self.microLib, self.block) totalMacroFissionXs = 0.0 diff --git a/armi/nuclearDataIO/xsCollections.py b/armi/nuclearDataIO/xsCollections.py index e453a016b..a6672f872 100644 --- a/armi/nuclearDataIO/xsCollections.py +++ b/armi/nuclearDataIO/xsCollections.py @@ -415,7 +415,6 @@ def createMacrosFromMicros( ------- macros : xsCollection.XSCollection A new XSCollection full of macroscopic cross sections - """ runLog.debug("Building macroscopic cross sections for {0}".format(block)) if nucNames is None: diff --git a/armi/reactor/converters/tests/test_axialExpansionChanger.py b/armi/reactor/converters/tests/test_axialExpansionChanger.py index 0e296c601..8f81b2eea 100644 --- a/armi/reactor/converters/tests/test_axialExpansionChanger.py +++ b/armi/reactor/converters/tests/test_axialExpansionChanger.py @@ -857,6 +857,10 @@ def test_coldAssemblyExpansion(self): :id: T_ARMI_ASSEM_HEIGHT_PRES :tests: R_ARMI_ASSEM_HEIGHT_PRES + .. test:: Axial expansion can be prescribed in blueprints for core constuction. + :id: T_ARMI_INP_COLD_HEIGHT + :tests: R_ARMI_INP_COLD_HEIGHT + Notes ----- Two assertions here: From 77623d58b69e3f4996258f9ab2673fc5c8b09e38 Mon Sep 17 00:00:00 2001 From: Chris Keckler Date: Fri, 1 Dec 2023 15:30:15 -0600 Subject: [PATCH 073/176] Remove unused parameters related to reactivity coefficients (#1501) * Remove block and assembly parameters which are unused * Remove unused core parameters * Update changelog * Put back rxFuelAxialExpansionPerPercent --- armi/reactor/assemblyParameters.py | 42 ----- armi/reactor/blockParameters.py | 209 ------------------------ armi/reactor/reactorParameters.py | 72 -------- armi/utils/reportPlotting.py | 2 - armi/utils/tests/test_reportPlotting.py | 4 - doc/release/0.2.rst | 1 + 6 files changed, 1 insertion(+), 329 deletions(-) diff --git a/armi/reactor/assemblyParameters.py b/armi/reactor/assemblyParameters.py index 9a4bfa8ff..5e5ff700b 100644 --- a/armi/reactor/assemblyParameters.py +++ b/armi/reactor/assemblyParameters.py @@ -319,40 +319,12 @@ def _enforceNotesRestrictions(self, value): pb.defParam("assemNum", units=units.UNITLESS, description="Assembly number") - pb.defParam( - "axExpWorthPT", - units=f"{units.PCM}/{units.PERCENT}/{units.CM}^3", - description="Axial swelling reactivity", - location=ParamLocation.AVERAGE, - ) - - pb.defParam( - "coolFlowingWorthPT", - units=f"{units.PCM}/{units.PERCENT}/{units.CM}^3", - description="Flowing coolant reactivity", - location=ParamLocation.AVERAGE, - ) - - pb.defParam( - "coolWorthPT", - units=f"{units.PCM}/{units.PERCENT}/{units.CM}^3", - description="Coolant reactivity", - location=ParamLocation.AVERAGE, - ) - pb.defParam( "dischargeTime", units=units.YEARS, description="Time the Assembly was removed from the Reactor.", ) - pb.defParam( - "fuelWorthPT", - units=f"{units.PCM}/{units.PERCENT}/{units.CM}^3", - description="Fuel reactivity", - location=ParamLocation.AVERAGE, - ) - pb.defParam( "hotChannelFactors", units=units.UNITLESS, @@ -362,20 +334,6 @@ def _enforceNotesRestrictions(self, value): categories=[parameters.Category.assignInBlueprints], ) - pb.defParam( - "radExpWorthPT", - units=f"{units.PCM}/{units.PERCENT}/{units.CM}^3", - description="Radial swelling reactivity", - location=ParamLocation.AVERAGE, - ) - - pb.defParam( - "structWorthPT", - units=f"{units.PCM}/{units.PERCENT}/{units.CM}^3", - description="Structure reactivity", - location=ParamLocation.AVERAGE, - ) - with pDefs.createBuilder(categories=["radialGeometry"]) as pb: pb.defParam( diff --git a/armi/reactor/blockParameters.py b/armi/reactor/blockParameters.py index 54ba57ba5..0bd3952b5 100644 --- a/armi/reactor/blockParameters.py +++ b/armi/reactor/blockParameters.py @@ -429,187 +429,6 @@ def xsTypeNum(self, value): saveToDB=True, ) - with pDefs.createBuilder( - default=0.0, - location=ParamLocation.AVERAGE, - categories=["reactivity coefficients"], - ) as pb: - - pb.defParam( - "VoideddopplerWorth", - units=f"{units.REACTIVITY}*{units.DEGK}^(n-1)", - description="Distributed Voided Doppler constant.", - ) - - pb.defParam( - "dopplerWorth", - units=f"{units.REACTIVITY}*{units.DEGK}^(n-1)", - description="Distributed Doppler constant.", - ) - - pb.defParam( - "fuelWorth", - units=f"{units.REACTIVITY}/{units.KG}", - description="Reactivity worth of fuel material per unit mass", - ) - - pb.defParam( - "fuelWorthPT", - units=f"{units.PCM}/{units.PERCENT}/{units.CM}^3", - description="Fuel reactivity", - ) - - pb.defParam( - "structWorthPT", - units=f"{units.PCM}/{units.PERCENT}/{units.CM}^3", - description="Structure reactivity", - ) - - pb.defParam( - "radExpWorthPT", - units=f"{units.PCM}/{units.PERCENT}/{units.CM}^3", - description="Radial swelling reactivity", - ) - - pb.defParam( - "coolWorthPT", - units=f"{units.PCM}/{units.PERCENT}/{units.CM}^3", - description="Coolant reactivity", - ) - - pb.defParam( - "coolFlowingWorthPT", - units=f"{units.PCM}/{units.PERCENT}/{units.CM}^3", - description="Flowing coolant reactivity", - ) - - pb.defParam( - "axExpWorthPT", - units=f"{units.PCM}/{units.PERCENT}/{units.CM}^3", - description="Axial swelling reactivity", - ) - - pb.defParam( - "coolantWorth", - units=f"{units.REACTIVITY}/{units.KG}", - description="Reactivity worth of coolant material per unit mass", - ) - - pb.defParam( - "cladWorth", - units=f"{units.REACTIVITY}/{units.KG}", - description="Reactivity worth of clad material per unit mass", - ) - - pb.defParam( - "rxAxialCentsPerK", - units=f"{units.CENTS}/{units.DEGK}", - description="Axial temperature reactivity coefficient", - ) - - pb.defParam( - "rxAxialCentsPerPow", - units=f"{units.CENTS}/{units.DEGK}", - description="Axial power reactivity coefficient", - ) - - pb.defParam( - "rxCoolantCentsPerK", - units=f"{units.CENTS}/{units.DEGK}", - description="Coolant temperature reactivity coefficient", - ) - - pb.defParam( - "rxCoolantCentsPerPow", - units=f"{units.CENTS}/{units.DEGK}", - description="Coolant power reactivity coefficient", - ) - - pb.defParam( - "rxDopplerCentsPerK", - units=f"{units.CENTS}/{units.DEGK}", - description="Doppler temperature reactivity coefficient", - ) - - pb.defParam( - "rxDopplerCentsPerPow", - units=f"{units.CENTS}/{units.DEGK}", - description="Doppler power reactivity coefficient", - ) - - pb.defParam( - "rxFuelCentsPerK", - units=f"{units.CENTS}/{units.DEGK}", - description="Fuel temperature reactivity coefficient", - ) - - pb.defParam( - "rxFuelCentsPerPow", - units=f"{units.CENTS}/{units.DEGK}", - description="Fuel power reactivity coefficient", - ) - - pb.defParam( - "rxNetCentsPerK", - units=f"{units.CENTS}/{units.DEGK}", - description="Net temperature reactivity coefficient", - ) - - pb.defParam( - "rxNetCentsPerPow", - units=f"{units.CENTS}/{units.DEGK}", - description="Net power reactivity coefficient", - location=ParamLocation.AVERAGE, - ) - - pb.defParam( - "rxNetPosNeg", - units=f"{units.CENTS}/{units.DEGK}", - description="Net temperature reactivity coefficient: positive or negative", - ) - - pb.defParam( - "rxNetPosNegPow", - units=f"{units.CENTS}/{units.DEGK}", - description="Net power reactivity coefficient: positive or negative", - ) - - pb.defParam( - "rxRadialCentsPerK", - units=f"{units.CENTS}/{units.DEGK}", - description="Radial temperature reactivity coefficient", - ) - - pb.defParam( - "rxRadialCentsPerPow", - units=f"{units.CENTS}/{units.DEGK}", - description="Radial power reactivity coefficient", - ) - - pb.defParam( - "rxStructCentsPerK", - units=f"{units.CENTS}/{units.DEGK}", - description="Structure temperature reactivity coefficient", - ) - - pb.defParam( - "rxStructCentsPerPow", - units=f"{units.CENTS}/{units.DEGK}", - description="Structure power reactivity coefficient", - ) - - pb.defParam( - "rxVoidedDopplerCentsPerK", - units=f"{units.CENTS}/{units.DEGK}", - description="Voided Doppler temperature reactivity coefficient", - ) - - pb.defParam( - "rxVoidedDopplerCentsPerPow", - units=f"{units.CENTS}/{units.DEGK}", - description="Voided Doppler power reactivity coefficient", - ) - with pDefs.createBuilder( default=0.0, location=ParamLocation.AVERAGE, @@ -883,13 +702,6 @@ def xsTypeNum(self, value): location=ParamLocation.AVERAGE, ) - pb.defParam( - "coolRemFrac", - units=units.UNITLESS, - description="Fractional sodium density change for each block", - location=ParamLocation.AVERAGE, - ) - pb.defParam( "crWastage", units=units.MICRONS, @@ -904,27 +716,6 @@ def xsTypeNum(self, value): location=ParamLocation.AVERAGE, ) - pb.defParam( - "deltaTclad", - units=f"{units.DEGK}/{units.PERCENT}", - description=r"Change in fuel temperature due to 1% rise in power.", - location=ParamLocation.AVERAGE, - ) - - pb.defParam( - "deltaTduct", - units=f"{units.DEGK}/{units.PERCENT}", - description=r"Change in fuel temperature due to 1% rise in power.", - location=ParamLocation.AVERAGE, - ) - - pb.defParam( - "deltaTfuel", - units=f"{units.DEGK}/{units.PERCENT}", - description=r"Change in fuel temperature due to 1% rise in power.", - location=ParamLocation.AVERAGE, - ) - pb.defParam( "heightBOL", units=units.CM, diff --git a/armi/reactor/reactorParameters.py b/armi/reactor/reactorParameters.py index e738535e3..4822d7b45 100644 --- a/armi/reactor/reactorParameters.py +++ b/armi/reactor/reactorParameters.py @@ -521,78 +521,6 @@ def defineCoreParameters(): ), ) - with pDefs.createBuilder( - default=0.0, - location=ParamLocation.AVERAGE, - categories=["reactivity coefficients"], - ) as pb: - - pb.defParam( - "axial", - units=f"{units.CENTS}/{units.DEGK}", - description="Axial expansion coefficient", - ) - - pb.defParam( - "doppler", - units=f"{units.CENTS}/{units.DEGK}", - description="Doppler coefficient", - ) - - pb.defParam( - "dopplerConst", - units=f"{units.CENTS}*{units.DEGK}^(n-1)", - description="Doppler constant", - ) - - pb.defParam( - "fuelDensity", - units=f"{units.CENTS}/{units.DEGK}", - description="Fuel temperature coefficient", - ) - - pb.defParam( - "coolantDensity", - units=f"{units.CENTS}/{units.DEGK}", - description="Coolant temperature coefficient", - ) - - pb.defParam( - "totalCoolantDensity", - units=f"{units.CENTS}/{units.DEGK}", - description="Coolant temperature coefficient weighted to include bond and interstitial effects", - ) - - pb.defParam( - "Voideddoppler", - units=f"{units.CENTS}/{units.DEGK}", - description="Voided Doppler coefficient", - ) - - pb.defParam( - "VoideddopplerConst", - units=f"{units.CENTS}*{units.DEGK}^(n-1)", - description="Voided Doppler constant", - ) - - pb.defParam( - "voidWorth", units=f"{units.DOLLARS}", description="Coolant void worth" - ) - - pb.defParam("voidedKeff", units=units.UNITLESS, description="Voided keff") - - pb.defParam( - "radialHT9", - units=f"{units.CENTS}/{units.DEGK}", - description="Radial expansion coefficient when driven by thermal expansion of HT9.", - ) - - pb.defParam( - "radialSS316", - units=f"{units.CENTS}/{units.DEGK}", - description="Radial expansion coefficient when driven by thermal expansion of SS316.", - ) - with pDefs.createBuilder( default=0.0, location=ParamLocation.AVERAGE, diff --git a/armi/utils/reportPlotting.py b/armi/utils/reportPlotting.py index 277f6cd47..e0b5ced3b 100644 --- a/armi/utils/reportPlotting.py +++ b/armi/utils/reportPlotting.py @@ -451,8 +451,6 @@ def _getNeutronicVals(r): ("Rx. Swing", r.core.p.rxSwing), ("Fast Flux Fr.", r.core.p.fastFluxFrAvg), ("Leakage", r.core.p.leakageFracTotal), - ("Void worth", r.core.p.voidWorth), - ("Doppler", r.core.p.doppler), ("Beta", r.core.p.beta), ("Peak flux", r.core.p.maxFlux), ] diff --git a/armi/utils/tests/test_reportPlotting.py b/armi/utils/tests/test_reportPlotting.py index 61dde08c7..5057857ff 100644 --- a/armi/utils/tests/test_reportPlotting.py +++ b/armi/utils/tests/test_reportPlotting.py @@ -44,11 +44,7 @@ def tearDown(self): def test_radar(self): """Test execution of radar plot. Note this has no asserts and is therefore a smoke test.""" - self.r.core.p.doppler = 0.5 - self.r.core.p.voidWorth = 0.5 r2 = copy.deepcopy(self.r) - r2.core.p.voidWorth = 1.0 - r2.core.p.doppler = 1.0 plotCoreOverviewRadar([self.r, r2], ["Label1", "Label2"]) def test_createPlotMetaData(self): diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst index 631fd05a8..e46cc420e 100644 --- a/doc/release/0.2.rst +++ b/doc/release/0.2.rst @@ -14,6 +14,7 @@ What's new in ARMI #. Downgrading Draft PRs as policy. (`PR#1444 `_) #. Attempt to set representative block number densities by component if possible. (`PR#1412 `_) #. Use functools to preserve function attributes when wrapping with codeTiming.timed (`PR#1466 `_) +#. Remove a number of deprecated block, assembly, and core parameters related to a defunct internal plugin #. TBD Bug fixes From 457ca15398f65684507c3dd73df25128bd3e1e7c Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Mon, 4 Dec 2023 08:38:51 -0800 Subject: [PATCH 074/176] Adding 3 test crumbs for requirements (#1510) --- armi/materials/tests/test_materials.py | 52 +++++++++++++- armi/nuclearDataIO/xsNuclides.py | 4 +- .../neutronics/macroXSGenerationInterface.py | 2 - .../tests/test_macroXSGenerationInterface.py | 39 +++++++++-- armi/physics/tests/test_executers.py | 68 ++++++++++++++++--- armi/tests/test_apps.py | 1 - armi/tests/test_plugins.py | 7 +- armi/utils/tests/test_densityTools.py | 15 +++- 8 files changed, 163 insertions(+), 25 deletions(-) diff --git a/armi/materials/tests/test_materials.py b/armi/materials/tests/test_materials.py index e04ac1cf8..11ec8f291 100644 --- a/armi/materials/tests/test_materials.py +++ b/armi/materials/tests/test_materials.py @@ -19,7 +19,8 @@ from numpy import testing -from armi import materials, settings +from armi import context, materials, settings +from armi.materials import _MATERIAL_NAMESPACE_ORDER, setMaterialNamespaceOrder from armi.nucDirectory import nuclideBases from armi.reactor import blueprints from armi.tests import mockRunLogs @@ -108,6 +109,10 @@ class MaterialFindingTests(unittest.TestCase): def test_findMaterial(self): """Test resolveMaterialClassByName() function. + .. 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 @@ -139,6 +144,51 @@ def test_findMaterial(self): "Unobtanium", namespaceOrder=["armi.materials"] ) + def __validateMaterialNamespace(self): + """Helper method to validate the material namespace a little.""" + self.assertTrue(isinstance(_MATERIAL_NAMESPACE_ORDER, list)) + self.assertGreater(len(_MATERIAL_NAMESPACE_ORDER), 0) + for nameSpace in _MATERIAL_NAMESPACE_ORDER: + self.assertTrue(isinstance(nameSpace, str)) + + @unittest.skipUnless(context.MPI_RANK == 0, "test only on root node") + def test_namespacing(self): + """Test loading materials with different material namespaces, to cover how they work. + + .. test:: Material can be found in defined packages. + :id: T_ARMI_MAT_NAMESPACE1 + :tests: R_ARMI_MAT_NAMESPACE + + .. test:: Material namespaces register materials with an order of priority. + :id: T_ARMI_MAT_ORDER + :tests: R_ARMI_MAT_ORDER + """ + # let's do a quick test of getting a material from the default namespace + setMaterialNamespaceOrder(["armi.materials"]) + uo2 = materials.resolveMaterialClassByName( + "UO2", namespaceOrder=["armi.materials"] + ) + self.assertGreater(uo2().density(500), 0) + + # validate the default namespace in ARMI + self.__validateMaterialNamespace() + + # show you can add a material namespace + newMats = "armi.utils.tests.test_densityTools" + setMaterialNamespaceOrder(["armi.materials", newMats]) + self.__validateMaterialNamespace() + + # show that adding a name material namespace provides access to new materials + testMatIgnoreFake = materials.resolveMaterialClassByName( + "TestMaterialIgnoreFake", namespaceOrder=["armi.materials", newMats] + ) + for t in range(200, 600): + self.assertEqual(testMatIgnoreFake().density(t), 0) + self.assertEqual(testMatIgnoreFake().pseudoDensity(t), 0) + + # for safety, reset the material namespace list and order + setMaterialNamespaceOrder(["armi.materials"]) + class Californium_TestCase(_Material_Test, unittest.TestCase): diff --git a/armi/nuclearDataIO/xsNuclides.py b/armi/nuclearDataIO/xsNuclides.py index 78f2da1d1..9ed04267a 100644 --- a/armi/nuclearDataIO/xsNuclides.py +++ b/armi/nuclearDataIO/xsNuclides.py @@ -95,7 +95,7 @@ def updateBaseNuclide(self): self._base = nuclideBase def getMicroXS(self, interaction, group): - r"""Returns the microscopic xs as the ISOTXS value if it exists or a 0 since it doesn't.""" + """Returns the microscopic xs as the ISOTXS value if it exists or a 0 since it doesn't.""" if interaction in self.micros.__dict__: try: return self.micros[interaction][group] @@ -109,7 +109,7 @@ def getMicroXS(self, interaction, group): return 0 def getXS(self, interaction): - r"""Get the cross section of a particular interaction. + """Get the cross section of a particular interaction. See Also -------- diff --git a/armi/physics/neutronics/macroXSGenerationInterface.py b/armi/physics/neutronics/macroXSGenerationInterface.py index 1a69f55c0..79c5c9cb1 100644 --- a/armi/physics/neutronics/macroXSGenerationInterface.py +++ b/armi/physics/neutronics/macroXSGenerationInterface.py @@ -67,7 +67,6 @@ def __reduce__(self): ) def invokeHook(self): - # logic here gets messy due to all the default arguments in the calling # method. There exists a large number of permutations to be handled. @@ -169,7 +168,6 @@ def buildMacros( libType : str, optional The block attribute containing the desired microscopic XS for this block: either "micros" for neutron XS or "gammaXS" for gamma XS. - """ cycle = self.r.p.cycle self.macrosLastBuiltAt = ( diff --git a/armi/physics/neutronics/tests/test_macroXSGenerationInterface.py b/armi/physics/neutronics/tests/test_macroXSGenerationInterface.py index fd12b9162..8ab46e20d 100644 --- a/armi/physics/neutronics/tests/test_macroXSGenerationInterface.py +++ b/armi/physics/neutronics/tests/test_macroXSGenerationInterface.py @@ -13,20 +13,51 @@ # limitations under the License. """MacroXSGenerationInterface tests.""" import unittest +from collections import defaultdict +from armi.nuclearDataIO import isotxs +from armi.nuclearDataIO.xsCollections import XSCollection from armi.physics.neutronics.macroXSGenerationInterface import ( MacroXSGenerationInterface, ) -from armi.reactor.tests.test_reactors import loadTestReactor +from armi.reactor.tests.test_reactors import loadTestReactor, reduceTestReactorRings from armi.settings import Settings +from armi.tests import ISOAA_PATH class TestMacroXSGenerationInterface(unittest.TestCase): - def test_macroXSGenerationInterface(self): + def test_macroXSGenerationInterfaceBasics(self): + """Test the macroscopic XS generating interfaces. + + .. test::Build macroscopic cross sections for all blocks in the reactor. + :id: T_ARMI_MACRO_XS + :tests: R_ARMI_MACRO_XS + """ cs = Settings() _o, r = loadTestReactor() - i = MacroXSGenerationInterface(r, cs) + reduceTestReactorRings(r, cs, 2) - self.assertIsNone(i.macrosLastBuiltAt) + # Before: verify there are no macro XS on each block + for b in r.core.getBlocks(): + self.assertIsNone(b.macros) + + # create the macro XS interface + i = MacroXSGenerationInterface(r, cs) self.assertEqual(i.minimumNuclideDensity, 1e-15) self.assertEqual(i.name, "macroXsGen") + + # Mock up a nuclide library + mockLib = isotxs.readBinary(ISOAA_PATH) + mockLib.__dict__["_nuclides"] = defaultdict( + lambda: mockLib.__dict__["_nuclides"]["CAA"], mockLib.__dict__["_nuclides"] + ) + + # This is the meat of it: build the macro XS + self.assertIsNone(i.macrosLastBuiltAt) + i.buildMacros(mockLib, buildScatterMatrix=False) + self.assertEqual(i.macrosLastBuiltAt, 0) + + # After: verify there are macro XS on each block + for b in r.core.getBlocks(): + self.assertIsNotNone(b.macros) + self.assertTrue(isinstance(b.macros, XSCollection)) diff --git a/armi/physics/tests/test_executers.py b/armi/physics/tests/test_executers.py index ae5512bc0..a918a6152 100644 --- a/armi/physics/tests/test_executers.py +++ b/armi/physics/tests/test_executers.py @@ -14,36 +14,33 @@ """This module provides tests for the generic Executers.""" import os +import subprocess import unittest +from armi.physics import executers from armi.reactor import geometry from armi.utils import directoryChangers -from armi.physics import executers -class MockReactorParams: +class MockParams: def __init__(self): self.cycle = 1 self.timeNode = 2 -class MockCoreParams: - pass - - class MockCore: def __init__(self): # just pick a random geomType self.geomType = geometry.GeomType.CARTESIAN self.symmetry = "full" - self.p = MockCoreParams() + self.p = MockParams() class MockReactor: def __init__(self): self.core = MockCore() self.o = None - self.p = MockReactorParams() + self.p = MockParams() class TestExecutionOptions(unittest.TestCase): @@ -106,3 +103,58 @@ def test_updateRunDir(self): self.executer.dcType = directoryChangers.ForcedCreationDirectoryChanger self.executer._updateRunDir("notThisString") self.assertEqual(self.executer.options.runDir, "runDir") + + def test_runExternalExecutable(self): + """Run an external executable with an Executer. + + .. test:: Run an external executable with an Executer. + :id: T_ARMI_EX + :tests: R_ARMI_EX + """ + filePath = "test_runExternalExecutable.py" + outFile = "tmp.txt" + + class TestSillyExecuter(executers.Executer): + def run(self, args): + subprocess.run(["python", filePath, args]) + + with directoryChangers.TemporaryDirectoryChanger(): + # build a mock external program (a little Python script) + self.__makeALittleTestProgram(filePath, outFile) + + # make sure the output file doesn't exist yet + self.assertFalse(os.path.exists(outFile)) + + # set up an executer for our little test program + exe = TestSillyExecuter(None, None) + exe.run("") + + # make sure the output file exists now + self.assertTrue(os.path.exists(outFile)) + + # run the executer with options + testString = "some options" + exe.run(testString) + + # make sure the output file exists now + self.assertTrue(os.path.exists(outFile)) + newTxt = open(outFile, "r").read() + self.assertIn(testString, newTxt) + + @staticmethod + def __makeALittleTestProgram(filePath, outFile): + """Helper method to write a tiny Python script. + + We need "an external program" for testing. + """ + txt = f"""import sys + +def main(): + with open("{outFile}", "w") as f: + f.write(str(sys.argv)) + +if __name__ == "__main__": + main() +""" + with open(filePath, "w") as f: + f.write(txt) diff --git a/armi/tests/test_apps.py b/armi/tests/test_apps.py index f092324e5..a6b6ea7ee 100644 --- a/armi/tests/test_apps.py +++ b/armi/tests/test_apps.py @@ -16,7 +16,6 @@ import copy import unittest -from armi import cli from armi import configure from armi import context from armi import getApp diff --git a/armi/tests/test_plugins.py b/armi/tests/test_plugins.py index 6c6d53a5d..8adf00ed2 100644 --- a/armi/tests/test_plugins.py +++ b/armi/tests/test_plugins.py @@ -18,16 +18,11 @@ import yamlize -from armi import getPluginManagerOrFail from armi import interfaces from armi import plugins from armi import settings from armi.physics.neutronics import NeutronicsPlugin -from armi.reactor import parameters -from armi.reactor.blocks import Block, HexBlock -from armi.reactor.parameters import ParamLocation -from armi.reactor.parameters.parameterCollections import collectPluginParameters -from armi.utils import units +from armi.reactor.blocks import Block class TestPluginBasics(unittest.TestCase): diff --git a/armi/utils/tests/test_densityTools.py b/armi/utils/tests/test_densityTools.py index 75b2d2160..1f823a109 100644 --- a/armi/utils/tests/test_densityTools.py +++ b/armi/utils/tests/test_densityTools.py @@ -14,11 +14,25 @@ """Test densityTools.""" import unittest +from armi.materials.material import Material from armi.materials.uraniumOxide import UO2 from armi.nucDirectory import elements, nuclideBases from armi.utils import densityTools +class TestMaterialIgnoreFake(Material): + """A test material that needs to be stored in a different namespace. + + For tests in: armi.materials.tests.test_materials.py + """ + + def pseudoDensity(self, Tk=None, Tc=None): + return 0.0 + + def density(self, Tk=None, Tc=None): + return 0.0 + + class TestDensityTools(unittest.TestCase): def test_expandElementalMassFracsToNuclides(self): """ @@ -50,7 +64,6 @@ def test_expandElementalZeroMassFrac(self): self.assertAlmostEqual(sum(mass.values()), 1.0) def test_getChemicals(self): - u235 = nuclideBases.byName["U235"] u238 = nuclideBases.byName["U238"] o16 = nuclideBases.byName["O16"] From 4f55d94d56cb81f3bd44714082c3295eea16853f Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Mon, 4 Dec 2023 10:48:31 -0800 Subject: [PATCH 075/176] Reving unit tests for parallel process param updating (#1514) --- armi/reactor/composites.py | 5 +- armi/reactor/tests/test_parameters.py | 253 ++++++-------------------- 2 files changed, 60 insertions(+), 198 deletions(-) diff --git a/armi/reactor/composites.py b/armi/reactor/composites.py index de3831104..ead79f514 100644 --- a/armi/reactor/composites.py +++ b/armi/reactor/composites.py @@ -2922,9 +2922,8 @@ def syncMpiState(self): runLog.error("\n".join(msg)) raise - errors = collections.defaultdict( - list - ) # key is (comp, paramName) value is conflicting nodes + # key is (comp, paramName) value is conflicting nodes + errors = collections.defaultdict(list) syncCount = 0 compsPerNode = {len(nodeSyncData) for nodeSyncData in allSyncData} diff --git a/armi/reactor/tests/test_parameters.py b/armi/reactor/tests/test_parameters.py index 2302d4fb5..330171c10 100644 --- a/armi/reactor/tests/test_parameters.py +++ b/armi/reactor/tests/test_parameters.py @@ -12,14 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. """Tests of the Parameters class.""" +from distutils.spawn import find_executable import copy -import traceback import unittest from armi import context from armi.reactor import composites from armi.reactor import parameters +# determine if this is a parallel run, and MPI is installed +MPI_EXE = None +if find_executable("mpiexec.exe") is not None: + MPI_EXE = "mpiexec.exe" +elif find_executable("mpiexec") is not None: + MPI_EXE = "mpiexec" + class MockComposite: def __init__(self, name): @@ -213,6 +220,13 @@ class Mock(parameters.ParameterCollection): self.assertEqual("encapsulated", mock.noSetter) def test_setter(self): + """Test the Parameter setter() tooling, that signifies if a Parameter has been updated. + + .. test:: Tooling that allows a Parameter to signal it needs to be updated across processes. + :id: T_ARMI_PARAM_PARALLEL0 + :tests: R_ARMI_PARAM_PARALLEL + """ + class Mock(parameters.ParameterCollection): pDefs = parameters.ParameterDefinitionCollection() with pDefs.createBuilder() as pb: @@ -241,6 +255,7 @@ def nPlus1(self, value): print(mock.n) with self.assertRaises(parameters.ParameterError): print(mock.nPlus1) + mock.n = 15 self.assertEqual(15, mock.n) self.assertEqual(16, mock.nPlus1) @@ -251,6 +266,13 @@ def nPlus1(self, value): self.assertTrue(all(pd.assigned for pd in mock.paramDefs)) def test_setterGetterBasics(self): + """Test the Parameter setter/getter tooling, through the lifecycle of a Parameter being updated. + + .. test:: Tooling that allows a Parameter to signal it needs to be updated across processes. + :id: T_ARMI_PARAM_PARALLEL1 + :tests: R_ARMI_PARAM_PARALLEL + """ + class Mock(parameters.ParameterCollection): pDefs = parameters.ParameterDefinitionCollection() with pDefs.createBuilder() as pb: @@ -445,9 +467,7 @@ class MockPC(parameters.ParameterCollection): self.assertEqual(set(pc.paramDefs.inCategory("bacon")), set([p2, p3])) def test_parameterCollectionsHave__slots__(self): - """Make sure something is implemented to prevent accidental creation of - attributes. - """ + """Tests we prevent accidental creation of attributes.""" self.assertEqual( set(["_hist", "_backup", "assigned", "_p_serialNum", "serialNum"]), set(parameters.ParameterCollection._slots), @@ -457,12 +477,6 @@ class MockPC(parameters.ParameterCollection): pass pc = MockPC() - # No longer protecting against __dict__ access. If someone REALLY wants to - # staple something to a parameter collection with no guarantees of anything, - # that's on them - # with self.assertRaises(AttributeError): - # pc.__dict__["foo"] = 5 - with self.assertRaises(AssertionError): pc.whatever = 22 @@ -507,230 +521,79 @@ class MockSyncPC(parameters.ParameterCollection): def makeComp(name): + """Helper method for MPI sync tests: mock up a Composite with a minimal param collections.""" c = composites.Composite(name) c.p = MockSyncPC() return c -class SynchronizationTests: - """Some unit tests that must be run with mpirun instead of the standard unittest - system. - """ +class SynchronizationTests(unittest.TestCase): + """Some tests that must be run with mpirun instead of the standard unittest system.""" def setUp(self): self.r = makeComp("reactor") self.r.core = makeComp("core") self.r.add(self.r.core) - for ai in range(context.MPI_SIZE * 4): + for ai in range(context.MPI_SIZE * 3): a = makeComp("assembly{}".format(ai)) self.r.core.add(a) - for bi in range(10): + for bi in range(3): a.add(makeComp("block{}-{}".format(ai, bi))) - self.comps = [self.r.core] + self.r.core.getChildren(deep=True) - for pd in MockSyncPC().paramDefs: - pd.assigned = parameters.NEVER - - def tearDown(self): - del self.r - - def run(self, testNamePrefix="mpitest_"): - with open("mpitest{}.temp".format(context.MPI_RANK), "w") as self.l: - for methodName in sorted(dir(self)): - if methodName.startswith(testNamePrefix): - self.write("{}.{}".format(self.__class__.__name__, methodName)) - try: - self.setUp() - getattr(self, methodName)() - except Exception: - self.write("failed, big time") - traceback.print_exc(file=self.l) - self.write("*** printed exception") - try: - self.tearDown() - except: # noqa: bare-except - pass - - self.l.write("done.") - - def write(self, msg): - self.l.write("{}\n".format(msg)) - self.l.flush() - - def assertRaises(self, exceptionType): - class ExceptionCatcher: - def __enter__(self): - pass - - def __exit__(self, exc_type, exc_value, traceback): - if exc_type is exceptionType: - return True - raise AssertionError( - "Expected {}, but got {}".format(exceptionType, exc_type) - ) - return ExceptionCatcher() + self.comps = [self.r.core] + self.r.core.getChildren(deep=True) - def assertEqual(self, expected, actual): - if expected != actual: - raise AssertionError( - "(expected) {} != {} (actual)".format(expected, actual) - ) + @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") + def test_noConflicts(self): + """Make sure sync works across processes. - def assertNotEqual(self, expected, actual): - if expected == actual: - raise AssertionError( - "(expected) {} == {} (actual)".format(expected, actual) - ) + .. test:: Synchronize a reactor's state across processes. + :id: T_ARMI_CMP_MPI0 + :tests: R_ARMI_CMP_MPI + """ + _syncCount = self.r.syncMpiState() - def mpitest_noConflicts(self): for ci, comp in enumerate(self.comps): if ci % context.MPI_SIZE == context.MPI_RANK: comp.p.param1 = (context.MPI_RANK + 1) * 30.0 else: self.assertNotEqual((context.MPI_RANK + 1) * 30.0, comp.p.param1) - self.assertEqual(len(self.comps), self.r.syncMpiState()) + syncCount = self.r.syncMpiState() + self.assertEqual(len(self.comps), syncCount) for ci, comp in enumerate(self.comps): self.assertEqual((ci % context.MPI_SIZE + 1) * 30.0, comp.p.param1) - def mpitest_noConflicts_setByString(self): - """Make sure params set by string also work with sync.""" - for ci, comp in enumerate(self.comps): - if ci % context.MPI_SIZE == context.MPI_RANK: - comp.p.param2 = (context.MPI_RANK + 1) * 30.0 - else: - self.assertNotEqual((context.MPI_RANK + 1) * 30.0, comp.p.param2) - - self.assertEqual(len(self.comps), self.r.syncMpiState()) - - for ci, comp in enumerate(self.comps): - self.assertEqual((ci % context.MPI_SIZE + 1) * 30.0, comp.p.param2) + @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") + def test_withConflicts(self): + """Test conflicts arise correctly if we force a conflict. - def mpitest_withConflicts(self): + .. test:: Raise errors when there are conflicts across processes. + :id: T_ARMI_CMP_MPI1 + :tests: R_ARMI_CMP_MPI + """ self.r.core.p.param1 = (context.MPI_RANK + 1) * 99.0 with self.assertRaises(ValueError): self.r.syncMpiState() - def mpitest_withConflictsButSameValue(self): + @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") + def test_withConflictsButSameValue(self): + """Test that conflicts are ignored if the values are the same. + + .. test:: Don't raise errors when multiple processes make the same changes. + :id: T_ARMI_CMP_MPI2 + :tests: R_ARMI_CMP_MPI + """ self.r.core.p.param1 = (context.MPI_SIZE + 1) * 99.0 self.r.syncMpiState() self.assertEqual((context.MPI_SIZE + 1) * 99.0, self.r.core.p.param1) - def mpitest_noConflictsMaintainWithStateRetainer(self): - assigned = [] - with self.r.retainState(parameters.inCategory("cat1")): - for ci, comp in enumerate(self.comps): - comp.p.param2 = 99 * ci - if ci % context.MPI_SIZE == context.MPI_RANK: - comp.p.param1 = (context.MPI_RANK + 1) * 30.0 - assigned.append(parameters.SINCE_ANYTHING) - else: - self.assertNotEqual((context.MPI_RANK + 1) * 30.0, comp.p.param1) - assigned.append(parameters.NEVER) - - # 1st inside state retainer - self.assertEqual( - True, all(c.p.assigned == parameters.SINCE_ANYTHING for c in self.comps) - ) - - # confirm outside state retainer - self.assertEqual(assigned, [c.p.assigned for ci, c in enumerate(self.comps)]) - - # this rank's "assigned" components are not assigned on the workers, and so will - # be updated - self.assertEqual(len(self.comps), self.r.syncMpiState()) - - for ci, comp in enumerate(self.comps): - self.assertEqual((ci % context.MPI_SIZE + 1) * 30.0, comp.p.param1) - - def mpitest_conflictsMaintainWithStateRetainer(self): + @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") + def test_conflictsMaintainWithStateRetainer(self): + """Test that the state retainer fails correctly when it should.""" with self.r.retainState(parameters.inCategory("cat2")): for _, comp in enumerate(self.comps): comp.p.param2 = 99 * context.MPI_RANK with self.assertRaises(ValueError): self.r.syncMpiState() - - def mpitest_rxCoeffsProcess(self): - """This test mimics the process for rxCoeffs when doing distributed doppler.""" - - def do(): - # we will do this over 4 passes (there are 4 * MPI_SIZE assemblies) - for passNum in range(4): - with self.r.retainState(parameters.inCategory("cat2")): - self.r.p.param3 = "hi" - for c in self.comps: - c.p.param1 = ( - 99 * context.MPI_RANK - ) # this will get reset after state retainer - a = self.r.core[passNum * context.MPI_SIZE + context.MPI_RANK] - a.p.param2 = context.MPI_RANK * 20.0 - for b in a: - b.p.param2 = context.MPI_RANK * 10.0 - - for ai, a2 in enumerate(self.r): - if ai % context.MPI_SIZE != context.MPI_RANK: - assert "param2" not in a2.p - - self.assertEqual(parameters.SINCE_ANYTHING, param1.assigned) - self.assertEqual(parameters.SINCE_ANYTHING, param2.assigned) - self.assertEqual(parameters.SINCE_ANYTHING, param3.assigned) - self.assertEqual(parameters.SINCE_ANYTHING, a.p.assigned) - - self.r.syncMpiState() - - self.assertEqual( - parameters.SINCE_ANYTHING - & ~parameters.SINCE_LAST_DISTRIBUTE_STATE, - param1.assigned, - ) - self.assertEqual( - parameters.SINCE_ANYTHING - & ~parameters.SINCE_LAST_DISTRIBUTE_STATE, - param2.assigned, - ) - self.assertEqual( - parameters.SINCE_ANYTHING - & ~parameters.SINCE_LAST_DISTRIBUTE_STATE, - param3.assigned, - ) - self.assertEqual( - parameters.SINCE_ANYTHING - & ~parameters.SINCE_LAST_DISTRIBUTE_STATE, - a.p.assigned, - ) - - self.assertEqual(parameters.NEVER, param1.assigned) - self.assertEqual(parameters.SINCE_ANYTHING, param2.assigned) - self.assertEqual(parameters.NEVER, param3.assigned) - self.assertEqual(parameters.SINCE_ANYTHING, a.p.assigned) - do_assert(passNum) - - param1 = self.r.p.paramDefs["param1"] - param2 = self.r.p.paramDefs["param2"] - param3 = self.r.p.paramDefs["param3"] - - def do_assert(passNum): - # ensure all assemblies and blocks set values for param2, but param1 is - # empty - for rank in range(context.MPI_SIZE): - a = self.r.core[passNum * context.MPI_SIZE + rank] - assert "param1" not in a.p - assert "param3" not in a.p - self.assertEqual(rank * 20, a.p.param2) - for b in a: - self.assertEqual(rank * 10, b.p.param2) - assert "param1" not in b.p - assert "param3" not in b.p - - if context.MPI_RANK == 0: - with self.r.retainState(parameters.inCategory("cat2")): - context.MPI_COMM.bcast(self.r) - do() - [do_assert(passNum) for passNum in range(4)] - [do_assert(passNum) for passNum in range(4)] - else: - del self.r - self.r = context.MPI_COMM.bcast(None) - do() From eef678c7792e56079e89c411b17d69531b2bdf98 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 7 Dec 2023 07:27:12 -0800 Subject: [PATCH 076/176] Improving grid tests and documentation (#1524) --- armi/reactor/grids/hexagonal.py | 12 ++++--- armi/reactor/grids/tests/test_grids.py | 46 ++++++++++++++------------ armi/reactor/tests/test_blocks.py | 11 +++++- 3 files changed, 42 insertions(+), 27 deletions(-) diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py index 000a8c765..2edd56e94 100644 --- a/armi/reactor/grids/hexagonal.py +++ b/armi/reactor/grids/hexagonal.py @@ -50,6 +50,10 @@ class HexGrid(StructuredGrid): It is recommended to use :meth:`fromPitch` rather than calling the ``__init__`` constructor directly. + .. impl:: Construct a hexagonal lattice. + :id: I_ARMI_GRID_HEX + :implements: R_ARMI_GRID_HEX + Notes ----- In an axial plane (i, j) are as follows (second one is pointedEndUp):: @@ -74,14 +78,14 @@ def fromPitch(pitch, numRings=25, armiObject=None, pointedEndUp=False, symmetry= """ Build a finite step-based 2-D hex grid from a hex pitch in cm. - .. impl:: Construct a hexagonal lattice. - :id: I_ARMI_GRID_HEX - :implements: R_ARMI_GRID_HEX - .. impl:: Hexagonal grids can be points-up or flats-up. :id: I_ARMI_GRID_HEX_TYPE :implements: R_ARMI_GRID_HEX_TYPE + .. impl:: The user can specify the symmetry of a hexagonal grid when creating one. + :id: I_ARMI_GRID_SYMMETRY1 + :implements: R_ARMI_GRID_SYMMETRY + Parameters ---------- pitch : float diff --git a/armi/reactor/grids/tests/test_grids.py b/armi/reactor/grids/tests/test_grids.py index 4cdb73ab7..bbfb5ca2e 100644 --- a/armi/reactor/grids/tests/test_grids.py +++ b/armi/reactor/grids/tests/test_grids.py @@ -337,18 +337,8 @@ def test_thirdAndFullSymmetry(self): :id: T_ARMI_GRID_SYMMETRY :tests: R_ARMI_GRID_SYMMETRY """ - full = grids.HexGrid.fromPitch(1.0) - full.symmetry = str( - geometry.SymmetryType( - geometry.DomainType.FULL_CORE, geometry.BoundaryType.NO_SYMMETRY - ) - ) - third = grids.HexGrid.fromPitch(1.0) - third.symmetry = str( - geometry.SymmetryType( - geometry.DomainType.THIRD_CORE, geometry.BoundaryType.PERIODIC - ) - ) + full = grids.HexGrid.fromPitch(1.0, symmetry="full core") + third = grids.HexGrid.fromPitch(1.0, symmetry="third core periodic") # check full core self.assertEqual(full.getMinimumRings(2), 2) @@ -453,16 +443,28 @@ def test_adjustPitch(self): :id: T_ARMI_GRID_GLOBAL_POS0 :tests: R_ARMI_GRID_GLOBAL_POS """ - grid = grids.HexGrid.fromPitch(1.0, numRings=3) - v1 = grid.getCoordinates((1, 0, 0)) - grid.changePitch(2.0) - v2 = grid.getCoordinates((1, 0, 0)) - assert_allclose(2 * v1, v2) - self.assertEqual(grid.pitch, 2.0) - - # test number of rings - numRings = 3 - self.assertEqual(grid._unitStepLimits[0][1], numRings) + # run this test for a grid with no offset, and then a few random offset values + for offset in [0, 1, 1.123, 3.14]: + # build a hex grid with pitch=1, 3 rings, and the above offset + grid = grids.HexGrid( + unitSteps=((1.5 / math.sqrt(3), 0.0, 0.0), (0.5, 1, 0.0), (0, 0, 0)), + unitStepLimits=((-3, 3), (-3, 3), (0, 1)), + offset=numpy.array([offset, offset, offset]), + ) + + # test that we CAN change the pitch, and it scales the grid (but not the offset) + v1 = grid.getCoordinates((1, 0, 0)) + grid.changePitch(2.0) + v2 = grid.getCoordinates((1, 0, 0)) + assert_allclose(2 * v1 - offset, v2) + self.assertEqual(grid.pitch, 2.0) + + # basic sanity: test number of rings has changed + self.assertEqual(grid._unitStepLimits[0][1], 3) + + # basic sanity: check the offset exists and is correct + for i in range(3): + self.assertEqual(grid.offset[i], offset) def test_badIndices(self): grid = grids.HexGrid.fromPitch(1.0, numRings=3) diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py index d420f245f..f99070263 100644 --- a/armi/reactor/tests/test_blocks.py +++ b/armi/reactor/tests/test_blocks.py @@ -2077,9 +2077,18 @@ def test_gridCreation(self): # Then it's spatialLocator must be of size 169 locations = c.spatialLocator self.assertEqual(type(locations), grids.MultiIndexLocation) + mult = 0 - for _ in locations: + uniqueLocations = set() + for loc in locations: mult = mult + 1 + + # test for the uniqueness of the locations (since mult > 1) + if loc not in uniqueLocations: + uniqueLocations.add(loc) + else: + self.assertTrue(False, msg="Duplicate location found!") + self.assertEqual(mult, 169) def test_gridNumPinsAndLocations(self): From db1d1525989368b03130e0e5709595bb7a84c8f5 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 7 Dec 2023 07:42:11 -0800 Subject: [PATCH 077/176] Add executer options to executer unit test (#1523) --- armi/physics/tests/test_executers.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/armi/physics/tests/test_executers.py b/armi/physics/tests/test_executers.py index a918a6152..e4e9fefad 100644 --- a/armi/physics/tests/test_executers.py +++ b/armi/physics/tests/test_executers.py @@ -113,10 +113,17 @@ def test_runExternalExecutable(self): """ filePath = "test_runExternalExecutable.py" outFile = "tmp.txt" + label = "printExtraStuff" - class TestSillyExecuter(executers.Executer): + class MockExecutionOptions(executers.ExecutionOptions): + pass + + class MockExecuter(executers.Executer): def run(self, args): - subprocess.run(["python", filePath, args]) + if self.options.label == label: + subprocess.run(["python", filePath, "extra stuff"]) + else: + subprocess.run(["python", filePath, args]) with directoryChangers.TemporaryDirectoryChanger(): # build a mock external program (a little Python script) @@ -126,7 +133,8 @@ def run(self, args): self.assertFalse(os.path.exists(outFile)) # set up an executer for our little test program - exe = TestSillyExecuter(None, None) + opts = MockExecutionOptions() + exe = MockExecuter(opts, None) exe.run("") # make sure the output file exists now @@ -141,6 +149,12 @@ def run(self, args): newTxt = open(outFile, "r").read() self.assertIn(testString, newTxt) + # now prove the options object can affect the execution + exe.options.label = label + exe.run("") + newerTxt = open(outFile, "r").read() + self.assertIn("extra stuff", newerTxt) + @staticmethod def __makeALittleTestProgram(filePath, outFile): """Helper method to write a tiny Python script. From 8b9873cfe9df8dac6a26b28a7520c45b65fa3a50 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 7 Dec 2023 08:02:51 -0800 Subject: [PATCH 078/176] Adding a test and crumbs for defining flags (#1521) --- .../neutronics/tests/test_energyGroups.py | 8 +-- armi/plugins.py | 4 ++ armi/tests/test_plugins.py | 62 +++++++++++++++++++ armi/utils/flags.py | 2 +- armi/utils/tests/test_flags.py | 2 +- 5 files changed, 69 insertions(+), 9 deletions(-) diff --git a/armi/physics/neutronics/tests/test_energyGroups.py b/armi/physics/neutronics/tests/test_energyGroups.py index cb68011ae..6d789cd49 100644 --- a/armi/physics/neutronics/tests/test_energyGroups.py +++ b/armi/physics/neutronics/tests/test_energyGroups.py @@ -34,17 +34,11 @@ def test_invalidGroupStructureType(self): energyGroups.getGroupStructureType(energyBounds) def test_consistenciesBetweenGroupStructureAndGroupStructureType(self): - """ - Test that the reverse lookup of the energy group structures work. + """Test that the reverse lookup of the energy group structures work. .. test:: Check the neutron energy group bounds for a given group structure. :id: T_ARMI_EG_NE1 :tests: R_ARMI_EG_NE - - Notes - ----- - Several group structures point to the same energy group structure so the reverse lookup will fail to - get the correct group structure type. """ for groupStructureType in energyGroups.GROUP_STRUCTURE: self.assertEqual( diff --git a/armi/plugins.py b/armi/plugins.py index f8ed8bb69..5ba26687a 100644 --- a/armi/plugins.py +++ b/armi/plugins.py @@ -256,6 +256,10 @@ def defineFlags() -> Dict[str, Union[int, flags.auto]]: of flags (see :py:mod:`armi.reactor.flags`), new flags should probably refer to novel design elements, rather than novel behaviors. + .. impl:: Plugins can define new, unique flags to the system. + :id: I_ARMI_FLAG_EXTEND1 + :implements: R_ARMI_FLAG_EXTEND + See Also -------- armi.reactor.flags diff --git a/armi/tests/test_plugins.py b/armi/tests/test_plugins.py index 8adf00ed2..029c9acb5 100644 --- a/armi/tests/test_plugins.py +++ b/armi/tests/test_plugins.py @@ -14,15 +14,77 @@ """Provides functionality for testing implementations of plugins.""" import unittest +from copy import deepcopy from typing import Optional import yamlize +from armi import context +from armi import getApp from armi import interfaces from armi import plugins from armi import settings +from armi import utils from armi.physics.neutronics import NeutronicsPlugin from armi.reactor.blocks import Block +from armi.reactor.flags import Flags + + +class PluginFlags1(plugins.ArmiPlugin): + """Simple Plugin that defines a single, new flag.""" + + @staticmethod + @plugins.HOOKIMPL + def defineFlags(): + """Function to provide new Flags definitions.""" + return {"SUPER_FLAG": utils.flags.auto()} + + +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()) + + def tearDown(self): + """Restore the App to its original state.""" + import armi + + armi._app = self._backupApp + context.APP_NAME = "armi" + + def test_defineFlags(self): + """Define a new flag using the plugin defineFlags() method. + + .. test:: Define a new, unique flag through the plugin pathway. + :id: T_ARMI_FLAG_EXTEND1 + :tests: R_ARMI_FLAG_EXTEND + """ + app = getApp() + + # show the new plugin isn't loaded yet + pluginNames = [p[0] for p in app.pluginManager.list_name_plugin()] + self.assertNotIn("PluginFlags1", pluginNames) + + # show the flag doesn't exist yet + with self.assertRaises(AttributeError): + Flags.SUPER_FLAG + + # load the plugin + app.pluginManager.register(PluginFlags1) + + # show the new plugin is loaded now + pluginNames = [p[0] for p in app.pluginManager.list_name_plugin()] + self.assertIn("PluginFlags1", pluginNames) + + # force-register new flags from the new plugin + app._pluginFlagsRegistered = False + app.registerPluginFlags() + + # show the flag exists now + self.assertEqual(type(Flags.SUPER_FLAG._value), int) class TestPluginBasics(unittest.TestCase): diff --git a/armi/utils/flags.py b/armi/utils/flags.py index 76ffd2597..55f60c5cd 100644 --- a/armi/utils/flags.py +++ b/armi/utils/flags.py @@ -221,7 +221,7 @@ def extend(cls, fields: Dict[str, Union[int, auto]]): the new data, since classes are mutable. .. impl:: Set of flags are extensible without loss of uniqueness. - :id: I_ARMI_FLAG_EXTEND + :id: I_ARMI_FLAG_EXTEND0 :implements: R_ARMI_FLAG_EXTEND Parameters diff --git a/armi/utils/tests/test_flags.py b/armi/utils/tests/test_flags.py index a6b0238fe..8d97abc81 100644 --- a/armi/utils/tests/test_flags.py +++ b/armi/utils/tests/test_flags.py @@ -73,7 +73,7 @@ def test_collision_extension(self): """Ensure the set of flags cannot be programmatically extended if duplicate created. .. test:: Set of flags are extensible without loss of uniqueness. - :id: T_ARMI_FLAG_EXTEND + :id: T_ARMI_FLAG_EXTEND0 :tests: R_ARMI_FLAG_EXTEND """ From 4690ef875631f41b60fdaa969b058ad31ad6d312 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 7 Dec 2023 16:39:40 -0800 Subject: [PATCH 079/176] Responding to review of assembly requirements (#1529) --- armi/reactor/assemblies.py | 15 +++++---------- armi/reactor/tests/test_assemblies.py | 23 ++++++++++++++--------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/armi/reactor/assemblies.py b/armi/reactor/assemblies.py index 66e3c1459..5683f640d 100644 --- a/armi/reactor/assemblies.py +++ b/armi/reactor/assemblies.py @@ -216,16 +216,16 @@ def getLocation(self): """ Get string label representing this object's location. + .. impl:: Assembly location is retrievable. + :id: I_ARMI_ASSEM_POSI0 + :implements: R_ARMI_ASSEM_POSI + Notes ----- This function (and its friends) were created before the advent of both the grid/spatialLocator system and the ability to represent things like the SFP as siblings of a Core. In future, this will likely be re-implemented in terms of just spatialLocator objects. - - .. impl:: Assembly location is retrievable. - :id: I_ARMI_ASSEM_POSI0 - :implements: R_ARMI_ASSEM_POSI """ # just use ring and position, not axial (which is 0) if not self.parent: @@ -1227,12 +1227,7 @@ def rotate(self, rad): class HexAssembly(Assembly): - """Placeholder, so users can explicitly define a hex-based assembly. - - .. impl:: Assembly of hex blocks. - :id: I_ARMI_ASSEM_HEX - :implements: R_ARMI_ASSEM_HEX - """ + """Placeholder, so users can explicitly define a hex-based assembly.""" pass diff --git a/armi/reactor/tests/test_assemblies.py b/armi/reactor/tests/test_assemblies.py index 128789cb5..92cf0dd40 100644 --- a/armi/reactor/tests/test_assemblies.py +++ b/armi/reactor/tests/test_assemblies.py @@ -1101,24 +1101,29 @@ def test_rotate(self): self.assertIn("No rotation method defined", mock.getStdout()) def test_assem_block_types(self): - """Test that all children of an assembly are blocks. + """Test that all children of an assembly are blocks, ordered from top to bottom. - .. test:: Validate child types of assembly are blocks. + .. test:: Validate child types of assembly are blocks, ordered from top to bottom. :id: T_ARMI_ASSEM_BLOCKS :tests: R_ARMI_ASSEM_BLOCKS """ + coords = [] for b in self.assembly.getBlocks(): - # Confirm children are blocks self.assertIsInstance(b, blocks.Block) - def test_assem_hex_type(self): - """Test that all children of a hex assembly are hexagons. + # get coords from the child blocks + coords.append(b.getLocation()) - .. test:: Validate child types of assembly are Hex type. - :id: T_ARMI_ASSEM_HEX - :tests: R_ARMI_ASSEM_HEX - """ + # get the Z-coords for each block + zCoords = [int(c.split("-")[-1]) for c in coords] + + # verify the blocks are ordered top-to-bottom, vertically + for i in range(1, len(zCoords)): + self.assertGreater(zCoords[i], zCoords[i - 1]) + + def test_assem_hex_type(self): + """Test that all children of a hex assembly are hexagons.""" for b in self.assembly.getBlocks(): # For a hex assem, confirm they are of type "Hexagon" From 7e1e16736b7082c2ce4892bb36243fb080c63a48 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Fri, 8 Dec 2023 06:41:41 -0800 Subject: [PATCH 080/176] Improving block tests and test crumbs (#1526) --- .../tests/test_macroXSGenerationInterface.py | 2 +- armi/reactor/tests/test_blocks.py | 34 +++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/armi/physics/neutronics/tests/test_macroXSGenerationInterface.py b/armi/physics/neutronics/tests/test_macroXSGenerationInterface.py index 8ab46e20d..9214916a5 100644 --- a/armi/physics/neutronics/tests/test_macroXSGenerationInterface.py +++ b/armi/physics/neutronics/tests/test_macroXSGenerationInterface.py @@ -29,7 +29,7 @@ class TestMacroXSGenerationInterface(unittest.TestCase): def test_macroXSGenerationInterfaceBasics(self): """Test the macroscopic XS generating interfaces. - .. test::Build macroscopic cross sections for all blocks in the reactor. + .. test:: Build macroscopic cross sections for all blocks in the reactor. :id: T_ARMI_MACRO_XS :tests: R_ARMI_MACRO_XS """ diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py index f99070263..d0a18eaa6 100644 --- a/armi/reactor/tests/test_blocks.py +++ b/armi/reactor/tests/test_blocks.py @@ -1842,17 +1842,39 @@ def setUp(self): r.core.add(a, loc1) def test_getArea(self): - cur = self.HexBlock.getArea() - ref = math.sqrt(3) / 2.0 * 70.6**2 - places = 6 - self.assertAlmostEqual(cur, ref, places=places) + """Test that we can correctly calculate the area of a hexagonal block. + + .. test:: Users can create blocks that have the correct hexagonal area. + :id: T_ARMI_BLOCK_HEX0 + :tests: R_ARMI_BLOCK_HEX + """ + # Test for various outer and inner pitches for HexBlocks with hex holes + for op in (20.0, 20.4, 20.1234, 25.001): + for ip in (0.0, 5.0001, 7.123, 10.0): + # generate a block with a different outer pitch + hBlock = blocks.HexBlock("TestAreaHexBlock") + hexDims = { + "Tinput": 273.0, + "Thot": 273.0, + "op": op, + "ip": ip, + "mult": 1.0, + } + hComponent = components.Hexagon("duct", "UZr", **hexDims) + hBlock.add(hComponent) + + # verify the area of the hexagon (with a hex hole) is correct + cur = hBlock.getArea() + ref = math.sqrt(3) / 2.0 * op**2 + ref -= math.sqrt(3) / 2.0 * ip**2 + self.assertAlmostEqual(cur, ref, places=6, msg=str(op)) def test_component_type(self): """ Test that a hex block has the proper "hexagon" __name__. - .. test:: Users can create hex shaped blocks. - :id: T_ARMI_BLOCK_HEX + .. test:: Users can create blocks with a hexagonal shape. + :id: T_ARMI_BLOCK_HEX1 :tests: R_ARMI_BLOCK_HEX """ pitch_comp_type = self.HexBlock.PITCH_COMPONENT_TYPE[0] From 9f2b322047af443fb96c3409c5afc91dcebdb5de Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Fri, 8 Dec 2023 10:32:24 -0800 Subject: [PATCH 081/176] Ruff linting is now mandatory on all PRs (#1534) --- .github/workflows/linting.yaml | 1 - armi/nuclearDataIO/cccc/tests/test_isotxs.py | 1 - armi/operators/tests/test_operators.py | 2 +- .../neutronics/crossSectionGroupManager.py | 6 +++--- .../tests/test_crossSectionManager.py | 20 +++++-------------- doc/release/0.2.rst | 2 ++ 6 files changed, 11 insertions(+), 21 deletions(-) diff --git a/.github/workflows/linting.yaml b/.github/workflows/linting.yaml index 28c960695..af340c393 100644 --- a/.github/workflows/linting.yaml +++ b/.github/workflows/linting.yaml @@ -18,5 +18,4 @@ jobs: - name: Install Tox and any other packages run: pip install tox - name: Run Linter - continue-on-error: true run: tox -e lint diff --git a/armi/nuclearDataIO/cccc/tests/test_isotxs.py b/armi/nuclearDataIO/cccc/tests/test_isotxs.py index 484f7aa4a..b54e3a29b 100644 --- a/armi/nuclearDataIO/cccc/tests/test_isotxs.py +++ b/armi/nuclearDataIO/cccc/tests/test_isotxs.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. """Tests the workings of the library wrappers.""" -import filecmp import unittest from armi import nuclearDataIO diff --git a/armi/operators/tests/test_operators.py b/armi/operators/tests/test_operators.py index b1a7274e3..e99253b29 100644 --- a/armi/operators/tests/test_operators.py +++ b/armi/operators/tests/test_operators.py @@ -237,7 +237,7 @@ def test_createOperator(self): # validate some more nitty-gritty operator details come from settings burnStepsSetting = cs["burnSteps"] - if not type(burnStepsSetting) == list: + if type(burnStepsSetting) != list: burnStepsSetting = [burnStepsSetting] self.assertEqual(o.burnSteps, burnStepsSetting) self.assertEqual(o.maxBurnSteps, max(burnStepsSetting)) diff --git a/armi/physics/neutronics/crossSectionGroupManager.py b/armi/physics/neutronics/crossSectionGroupManager.py index 4de167e58..f0cb75abd 100644 --- a/armi/physics/neutronics/crossSectionGroupManager.py +++ b/armi/physics/neutronics/crossSectionGroupManager.py @@ -395,7 +395,7 @@ def _getAverageComponentNumberDensities(self, compIndex): def _getAverageComponentTemperature(self, compIndex): """ - Get weighted average component temperature for the collection + Get weighted average component temperature for the collection. Notes ----- @@ -430,7 +430,7 @@ def _getAverageComponentTemperature(self, compIndex): def _performAverageByComponent(self): """ - Check if block collection averaging can/should be performed by component + Check if block collection averaging can/should be performed by component. If the components of blocks in the collection are similar and the user has requested component-level averaging, return True. @@ -443,7 +443,7 @@ def _performAverageByComponent(self): def _checkBlockSimilarity(self): """ - Check if blocks in the collection have similar components + Check if blocks in the collection have similar components. If the components of blocks in the collection are similar and the user has requested component-level averaging, return True. diff --git a/armi/physics/neutronics/tests/test_crossSectionManager.py b/armi/physics/neutronics/tests/test_crossSectionManager.py index 1b1e99b35..9527fc731 100644 --- a/armi/physics/neutronics/tests/test_crossSectionManager.py +++ b/armi/physics/neutronics/tests/test_crossSectionManager.py @@ -169,9 +169,7 @@ def test_createRepresentativeBlock(self): self.assertAlmostEqual(newBc.avgNucTemperatures["NA23"], 402.0) def test_createRepresentativeBlockDissimilar(self): - """ - Test creation of a representative block from a collection with dissimilar blocks - """ + """Test creation of a representative block from a collection with dissimilar blocks.""" uniqueBlock = test_blocks.loadTestBlock() uniqueBlock.p.percentBu = 50.0 fpFactory = test_lumpedFissionProduct.getDummyLFPFile() @@ -259,9 +257,7 @@ def setUp(self): self.bc.extend(blockCopies) def test_getAverageComponentNumberDensities(self): - """ - Test component number density averaging - """ + """Test component number density averaging.""" # becaue of the way densities are set up, the middle block (index 1 of 0-2) component densities are equivalent to the average b = self.bc[1] for compIndex, c in enumerate(b.getComponents()): @@ -275,9 +271,7 @@ def test_getAverageComponentNumberDensities(self): ) def test_getAverageComponentTemperature(self): - """ - Test mass-weighted component temperature averaging - """ + """Test mass-weighted component temperature averaging.""" b = self.bc[0] massWeightedIncrease = 5.0 / 3.0 baseTemps = [600, 400, 500, 500, 400, 500, 400] @@ -291,9 +285,7 @@ def test_getAverageComponentTemperature(self): ) def test_getAverageComponentTemperatureVariedWeights(self): - """ - Test mass-weighted component temperature averaging with variable weights - """ + """Test mass-weighted component temperature averaging with variable weights.""" # make up a fake weighting with power param self.bc.weightingParam = "power" for i, b in enumerate(self.bc): @@ -310,9 +302,7 @@ def test_getAverageComponentTemperatureVariedWeights(self): ) def test_getAverageComponentTemperatureNoMass(self): - """ - Test component temperature averaging when the components have no mass - """ + """Test component temperature averaging when the components have no mass.""" for b in self.bc: for nuc in b.getNuclides(): b.setNumberDensity(nuc, 0.0) diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst index e46cc420e..c86167e60 100644 --- a/doc/release/0.2.rst +++ b/doc/release/0.2.rst @@ -8,6 +8,8 @@ Release Date: TBD What's new in ARMI ------------------ +#. Many new references to requirement tests and implementations were added to docstrings. +#. The ``ruff`` linter is now mandatory on all ARMI PRs. #. The ``_copyInputsHelper()`` gives relative path and not absolute after copy. (`PR#1416 `_) #. ARMI now mandates ``ruff`` linting. (`PR#1419 `_) #. Removed all old ARMI requirements, to start the work fresh. (`PR#1438 `_) From 4be1314938ca4754724960ac9df34dd245650c94 Mon Sep 17 00:00:00 2001 From: bsculac <102382931+bsculac@users.noreply.github.com> Date: Fri, 8 Dec 2023 16:35:42 -0500 Subject: [PATCH 082/176] Global flux review (#1536) * respond to comments on the globalFluxInterface reqs * add assert for nonuniform assemblies * remove unused assignment --- .../globalFlux/globalFluxInterface.py | 2 +- .../tests/test_globalFluxInterface.py | 88 ++++++++++++++----- 2 files changed, 67 insertions(+), 23 deletions(-) diff --git a/armi/physics/neutronics/globalFlux/globalFluxInterface.py b/armi/physics/neutronics/globalFlux/globalFluxInterface.py index 9aa8e1c2c..d7b6cc48d 100644 --- a/armi/physics/neutronics/globalFlux/globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/globalFluxInterface.py @@ -1183,7 +1183,7 @@ def computeDpaRate(mgFlux, dpaXs): r""" Compute the DPA rate incurred by exposure of a certain flux spectrum. - .. impl:: Compute DPA and DPA rates. + .. impl:: Compute DPA rates. :id: I_ARMI_FLUX_DPA :implements: R_ARMI_FLUX_DPA diff --git a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py index 3d152ab6a..01530d241 100644 --- a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py @@ -13,6 +13,7 @@ # limitations under the License. """Tests for generic global flux interface.""" import unittest +from unittest.mock import patch import numpy @@ -107,11 +108,14 @@ class TestGlobalFluxOptions(unittest.TestCase): """Tests for GlobalFluxOptions.""" def test_readFromSettings(self): - """Test reading global flux options. + """Test reading global flux options from case settings. .. test:: Tests GlobalFluxOptions. - :id: T_ARMI_FLUX_OPTIONS + :id: T_ARMI_FLUX_OPTIONS_FROM_CASE_SETTINGS :tests: R_ARMI_FLUX_OPTIONS + :acceptance_criteria: This test is satisfied when it demonstrates + the ability of the globalFluxInterface to read options from a case + settings object. """ cs = settings.Settings() opts = globalFluxInterface.GlobalFluxOptions("neutronics-run") @@ -119,6 +123,15 @@ def test_readFromSettings(self): self.assertFalse(opts.adjoint) def test_readFromReactors(self): + """Test reading global flux options from reactor objects. + + .. test:: Tests GlobalFluxOptions. + :id: T_ARMI_FLUX_OPTIONS_FROM_REACTOR + :tests: R_ARMI_FLUX_OPTIONS + :acceptance_criteria: This test is satisfied when it demonstrates + the ability of the globalFluxInterface to read options from a + reactor object. + """ reactor = MockReactor() opts = globalFluxInterface.GlobalFluxOptions("neutronics-run") opts.fromReactor(reactor) @@ -145,7 +158,7 @@ def test_computeDpaRate(self): """ Compute DPA and DPA rates from multi-group neutron flux and cross sections. - .. test:: Compute DPA and DPA rates. + .. test:: Compute DPA rates. :id: T_ARMI_FLUX_DPA :tests: R_ARMI_FLUX_DPA """ @@ -189,7 +202,12 @@ def test_checkEnergyBalance(self): cs = settings.Settings() _o, r = test_reactors.loadTestReactor() gfi = MockGlobalFluxInterface(r, cs) - gfi.checkEnergyBalance() + self.assertEqual(gfi.checkEnergyBalance(), None) + + # Test when nameplate power doesn't equal sum of block power + r.core.p.power = 1e-10 + with self.assertRaises(ValueError): + gfi.checkEnergyBalance() class TestGlobalFluxInterfaceWithExecuters(unittest.TestCase): @@ -204,20 +222,31 @@ def setUp(self): self.r.core.p.keff = 1.0 self.gfi = MockGlobalFluxWithExecuters(self.r, self.cs) - def test_executerInteraction(self): + @patch( + "armi.physics.neutronics.globalFlux.globalFluxInterface.GlobalFluxExecuter._execute" + ) + @patch( + "armi.physics.neutronics.globalFlux.globalFluxInterface.GlobalFluxExecuter._performGeometryTransformations" + ) + def test_executerInteraction(self, mockGeometryTransform, mockExecute): """Run the global flux interface and executer though one time now. - .. test:: Run the global flux interface to prove the mesh in the reactor is suffience for the neutronics solver. - :id: T_ARMI_FLUX_GEOM_TRANSFORM1 + .. test:: Run the global flux interface to check that the mesh + converter is called before the neutronics solver. + :id: T_ARMI_FLUX_GEOM_TRANSFORM_CHECK_CALL_ORDER :tests: R_ARMI_FLUX_GEOM_TRANSFORM + :acceptance_criteria: The test is considered passing when the mesh + converter is verified to be called before the neutronics solver. """ - gfi, r = self.gfi, self.r + call_order = [] + mockGeometryTransform.side_effect = lambda *a, **kw: call_order.append( + mockGeometryTransform + ) + mockExecute.side_effect = lambda *a, **kw: call_order.append(mockExecute) + gfi = self.gfi gfi.interactBOC() gfi.interactEveryNode(0, 0) - r.p.timeNode += 1 - gfi.interactEveryNode(0, 1) - gfi.interactEOC() - self.assertAlmostEqual(r.core.p.rxSwing, (1.02 - 1.01) / 1.01 * 1e5) + self.assertEqual([mockGeometryTransform, mockExecute], call_order) def test_calculateKeff(self): self.assertEqual(self.gfi.calculateKeff(), 1.05) # set in mock @@ -246,9 +275,21 @@ def test_getTightCouplingValue(self): for a in self.r.core.getChildren(): for b in a: b.p.power = 10.0 - self.assertIsInstance(self.gfi.getTightCouplingValue(), list) + self.assertEqual( + self.gfi.getTightCouplingValue(), + self._getCouplingPowerDistributions(self.r.core), + ) self._setTightCouplingFalse() + @staticmethod + def _getCouplingPowerDistributions(core): + scaledPowers = [] + for a in core: + assemblyPower = sum(b.p.power for b in a) + scaledPowers.append([b.p.power / assemblyPower for b in a]) + + return scaledPowers + def _setTightCouplingTrue(self): self.cs["tightCoupling"] = True self.gfi._setTightCouplingDefaults() @@ -267,23 +308,26 @@ def setUpClass(cls): cls.r.core.p.keff = 1.0 cls.gfi = MockGlobalFluxWithExecutersNonUniform(cls.r, cs) - def test_executerInteractionNonUniformAssems(self): + @patch("armi.reactor.converters.uniformMesh.converterFactory") + def test_executerInteractionNonUniformAssems(self, mockConverterFactory): """Run the global flux interface with non-uniform assemblies. This will serve as a broad end-to-end test of the interface, and also stress test the mesh issues with non-uniform assemblies. - .. test:: Run the global flux interface to prove the mesh in the reactor is suffience for the neutronics solver. - :id: T_ARMI_FLUX_GEOM_TRANSFORM0 + .. test:: Run the global flux interface to show the geometry converter + is called when the nonuniform mesh option is used. + :id: T_ARMI_FLUX_GEOM_TRANSFORM_CHECK_CONVERTER_CALL :tests: R_ARMI_FLUX_GEOM_TRANSFORM + :acceptance_criteria: The test is satisfied when the geometry + converter is shown to have been called when a nonuniform flag is + used. """ - gfi, r = self.gfi, self.r + gfi = self.gfi gfi.interactBOC() gfi.interactEveryNode(0, 0) - r.p.timeNode += 1 - gfi.interactEveryNode(0, 1) - gfi.interactEOC() - self.assertAlmostEqual(r.core.p.rxSwing, (1.02 - 1.01) / 1.01 * 1e5) + self.assertTrue(gfi.getExecuterOptions().hasNonUniformAssems) + mockConverterFactory.assert_called() def test_calculateKeff(self): self.assertEqual(self.gfi.calculateKeff(), 1.05) # set in mock @@ -402,7 +446,7 @@ def test_calcReactionRates(self): """ b = test_blocks.loadTestBlock() test_blocks.applyDummyData(b) - self.assertAlmostEqual(b.p.rateAbs, 0.0) + self.assertEqual(b.p.rateAbs, 0.0) globalFluxInterface.calcReactionRates(b, 1.01, b.r.core.lib) self.assertGreater(b.p.rateAbs, 0.0) vfrac = b.getComponentAreaFrac(Flags.FUEL) From c2b2c238e598e5668e00e08db79145e0a07e516b Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Fri, 8 Dec 2023 13:53:46 -0800 Subject: [PATCH 083/176] Adding new MPI tests to code coverage (#1532) --- .github/workflows/coverage.yaml | 4 +- armi/reactor/tests/test_parameters.py | 99 ---------------------- armi/tests/test_mpiParameters.py | 116 ++++++++++++++++++++++++++ tox.ini | 10 ++- 4 files changed, 124 insertions(+), 105 deletions(-) create mode 100644 armi/tests/test_mpiParameters.py diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 114f5acd5..0effcb038 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -29,9 +29,9 @@ jobs: - name: Install Tox and any other packages run: pip install tox - name: Run Coverage Part 1 - run: tox -e cov1 || true + run: tox -e cov1 - name: Run Coverage Part 2 - run: tox -e cov2 + run: tox -e cov2 || true - name: Publish to coveralls.io uses: coverallsapp/github-action@v1.1.2 with: diff --git a/armi/reactor/tests/test_parameters.py b/armi/reactor/tests/test_parameters.py index 330171c10..16bcb7f28 100644 --- a/armi/reactor/tests/test_parameters.py +++ b/armi/reactor/tests/test_parameters.py @@ -12,21 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. """Tests of the Parameters class.""" -from distutils.spawn import find_executable import copy import unittest -from armi import context -from armi.reactor import composites from armi.reactor import parameters -# determine if this is a parallel run, and MPI is installed -MPI_EXE = None -if find_executable("mpiexec.exe") is not None: - MPI_EXE = "mpiexec.exe" -elif find_executable("mpiexec") is not None: - MPI_EXE = "mpiexec" - class MockComposite: def __init__(self, name): @@ -508,92 +498,3 @@ class MockPCChild(MockPC): pcc = MockPCChild() with self.assertRaises(AssertionError): pcc.whatever = 33 - - -class MockSyncPC(parameters.ParameterCollection): - pDefs = parameters.ParameterDefinitionCollection() - with pDefs.createBuilder( - default=0.0, location=parameters.ParamLocation.AVERAGE - ) as pb: - pb.defParam("param1", "units", "p1 description", categories=["cat1"]) - pb.defParam("param2", "units", "p2 description", categories=["cat2"]) - pb.defParam("param3", "units", "p3 description", categories=["cat3"]) - - -def makeComp(name): - """Helper method for MPI sync tests: mock up a Composite with a minimal param collections.""" - c = composites.Composite(name) - c.p = MockSyncPC() - return c - - -class SynchronizationTests(unittest.TestCase): - """Some tests that must be run with mpirun instead of the standard unittest system.""" - - def setUp(self): - self.r = makeComp("reactor") - self.r.core = makeComp("core") - self.r.add(self.r.core) - for ai in range(context.MPI_SIZE * 3): - a = makeComp("assembly{}".format(ai)) - self.r.core.add(a) - for bi in range(3): - a.add(makeComp("block{}-{}".format(ai, bi))) - - self.comps = [self.r.core] + self.r.core.getChildren(deep=True) - - @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") - def test_noConflicts(self): - """Make sure sync works across processes. - - .. test:: Synchronize a reactor's state across processes. - :id: T_ARMI_CMP_MPI0 - :tests: R_ARMI_CMP_MPI - """ - _syncCount = self.r.syncMpiState() - - for ci, comp in enumerate(self.comps): - if ci % context.MPI_SIZE == context.MPI_RANK: - comp.p.param1 = (context.MPI_RANK + 1) * 30.0 - else: - self.assertNotEqual((context.MPI_RANK + 1) * 30.0, comp.p.param1) - - syncCount = self.r.syncMpiState() - self.assertEqual(len(self.comps), syncCount) - - for ci, comp in enumerate(self.comps): - self.assertEqual((ci % context.MPI_SIZE + 1) * 30.0, comp.p.param1) - - @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") - def test_withConflicts(self): - """Test conflicts arise correctly if we force a conflict. - - .. test:: Raise errors when there are conflicts across processes. - :id: T_ARMI_CMP_MPI1 - :tests: R_ARMI_CMP_MPI - """ - self.r.core.p.param1 = (context.MPI_RANK + 1) * 99.0 - with self.assertRaises(ValueError): - self.r.syncMpiState() - - @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") - def test_withConflictsButSameValue(self): - """Test that conflicts are ignored if the values are the same. - - .. test:: Don't raise errors when multiple processes make the same changes. - :id: T_ARMI_CMP_MPI2 - :tests: R_ARMI_CMP_MPI - """ - self.r.core.p.param1 = (context.MPI_SIZE + 1) * 99.0 - self.r.syncMpiState() - self.assertEqual((context.MPI_SIZE + 1) * 99.0, self.r.core.p.param1) - - @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") - def test_conflictsMaintainWithStateRetainer(self): - """Test that the state retainer fails correctly when it should.""" - with self.r.retainState(parameters.inCategory("cat2")): - for _, comp in enumerate(self.comps): - comp.p.param2 = 99 * context.MPI_RANK - - with self.assertRaises(ValueError): - self.r.syncMpiState() diff --git a/armi/tests/test_mpiParameters.py b/armi/tests/test_mpiParameters.py new file mode 100644 index 000000000..a5a6bd5e9 --- /dev/null +++ b/armi/tests/test_mpiParameters.py @@ -0,0 +1,116 @@ +# Copyright 2023 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. +"""Tests of the MPI portion of the Parameters class.""" +from distutils.spawn import find_executable +import unittest + +from armi import context +from armi.reactor import composites +from armi.reactor import parameters + +# determine if this is a parallel run, and MPI is installed +MPI_EXE = None +if find_executable("mpiexec.exe") is not None: + MPI_EXE = "mpiexec.exe" +elif find_executable("mpiexec") is not None: + MPI_EXE = "mpiexec" + + +class MockSyncPC(parameters.ParameterCollection): + pDefs = parameters.ParameterDefinitionCollection() + with pDefs.createBuilder( + default=0.0, location=parameters.ParamLocation.AVERAGE + ) as pb: + pb.defParam("param1", "units", "p1 description", categories=["cat1"]) + pb.defParam("param2", "units", "p2 description", categories=["cat2"]) + pb.defParam("param3", "units", "p3 description", categories=["cat3"]) + + +def makeComp(name): + """Helper method for MPI sync tests: mock up a Composite with a minimal param collections.""" + c = composites.Composite(name) + c.p = MockSyncPC() + return c + + +class SynchronizationTests(unittest.TestCase): + """Some tests that must be run with mpirun instead of the standard unittest system.""" + + def setUp(self): + self.r = makeComp("reactor") + self.r.core = makeComp("core") + self.r.add(self.r.core) + for ai in range(context.MPI_SIZE * 3): + a = makeComp("assembly{}".format(ai)) + self.r.core.add(a) + for bi in range(3): + a.add(makeComp("block{}-{}".format(ai, bi))) + + self.comps = [self.r.core] + self.r.core.getChildren(deep=True) + + @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") + def test_noConflicts(self): + """Make sure sync works across processes. + + .. test:: Synchronize a reactor's state across processes. + :id: T_ARMI_CMP_MPI0 + :tests: R_ARMI_CMP_MPI + """ + _syncCount = self.r.syncMpiState() + + for ci, comp in enumerate(self.comps): + if ci % context.MPI_SIZE == context.MPI_RANK: + comp.p.param1 = (context.MPI_RANK + 1) * 30.0 + else: + self.assertNotEqual((context.MPI_RANK + 1) * 30.0, comp.p.param1) + + syncCount = self.r.syncMpiState() + self.assertEqual(len(self.comps), syncCount) + + for ci, comp in enumerate(self.comps): + self.assertEqual((ci % context.MPI_SIZE + 1) * 30.0, comp.p.param1) + + @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") + def test_withConflicts(self): + """Test conflicts arise correctly if we force a conflict. + + .. test:: Raise errors when there are conflicts across processes. + :id: T_ARMI_CMP_MPI1 + :tests: R_ARMI_CMP_MPI + """ + self.r.core.p.param1 = (context.MPI_RANK + 1) * 99.0 + with self.assertRaises(ValueError): + self.r.syncMpiState() + + @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") + def test_withConflictsButSameValue(self): + """Test that conflicts are ignored if the values are the same. + + .. test:: Don't raise errors when multiple processes make the same changes. + :id: T_ARMI_CMP_MPI2 + :tests: R_ARMI_CMP_MPI + """ + self.r.core.p.param1 = (context.MPI_SIZE + 1) * 99.0 + self.r.syncMpiState() + self.assertEqual((context.MPI_SIZE + 1) * 99.0, self.r.core.p.param1) + + @unittest.skipIf(context.MPI_SIZE <= 1 or MPI_EXE is None, "Parallel test only") + def test_conflictsMaintainWithStateRetainer(self): + """Test that the state retainer fails correctly when it should.""" + with self.r.retainState(parameters.inCategory("cat2")): + for _, comp in enumerate(self.comps): + comp.p.param2 = 99 * context.MPI_RANK + + with self.assertRaises(ValueError): + self.r.syncMpiState() diff --git a/tox.ini b/tox.ini index 9c330e78e..c7e4a6693 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,7 @@ commands = git submodule update make html -# First, run code coverage over the unit tests that run MPI library code. +# First, run code coverage over the rest of the usual unit tests. [testenv:cov1] deps= mpi4py @@ -33,9 +33,9 @@ allowlist_externals = /usr/bin/mpiexec commands = pip install -e .[memprof,mpi,test] - mpiexec -n 2 --use-hwthread-cpus coverage run --rcfile=.coveragerc -m pytest --cov=armi --cov-config=.coveragerc --cov-report=lcov --ignore=venv --cov-fail-under=80 armi/tests/test_mpiFeatures.py + coverage run --rcfile=.coveragerc -m pytest -n 4 --cov=armi --cov-config=.coveragerc --cov-report=lcov --ignore=venv armi -# Second, run code coverage over the rest of the unit tests, and combine the coverage results together +# Second, run code coverage over the unit tests that run MPI library code, and combine the coverage results together. [testenv:cov2] deps= mpi4py @@ -43,7 +43,8 @@ allowlist_externals = /usr/bin/mpiexec commands = pip install -e .[memprof,mpi,test] - coverage run --rcfile=.coveragerc -m pytest -n 4 --cov=armi --cov-config=.coveragerc --cov-report=lcov --cov-append --ignore=venv armi + mpiexec -n 2 --use-hwthread-cpus coverage run --rcfile=.coveragerc -m pytest --cov=armi --cov-config=.coveragerc --cov-report=lcov --cov-append --ignore=venv armi/tests/test_mpiFeatures.py + mpiexec -n 2 --use-hwthread-cpus coverage run --rcfile=.coveragerc -m pytest --cov=armi --cov-config=.coveragerc --cov-report=lcov --cov-append --ignore=venv armi/tests/test_mpiParameters.py coverage combine --rcfile=.coveragerc --keep -a # NOTE: This only runs the MPI unit tests. @@ -56,6 +57,7 @@ allowlist_externals = commands = pip install -e .[memprof,mpi,test] mpiexec -n 2 --use-hwthread-cpus pytest armi/tests/test_mpiFeatures.py + mpiexec -n 2 --use-hwthread-cpus pytest armi/tests/test_mpiParameters.py [testenv:lint] deps= From 4a4d832c2a4312a7335974959deee37a0bd07aa6 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Fri, 8 Dec 2023 14:10:49 -0800 Subject: [PATCH 084/176] Updating runLog requirement crumbs (#1517) --- armi/materials/tests/test_materials.py | 8 ++-- armi/tests/test_runLog.py | 63 ++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/armi/materials/tests/test_materials.py b/armi/materials/tests/test_materials.py index 11ec8f291..46224c53b 100644 --- a/armi/materials/tests/test_materials.py +++ b/armi/materials/tests/test_materials.py @@ -813,7 +813,7 @@ def test_setDefaultMassFracs(self): .. test:: The materials generate nuclide mass fractions. :id: T_ARMI_MAT_FRACS0 - :tests: R_ARMI_LOG + :tests: R_ARMI_MAT_FRACS """ self.mat.setDefaultMassFracs() cur = self.mat.massFrac @@ -945,7 +945,7 @@ def test_setDefaultMassFracs(self): .. test:: The materials generate nuclide mass fractions. :id: T_ARMI_MAT_FRACS1 - :tests: R_ARMI_LOG + :tests: R_ARMI_MAT_FRACS """ self.mat.setDefaultMassFracs() cur = self.mat.pseudoDensity(500) @@ -998,7 +998,7 @@ def test_setDefaultMassFracs(self): .. test:: The materials generate nuclide mass fractions. :id: T_ARMI_MAT_FRACS2 - :tests: R_ARMI_LOG + :tests: R_ARMI_MAT_FRACS """ self.mat.setDefaultMassFracs() cur = self.mat.massFrac @@ -1035,7 +1035,7 @@ def test_setDefaultMassFracs(self): .. test:: The materials generate nuclide mass fractions. :id: T_ARMI_MAT_FRACS3 - :tests: R_ARMI_LOG + :tests: R_ARMI_MAT_FRACS """ self.mat.setDefaultMassFracs() cur = self.mat.massFrac diff --git a/armi/tests/test_runLog.py b/armi/tests/test_runLog.py index 822ffd015..c79bfd139 100644 --- a/armi/tests/test_runLog.py +++ b/armi/tests/test_runLog.py @@ -25,7 +25,12 @@ class TestRunLog(unittest.TestCase): def test_setVerbosityFromInteger(self): - """Test that the log verbosity can be set with an integer.""" + """Test that the log verbosity can be set with an integer. + + .. test:: The run log verbosity can be configured with an integer. + :id: T_ARMI_LOG0 + :tests: R_ARMI_LOG + """ log = runLog._RunLog(1) expectedStrVerbosity = "debug" verbosityRank = log.getLogVerbosityRank(expectedStrVerbosity) @@ -37,8 +42,8 @@ def test_setVerbosityFromString(self): """ Test that the log verbosity can be set with a string. - .. test:: The run log has configurable verbosity. - :id: T_ARMI_LOG0 + .. test:: The run log verbosity can be configured with a string. + :id: T_ARMI_LOG1 :tests: R_ARMI_LOG """ log = runLog._RunLog(1) @@ -105,7 +110,12 @@ def test_getWhiteSpace(self): self.assertEqual(space1, space9) def test_warningReport(self): - """A simple test of the warning tracking and reporting logic.""" + """A simple test of the warning tracking and reporting logic. + + .. test:: Generate a warning report after a simulation is complete. + :id: T_ARMI_LOG2 + :tests: R_ARMI_LOG + """ # create the logger and do some logging log = runLog.LOG = runLog._RunLog(321) log.startLog("test_warningReport") @@ -147,7 +157,12 @@ def test_warningReport(self): log.logger = backupLog def test_warningReportInvalid(self): - """A test of warningReport in an invalid situation.""" + """A test of warningReport in an invalid situation. + + .. test:: Test an important edge case for a warning report. + :id: T_ARMI_LOG3 + :tests: R_ARMI_LOG + """ # create the logger and do some logging testName = "test_warningReportInvalid" log = runLog.LOG = runLog._RunLog(323) @@ -216,7 +231,7 @@ def test_setVerbosity(self): """Let's test the setVerbosity() method carefully. .. test:: The run log has configurable verbosity. - :id: T_ARMI_LOG1 + :id: T_ARMI_LOG4 :tests: R_ARMI_LOG .. test:: The run log can log to stream. @@ -265,9 +280,15 @@ def test_setVerbosity(self): self.assertEqual(runLog.LOG.getVerbosity(), logging.WARNING) def test_setVerbosityBeforeStartLog(self): - """The user/dev may accidentally call ``setVerbosity()`` before ``startLog()``, this should be mostly supportable.""" + """The user/dev may accidentally call ``setVerbosity()`` before ``startLog()``, + this should be mostly supportable. This is just an edge case. + + .. test:: Test that we support the user setting log verbosity BEFORE the logging starts. + :id: T_ARMI_LOG5 + :tests: R_ARMI_LOG + """ with mockRunLogs.BufferLog() as mock: - # we should start with a clean slate + # we should start with a clean slate, before debug logging self.assertEqual("", mock.getStdout()) runLog.LOG.setVerbosity(logging.DEBUG) runLog.LOG.startLog("test_setVerbosityBeforeStartLog") @@ -278,6 +299,19 @@ def test_setVerbosityBeforeStartLog(self): self.assertIn("hi", mock.getStdout()) mock.emptyStdout() + # we should start with a clean slate, before info loggin + self.assertEqual("", mock.getStdout()) + runLog.LOG.setVerbosity(logging.INFO) + runLog.LOG.startLog("test_setVerbosityBeforeStartLog2") + + # we should start at info level, and that should be working correctly + self.assertEqual(runLog.LOG.getVerbosity(), logging.INFO) + runLog.debug("nope") + runLog.info("hi") + self.assertIn("hi", mock.getStdout()) + self.assertNotIn("nope", mock.getStdout()) + mock.emptyStdout() + def test_callingStartLogMultipleTimes(self): """Calling startLog() multiple times will lead to multiple output files, but logging should still work.""" with mockRunLogs.BufferLog() as mock: @@ -377,7 +411,12 @@ def test_concatenateLogs(self): self.assertFalse(os.path.exists(stderrFile)) def test_createLogDir(self): - """Test the createLogDir() method.""" + """Test the createLogDir() method. + + .. test:: Test that log directories can be created for logging output files. + :id: T_ARMI_LOG6 + :tests: R_ARMI_LOG + """ with TemporaryDirectoryChanger(): logDir = "test_createLogDir" self.assertFalse(os.path.exists(logDir)) @@ -410,6 +449,12 @@ def test_allowStopDuplicates(self): self.assertEqual(len(self.rl.filters), 1) def test_write(self): + """Test that we can write text to the logger output stream. + + .. test:: Write logging text to the logging stream and/or file. + :id: T_ARMI_LOG7 + :tests: R_ARMI_LOG + """ # divert the logging to a stream, to make testing easier stream = StringIO() handler = logging.StreamHandler(stream) From adb6cbdcdae8e0ca686c20b826ac86b5f9c432f4 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Fri, 8 Dec 2023 14:27:42 -0800 Subject: [PATCH 085/176] Improving XSGM tests (#1535) --- .../neutronics/crossSectionGroupManager.py | 9 ++++---- .../tests/test_crossSectionManager.py | 23 ++++++++++++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/armi/physics/neutronics/crossSectionGroupManager.py b/armi/physics/neutronics/crossSectionGroupManager.py index f0cb75abd..80c4f116c 100644 --- a/armi/physics/neutronics/crossSectionGroupManager.py +++ b/armi/physics/neutronics/crossSectionGroupManager.py @@ -1214,6 +1214,7 @@ def _getModifiedReprBlocks(self, blockList, originalRepresentativeBlocks): modifiedBlockXSTypes[origXSType] + origXSID[1] ) # New XS Type + Old Burnup Group origXSIDsFromNew[newXSID] = origXSID + # Create new representative blocks based on the original XS IDs for newXSID, origXSID in origXSIDsFromNew.items(): runLog.extra( @@ -1230,6 +1231,7 @@ def _getModifiedReprBlocks(self, blockList, originalRepresentativeBlocks): for b in blockList: if b.getMicroSuffix() == origXSID: b.p.xsType = newXSType + return modifiedReprBlocks, origXSIDsFromNew def getNextAvailableXsTypes(self, howMany=1, excludedXSTypes=None): @@ -1266,8 +1268,8 @@ def getNextAvailableXsTypes(self, howMany=1, excludedXSTypes=None): return availableXsTypes[:howMany] def _getUnrepresentedBlocks(self, blockCollectionsByXsGroup): - r""" - gets all blocks with suffixes not yet represented (for blocks in assemblies in the blueprints but not the core). + """ + Gets all blocks with suffixes not yet represented (for blocks in assemblies in the blueprints but not the core). Notes ----- @@ -1298,11 +1300,10 @@ def makeCrossSectionGroups(self): def _modifyUnrepresentedXSIDs(self, blockCollectionsByXsGroup): """ - adjust the xsID of blocks in the groups that are not represented. + Adjust the xsID of blocks in the groups that are not represented. Try to just adjust the burnup group up to something that is represented (can happen to structure in AA when only AB, AC, AD still remain). - """ for xsID in self._unrepresentedXSIDs: missingXsType, _missingBuGroup = xsID diff --git a/armi/physics/neutronics/tests/test_crossSectionManager.py b/armi/physics/neutronics/tests/test_crossSectionManager.py index 9527fc731..706e58d93 100644 --- a/armi/physics/neutronics/tests/test_crossSectionManager.py +++ b/armi/physics/neutronics/tests/test_crossSectionManager.py @@ -789,7 +789,7 @@ def test_getRepresentativeBlocks(self): intercoolant.setNumberDensity("NA23", units.TRACE_NUMBER_DENSITY) self.csm.createRepresentativeBlocks() - blocks = self.csm.representativeBlocks + blocks = list(self.csm.representativeBlocks.values()) self.assertGreater(len(blocks), 0) # Test ability to get average nuclide temperature in block. @@ -807,6 +807,16 @@ def test_getRepresentativeBlocks(self): # Test that retrieving temperatures fails if a representative block for a given XS ID does not exist self.assertEqual(self.csm.getNucTemperature("Z", "U235"), None) + # Test dimensions + self.assertEqual(blocks[0].getHeight(), 25.0) + self.assertEqual(blocks[1].getHeight(), 25.0) + self.assertAlmostEqual(blocks[0].getVolume(), 6074.356308731789) + self.assertAlmostEqual(blocks[1].getVolume(), 6074.356308731789) + + # Number densities haven't been calculated yet + self.assertIsNone(blocks[0].p.detailedNDens) + self.assertIsNone(blocks[1].p.detailedNDens) + def test_createRepresentativeBlocksUsingExistingBlocks(self): """ Demonstrates that a new representative block can be generated from an existing representative block. @@ -912,6 +922,17 @@ def test_interactAllCoupled(self): self.csm.interactCoupled(iteration=1) self.assertTrue(self.csm.representativeBlocks) + def test_xsgmIsRunBeforeXS(self): + """Test that the XSGM is run before the cross sections are calculated. + + .. test:: Test that the XSGM is run before the cross sections are calculated. + :id: T_ARMI_XSGM_FREQ5 + :tests: R_ARMI_XSGM_FREQ + """ + from armi.interfaces import STACK_ORDER + + self.assertLess(crossSectionGroupManager.ORDER, STACK_ORDER.CROSS_SECTIONS) + def test_copyPregeneratedFiles(self): """ Tests copying pre-generated cross section and flux files From f43d2f5965e5be331adc2cc3509feb6666e07dbe Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Fri, 8 Dec 2023 14:55:32 -0800 Subject: [PATCH 086/176] Updating Framework Concept tests and crumbs (#1520) --- armi/operators/tests/test_operators.py | 23 +++++++--- .../tests/test_crossSectionManager.py | 2 + armi/reactor/tests/test_reactors.py | 2 - armi/tests/test_interfaces.py | 7 +++ armi/tests/test_plugins.py | 44 ++++++++++++++++++- 5 files changed, 68 insertions(+), 10 deletions(-) diff --git a/armi/operators/tests/test_operators.py b/armi/operators/tests/test_operators.py index e99253b29..e389e8955 100644 --- a/armi/operators/tests/test_operators.py +++ b/armi/operators/tests/test_operators.py @@ -84,6 +84,10 @@ def test_orderedInterfaces(self, interactAll): .. test:: Interfaces are run at BOC, EOC, and at time points between. :id: T_ARMI_INTERFACE :tests: R_ARMI_INTERFACE + + .. test:: When users set the time discretization, it is enforced. + :id: T_ARMI_FW_HISTORY2 + :tests: R_ARMI_FW_HISTORY """ # an ordered list of interfaces self.assertGreater(len(self.o.interfaces), 0) @@ -91,11 +95,11 @@ def test_orderedInterfaces(self, interactAll): self.assertTrue(isinstance(i, Interface)) # make sure we only iterate one time step - self.o.cs = self.o.cs.modified(newSettings={"nCycles": 1}) + self.o.cs = self.o.cs.modified(newSettings={"nCycles": 2}) self.r.p.cycle = 1 # mock some stdout logging of what's happening when - def sideEffect(node, activeInts): + def sideEffect(node, activeInts, *args, **kwargs): print(node) print(activeInts) @@ -110,23 +114,28 @@ def sideEffect(node, activeInts): finally: sys.stdout = origout - # check the outputs + # grab the log data log = out.getvalue() - # the BOL timestep comes before the EOL - self.assertIn("BOL", log) - self.assertIn("EOL", log.split("BOL")[-1]) - # we have some common interfaces listed + + # verify we have some common interfaces listed self.assertIn("main", log) self.assertIn("fuelHandler", log) self.assertIn("fissionProducts", log) self.assertIn("history", log) self.assertIn("snapshot", log) + # At the first time step, we get one ordered list of interfaces interfaces = log.split("BOL")[1].split("EOL")[0].split(",") self.assertGreater(len(interfaces), 0) for i in interfaces: self.assertIn("Interface", i) + # verify the various time nodes are hit in order + timeNodes = ["BOL", "BOC"] + ["EveryNode"] * 3 + ["EOC", "EOL"] + for node in timeNodes: + self.assertIn(node, log) + log = node.join(log.split(node)[1:]) + def test_addInterfaceSubclassCollision(self): cs = settings.Settings() diff --git a/armi/physics/neutronics/tests/test_crossSectionManager.py b/armi/physics/neutronics/tests/test_crossSectionManager.py index 706e58d93..40be69726 100644 --- a/armi/physics/neutronics/tests/test_crossSectionManager.py +++ b/armi/physics/neutronics/tests/test_crossSectionManager.py @@ -856,6 +856,7 @@ def test_interactBOL(self): :id: T_ARMI_XSGM_FREQ0 :tests: R_ARMI_XSGM_FREQ """ + self.assertFalse(self.csm.representativeBlocks) self.blockList[0].r.p.timeNode = 0 self.csm.cs[CONF_LATTICE_PHYSICS_FREQUENCY] = "BOL" self.csm.interactBOL() @@ -868,6 +869,7 @@ def test_interactBOC(self): :id: T_ARMI_XSGM_FREQ1 :tests: R_ARMI_XSGM_FREQ """ + self.assertFalse(self.csm.representativeBlocks) self.blockList[0].r.p.timeNode = 0 self.csm.cs[CONF_LATTICE_PHYSICS_FREQUENCY] = "BOC" self.csm.interactBOL() diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 53fb0ef77..27cf69ef0 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -161,8 +161,6 @@ def loadTestReactor( o : Operator r : Reactor """ - # TODO: it would be nice to have this be more stream-oriented. Juggling files is - # devilishly difficult. global TEST_REACTOR fName = os.path.join(inputFilePath, inputFileName) customSettings = customSettings or {} diff --git a/armi/tests/test_interfaces.py b/armi/tests/test_interfaces.py index 21395c6ff..2ae280457 100644 --- a/armi/tests/test_interfaces.py +++ b/armi/tests/test_interfaces.py @@ -137,6 +137,7 @@ def test_isConverged(self): Ragged lists are easier to manage with lists as opposed to numpy.arrays, namely, their dimension is preserved. """ + # show a situation where it doesn't converge previousValues = { "float": 1.0, "list1D": [1.0, 2.0], @@ -151,6 +152,12 @@ def test_isConverged(self): self.interface.coupler.storePreviousIterationValue(previous) self.assertFalse(self.interface.coupler.isConverged(current)) + # show a situation where it DOES converge + previousValues = updatedValues + for previous, current in zip(previousValues.values(), updatedValues.values()): + self.interface.coupler.storePreviousIterationValue(previous) + self.assertTrue(self.interface.coupler.isConverged(current)) + def test_isConvergedRuntimeError(self): """Test to ensure 3D arrays do not work.""" previous = [[[1, 2, 3]], [[1, 2, 3]], [[1, 2, 3]]] diff --git a/armi/tests/test_plugins.py b/armi/tests/test_plugins.py index 029c9acb5..2ee25e866 100644 --- a/armi/tests/test_plugins.py +++ b/armi/tests/test_plugins.py @@ -21,6 +21,7 @@ from armi import context from armi import getApp +from armi import getPluginManagerOrFail from armi import interfaces from armi import plugins from armi import settings @@ -28,6 +29,7 @@ from armi.physics.neutronics import NeutronicsPlugin from armi.reactor.blocks import Block from armi.reactor.flags import Flags +from armi.reactor.tests.test_reactors import loadTestReactor, TEST_ROOT class PluginFlags1(plugins.ArmiPlugin): @@ -116,7 +118,7 @@ def test_exposeInterfaces(self): """Make sure that the exposeInterfaces hook is properly implemented. .. test:: Plugins can add interfaces to the interface stack. - :id: T_ARMI_PLUGIN_INTERFACES + :id: T_ARMI_PLUGIN_INTERFACES0 :tests: R_ARMI_PLUGIN_INTERFACES """ plugin = NeutronicsPlugin() @@ -138,6 +140,46 @@ def test_exposeInterfaces(self): self.assertTrue(issubclass(interface, interfaces.Interface)) self.assertIsInstance(kwargs, dict) + def test_pluginsExposeInterfaces(self): + """Make sure that plugins properly expose their interfaces, by checking some + known examples. + + .. test:: Check that some known plugins correctly add interfaces to the stack. + :id: T_ARMI_PLUGIN_INTERFACES1 + :tests: R_ARMI_PLUGIN_INTERFACES + """ + # generate a test operator, with a full set of interfaces from plugsin + o = loadTestReactor(TEST_ROOT)[0] + pm = getPluginManagerOrFail() + + # test the plugins were generated + plugins = pm.get_plugins() + self.assertGreater(len(plugins), 0) + + # test interfaces were generated from those plugins + ints = o.interfaces + self.assertGreater(len(ints), 0) + + # test that certain plugins exist and correctly registered their interfaces + pluginStrings = " ".join([str(p) for p in plugins]) + interfaceStrings = " ".join([str(i) for i in ints]) + + # Test that the BookkeepingPlugin registered the DatabaseInterface + self.assertIn("BookkeepingPlugin", pluginStrings) + self.assertIn("DatabaseInterface", interfaceStrings) + + # Test that the BookkeepingPlugin registered the history interface + self.assertIn("BookkeepingPlugin", pluginStrings) + self.assertIn("history", interfaceStrings) + + # Test that the EntryPointsPlugin registered the main interface + self.assertIn("EntryPointsPlugin", pluginStrings) + self.assertIn("main", interfaceStrings) + + # Test that the FuelHandlerPlugin registered the fuelHandler interface + self.assertIn("FuelHandlerPlugin", pluginStrings) + self.assertIn("fuelHandler", interfaceStrings) + class TestPlugin(unittest.TestCase): """This contains some sanity tests that can be used by implementing plugins.""" From 1d2025b3a0a19eb8ea44e364a78718d0427ca367 Mon Sep 17 00:00:00 2001 From: Tony Alberti Date: Fri, 8 Dec 2023 15:13:58 -0800 Subject: [PATCH 087/176] Updates for review on fuel handlers requirements (#1527) --- .../fuelCycle/assemblyRotationAlgorithms.py | 4 --- armi/physics/fuelCycle/fuelHandlers.py | 27 ++++++++++++---- .../tests/test_assemblyRotationAlgorithms.py | 7 +---- .../fuelCycle/tests/test_fuelHandlers.py | 31 +++++++++++++------ armi/reactor/assemblies.py | 10 +++++- armi/reactor/tests/test_assemblies.py | 7 ++++- 6 files changed, 59 insertions(+), 27 deletions(-) diff --git a/armi/physics/fuelCycle/assemblyRotationAlgorithms.py b/armi/physics/fuelCycle/assemblyRotationAlgorithms.py index 2bf3bd0d4..a56d30827 100644 --- a/armi/physics/fuelCycle/assemblyRotationAlgorithms.py +++ b/armi/physics/fuelCycle/assemblyRotationAlgorithms.py @@ -77,10 +77,6 @@ def simpleAssemblyRotation(fh): """ Rotate all pin-detail assemblies that were just shuffled by 60 degrees. - .. impl:: An assembly can be rotated about its z-axis. - :id: I_ARMI_SHUFFLE_ROTATE - :implements: R_ARMI_SHUFFLE_ROTATE - Parameters ---------- fh : FuelHandler object diff --git a/armi/physics/fuelCycle/fuelHandlers.py b/armi/physics/fuelCycle/fuelHandlers.py index 2fe524263..8e2e5483a 100644 --- a/armi/physics/fuelCycle/fuelHandlers.py +++ b/armi/physics/fuelCycle/fuelHandlers.py @@ -711,8 +711,7 @@ def _getAssembliesInRings( return assemblyList def swapAssemblies(self, a1, a2): - r""" - Moves a whole assembly from one place to another. + """Moves a whole assembly from one place to another. .. impl:: Assemblies can be moved from one place to another. :id: I_ARMI_SHUFFLE_MOVE @@ -724,11 +723,16 @@ def swapAssemblies(self, a1, a2): Parameters ---------- - a1 : Assembly + a1 : :py:class:`Assembly ` The first assembly - a2 : Assembly + a2 : :py:class:`Assembly ` The second assembly + Notes + ----- + The implementation for ``R_ARMI_SHUFFLE_STATIONARY`` occurs within + :py:meth:`` + The assembly getting swapped into the core. + outgoing : :py:class:`Assembly ` + The assembly getting discharged out the core. .. impl:: User-specified blocks can be left in place and not moved. :id: I_ARMI_SHUFFLE_STATIONARY1 :implements: R_ARMI_SHUFFLE_STATIONARY + Notes + ----- + The implementation for ``R_ARMI_SHUFFLE_STATIONARY`` occurs within + :py:meth:` Date: Fri, 8 Dec 2023 15:28:07 -0800 Subject: [PATCH 088/176] Update impl/test tags for materials (#1537) --- armi/materials/tests/test_materials.py | 21 +++++------- armi/materials/tests/test_uZr.py | 11 ++---- .../blueprints/tests/test_customIsotopics.py | 34 ++++++++++++++----- armi/utils/tests/test_densityTools.py | 5 +-- 4 files changed, 41 insertions(+), 30 deletions(-) diff --git a/armi/materials/tests/test_materials.py b/armi/materials/tests/test_materials.py index 46224c53b..abc79f455 100644 --- a/armi/materials/tests/test_materials.py +++ b/armi/materials/tests/test_materials.py @@ -165,10 +165,10 @@ def test_namespacing(self): """ # let's do a quick test of getting a material from the default namespace setMaterialNamespaceOrder(["armi.materials"]) - uo2 = materials.resolveMaterialClassByName( - "UO2", namespaceOrder=["armi.materials"] + uraniumOxide = materials.resolveMaterialClassByName( + "UraniumOxide", namespaceOrder=["armi.materials"] ) - self.assertGreater(uo2().density(500), 0) + self.assertGreater(uraniumOxide().density(500), 0) # validate the default namespace in ARMI self.__validateMaterialNamespace() @@ -178,13 +178,14 @@ def test_namespacing(self): setMaterialNamespaceOrder(["armi.materials", newMats]) self.__validateMaterialNamespace() - # show that adding a name material namespace provides access to new materials - testMatIgnoreFake = materials.resolveMaterialClassByName( - "TestMaterialIgnoreFake", namespaceOrder=["armi.materials", newMats] + # in the case of duplicate materials, show that the material namespace determines + # which material is chosen + uraniumOxideTest = materials.resolveMaterialClassByName( + "UraniumOxide", namespaceOrder=[newMats, "armi.materials"] ) for t in range(200, 600): - self.assertEqual(testMatIgnoreFake().density(t), 0) - self.assertEqual(testMatIgnoreFake().pseudoDensity(t), 0) + self.assertEqual(uraniumOxideTest().density(t), 0) + self.assertEqual(uraniumOxideTest().pseudoDensity(t), 0) # for safety, reset the material namespace list and order setMaterialNamespaceOrder(["armi.materials"]) @@ -768,10 +769,6 @@ def test_getTempChangeForDensityChange(self): def test_duplicate(self): """Test the material duplication. - .. test:: Test the material base class is usable. - :id: T_ARMI_MAT_PROPERTIES3 - :tests: R_ARMI_MAT_PROPERTIES - .. test:: Materials shall calc mass fracs at init. :id: T_ARMI_MAT_FRACS4 :tests: R_ARMI_MAT_FRACS diff --git a/armi/materials/tests/test_uZr.py b/armi/materials/tests/test_uZr.py index d684bd184..e2b4eb270 100644 --- a/armi/materials/tests/test_uZr.py +++ b/armi/materials/tests/test_uZr.py @@ -42,12 +42,7 @@ def test_isPicklable(self): ) def test_TD(self): - """Test the material theoretical density. - - .. test:: Test the material base class has temp-dependent TD curves. - :id: T_ARMI_MAT_PROPERTIES2 - :tests: R_ARMI_MAT_PROPERTIES - """ + """Test the material theoretical density.""" self.assertEqual(self.mat.getTD(), self.mat.theoreticalDensityFrac) self.mat.clearCache() @@ -89,7 +84,7 @@ def test_densityKgM3(self): """Test the density for kg/m^3. .. test:: Test the material base class has temp-dependent density. - :id: T_ARMI_MAT_PROPERTIES5 + :id: T_ARMI_MAT_PROPERTIES2 :tests: R_ARMI_MAT_PROPERTIES """ dens = self.mat.density(500) @@ -100,7 +95,7 @@ def test_pseudoDensityKgM3(self): """Test the pseudo density for kg/m^3. .. test:: Test the material base class has temp-dependent 2D density. - :id: T_ARMI_MAT_PROPERTIES6 + :id: T_ARMI_MAT_PROPERTIES3 :tests: R_ARMI_MAT_PROPERTIES """ dens = self.mat.pseudoDensity(500) diff --git a/armi/reactor/blueprints/tests/test_customIsotopics.py b/armi/reactor/blueprints/tests/test_customIsotopics.py index 6471a6388..01b6cca17 100644 --- a/armi/reactor/blueprints/tests/test_customIsotopics.py +++ b/armi/reactor/blueprints/tests/test_customIsotopics.py @@ -210,6 +210,12 @@ def test_unmodified(self): self.assertAlmostEqual(15.5, fuel.density(), 0) # i.e. it is not 19.1 def test_massFractionsAreApplied(self): + """Ensure that the custom isotopics can be specified via mass fractions. + + .. test:: Test that custom isotopics can be specified via mass fractions. + :id: T_ARMI_MAT_USER_INPUT3 + :tests: R_ARMI_MAT_USER_INPUT + """ fuel0 = self.a[0].getComponent(Flags.FUEL) fuel1 = self.a[1].getComponent(Flags.FUEL) fuel2 = self.a[2].getComponent(Flags.FUEL) @@ -223,25 +229,37 @@ def test_massFractionsAreApplied(self): ) # keys are same def test_numberFractions(self): - # fuel 2 and 3 should be the same, one is defined as mass fractions, and the other as number fractions + """Ensure that the custom isotopics can be specified via number fractions. + + .. test:: Test that custom isotopics can be specified via number fractions. + :id: T_ARMI_MAT_USER_INPUT4 + :tests: R_ARMI_MAT_USER_INPUT + """ + # fuel blocks 2 and 4 should be the same, one is defined as mass fractions, and the other as number fractions fuel2 = self.a[1].getComponent(Flags.FUEL) - fuel3 = self.a[3].getComponent(Flags.FUEL) - self.assertAlmostEqual(fuel2.density(), fuel3.density()) + fuel4 = self.a[3].getComponent(Flags.FUEL) + self.assertAlmostEqual(fuel2.density(), fuel4.density()) for nuc in fuel2.p.numberDensities.keys(): self.assertAlmostEqual( - fuel2.p.numberDensities[nuc], fuel3.p.numberDensities[nuc] + fuel2.p.numberDensities[nuc], fuel4.p.numberDensities[nuc] ) def test_numberDensities(self): - # fuel 2 and 3 should be the same, one is defined as mass fractions, and the other as number fractions + """Ensure that the custom isotopics can be specified via number densities. + + .. test:: Test that custom isotopics can be specified via number fractions. + :id: T_ARMI_MAT_USER_INPUT5 + :tests: R_ARMI_MAT_USER_INPUT + """ + # fuel blocks 2 and 5 should be the same, one is defined as mass fractions, and the other as number densities fuel2 = self.a[1].getComponent(Flags.FUEL) - fuel3 = self.a[4].getComponent(Flags.FUEL) - self.assertAlmostEqual(fuel2.density(), fuel3.density()) + fuel5 = self.a[4].getComponent(Flags.FUEL) + self.assertAlmostEqual(fuel2.density(), fuel5.density()) for nuc in fuel2.p.numberDensities.keys(): self.assertAlmostEqual( - fuel2.p.numberDensities[nuc], fuel3.p.numberDensities[nuc] + fuel2.p.numberDensities[nuc], fuel5.p.numberDensities[nuc] ) def test_numberDensitiesAnchor(self): diff --git a/armi/utils/tests/test_densityTools.py b/armi/utils/tests/test_densityTools.py index 1f823a109..557cdb2bc 100644 --- a/armi/utils/tests/test_densityTools.py +++ b/armi/utils/tests/test_densityTools.py @@ -20,10 +20,11 @@ from armi.utils import densityTools -class TestMaterialIgnoreFake(Material): +class UraniumOxide(Material): """A test material that needs to be stored in a different namespace. - For tests in: armi.materials.tests.test_materials.py + This is a duplicate (by name only) of :py:class:`armi.materials.uraniumOxide.UraniumOxide` + and is used for testing in :py:meth:`armi.materials.tests.test_materials.MaterialFindingTests.test_namespacing` """ def pseudoDensity(self, Tk=None, Tc=None): From 7e89b41113a84c9e0d584499d30841e24bd32ec0 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Fri, 8 Dec 2023 15:58:43 -0800 Subject: [PATCH 089/176] Allowing a test to skip if it is being run outside a git repo (#1539) --- armi/bookkeeping/db/tests/test_database3.py | 24 ++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/armi/bookkeeping/db/tests/test_database3.py b/armi/bookkeeping/db/tests/test_database3.py index 7ca30ca3f..d7c4eca7d 100644 --- a/armi/bookkeeping/db/tests/test_database3.py +++ b/armi/bookkeeping/db/tests/test_database3.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Tests for the Database3 class.""" +from distutils.spawn import find_executable import subprocess import unittest @@ -28,6 +29,13 @@ from armi.utils import getPreviousTimeNode from armi.utils.directoryChangers import TemporaryDirectoryChanger +# determine if this is a parallel run, and git is installed +GIT_EXE = None +if find_executable("git") is not None: + GIT_EXE = "git" +elif find_executable("git.exe") is not None: + GIT_EXE = "git.exe" + class TestDatabase3(unittest.TestCase): """Tests for the Database3 class.""" @@ -553,6 +561,7 @@ def test_splitDatabase(self): [(c, n) for c in (0, 1) for n in range(2)], "-all-iterations" ) + @unittest.skipIf(GIT_EXE is None, "This test needs Git.") def test_grabLocalCommitHash(self): """Test of static method to grab a local commit hash with ARMI version.""" # 1. test outside a Git repo @@ -560,11 +569,16 @@ def test_grabLocalCommitHash(self): self.assertEqual(localHash, "unknown") # 2. test inside an empty git repo - code = subprocess.run( - ["git", "init", "."], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ).returncode + try: + code = subprocess.run( + ["git", "init", "."], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ).returncode + except FileNotFoundError: + print("Skipping this test because it is being run outside a git repo.") + return + self.assertEqual(code, 0) localHash = database3.Database3.grabLocalCommitHash() self.assertEqual(localHash, "unknown") From 158723d88835f6add541151986051bf71e73a8c8 Mon Sep 17 00:00:00 2001 From: Chris Keckler Date: Mon, 11 Dec 2023 10:11:45 -0600 Subject: [PATCH 090/176] Updating docstring of getAssemblyWithStringLocation (#1530) --- armi/reactor/reactors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index 8e9824bca..117034bd4 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -1653,7 +1653,7 @@ def getAssemblyWithAssemNum(self, assemNum): return self.getAssembly(assemNum=assemNum) def getAssemblyWithStringLocation(self, locationString): - """Returns an assembly or none if given a location string like 'B0014'. + """Returns an assembly or none if given a location string like '001-001'. .. impl:: Get assembly by location. :id: I_ARMI_R_GET_ASSEM_LOC From 02af3a7f0d0eac874eecd949cd2b418fa43d6bd7 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Mon, 11 Dec 2023 10:39:56 -0800 Subject: [PATCH 091/176] Adding test crumb to settings requirement (#1541) --- armi/nucDirectory/nucDir.py | 22 +++++++++++----------- armi/settings/settingsIO.py | 1 - armi/settings/tests/test_settingsIO.py | 11 ++++++++--- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/armi/nucDirectory/nucDir.py b/armi/nucDirectory/nucDir.py index ffbaea42d..1a5ec7ca5 100644 --- a/armi/nucDirectory/nucDir.py +++ b/armi/nucDirectory/nucDir.py @@ -66,7 +66,7 @@ def getNuclideFromName(name): def getNaturalIsotopics(elementSymbol=None, z=None): - r""" + """ Determines the atom fractions of all natural isotopes. Parameters @@ -90,7 +90,8 @@ def getNaturalIsotopics(elementSymbol=None, z=None): def getNaturalMassIsotopics(elementSymbol=None, z=None): - r"""Return mass fractions of all natural isotopes. + """Return mass fractions of all natural isotopes. + To convert number fractions to mass fractions, we multiply by A. """ numIso = getNaturalIsotopics(elementSymbol, z) @@ -107,7 +108,7 @@ def getNaturalMassIsotopics(elementSymbol=None, z=None): def getMc2Label(name): - r""" + """ Return a MC2 prefix label without a xstype suffix. MC**2 has labels and library names. The labels are like @@ -146,7 +147,7 @@ def getMc2Label(name): def getElementName(z=None, symbol=None): - r""" + """ Returns element name. Parameters @@ -173,7 +174,7 @@ def getElementName(z=None, symbol=None): def getElementSymbol(z=None, name=None): - r""" + """ Returns element abbreviation given atomic number Z. Parameters @@ -200,7 +201,7 @@ def getElementSymbol(z=None, name=None): def getNuclide(nucName): - r""" + """ Looks up the ARMI nuclide object that has this name. Parameters @@ -212,7 +213,6 @@ def getNuclide(nucName): ------- nuc : Nuclide An armi nuclide object. - """ nuc = nuclideBases.byName.get(nucName, None) if nucName and not nuc: @@ -223,7 +223,7 @@ def getNuclide(nucName): def getNuclides(nucName=None, elementSymbol=None): - r""" + """ Returns a list of nuclide names in a particular nuclide or element. If no arguments, returns all nuclideBases in the directory @@ -250,7 +250,7 @@ def getNuclides(nucName=None, elementSymbol=None): def getNuclideNames(nucName=None, elementSymbol=None): - r""" + """ Returns a list of nuclide names in a particular nuclide or element. If no arguments, returns all nuclideBases in the directory. @@ -269,7 +269,7 @@ def getNuclideNames(nucName=None, elementSymbol=None): def getAtomicWeight(lab=None, z=None, a=None): - r""" + """ Returns atomic weight in g/mole. Parameters @@ -339,7 +339,7 @@ def isFissile(name): def getThresholdDisplacementEnergy(nuc): - r""" + """ Return the Lindhard cutoff; the energy required to displace an atom. From SPECTER.pdf Table II diff --git a/armi/settings/settingsIO.py b/armi/settings/settingsIO.py index 1da66385b..9fb02eab1 100644 --- a/armi/settings/settingsIO.py +++ b/armi/settings/settingsIO.py @@ -284,7 +284,6 @@ class SettingsWriter: preserves all settings originally in file even if they match the default value full all setting values regardless of default status - """ def __init__(self, settings_instance, style="short", settingsSetByUser=[]): diff --git a/armi/settings/tests/test_settingsIO.py b/armi/settings/tests/test_settingsIO.py index 816eaa88a..a2c1185cc 100644 --- a/armi/settings/tests/test_settingsIO.py +++ b/armi/settings/tests/test_settingsIO.py @@ -72,8 +72,8 @@ def test_basicSettingsReader(self): def test_readFromFile(self): """Read settings from a (human-readable) YAML file. - .. test:: The setting file is in a human-readable plain text file. - :id: T_ARMI_SETTINGS_IO_TXT + .. test:: Settings can be input from a human-readable text file. + :id: T_ARMI_SETTINGS_IO_TXT0 :tests: R_ARMI_SETTINGS_IO_TXT """ with directoryChangers.TemporaryDirectoryChanger(): @@ -166,7 +166,12 @@ def test_writeMedium(self): self.assertIn("numProcessors: 1", txt) def test_writeFull(self): - """Setting output as a full, all defaults included file.""" + """Setting output as a full, all defaults included file. + + .. test:: Settings can be output to a human-readable text file. + :id: T_ARMI_SETTINGS_IO_TXT1 + :tests: R_ARMI_SETTINGS_IO_TXT + """ self.cs.writeToYamlFile(self.filepathYaml, style="full") txt = open(self.filepathYaml, "r").read() self.assertIn("nCycles: 55", txt) From 60731eb54fa8f527241932a6aee68447dc71ff76 Mon Sep 17 00:00:00 2001 From: Tony Alberti Date: Tue, 12 Dec 2023 08:56:43 -0800 Subject: [PATCH 092/176] Some updates to testing for parameters tests (#1543) --- armi/reactor/tests/test_parameters.py | 2 +- armi/reactor/tests/test_reactors.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/armi/reactor/tests/test_parameters.py b/armi/reactor/tests/test_parameters.py index 16bcb7f28..18e0668c9 100644 --- a/armi/reactor/tests/test_parameters.py +++ b/armi/reactor/tests/test_parameters.py @@ -253,7 +253,7 @@ def nPlus1(self, value): mock.nPlus1 = 22 self.assertEqual(21, mock.n) self.assertEqual(22, mock.nPlus1) - self.assertTrue(all(pd.assigned for pd in mock.paramDefs)) + self.assertTrue(all(pd.assigned != parameters.NEVER for pd in mock.paramDefs)) def test_setterGetterBasics(self): """Test the Parameter setter/getter tooling, through the lifecycle of a Parameter being updated. diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 27cf69ef0..765eacdb7 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -300,6 +300,10 @@ def test_getSetParameters(self): :id: T_ARMI_SETTINGS_POWER :tests: R_ARMI_SETTINGS_POWER """ + # Test at reactor level + self.assertEqual(self.r.p.cycle, 0) + self.assertEqual(self.r.p.availabilityFactor, 1.0) + # Test at core level core = self.r.core self.assertGreater(core.p.power, -1) @@ -321,6 +325,10 @@ def test_getSetParameters(self): block.p.THTfuelCL = 57 self.assertEqual(block.p.THTfuelCL, 57) + # Test at component level + component = block[0] + self.assertEqual(component.p.temperatureInC, 450.0) + def test_sortChildren(self): self.assertEqual(next(self.r.core.__iter__()), self.r.core[0]) self.assertEqual(self.r.core._children, sorted(self.r.core._children)) From 42174942f7364b98a8f16b014baab2b337299438 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 12 Dec 2023 09:21:12 -0800 Subject: [PATCH 093/176] Improving tests for composites (#1540) --- armi/reactor/composites.py | 6 ++- armi/reactor/tests/test_blocks.py | 15 +++--- armi/reactor/tests/test_composites.py | 76 +++++++++++++++++++++++---- armi/reactor/tests/test_reactors.py | 4 -- 4 files changed, 81 insertions(+), 20 deletions(-) diff --git a/armi/reactor/composites.py b/armi/reactor/composites.py index ead79f514..84f3d42f1 100644 --- a/armi/reactor/composites.py +++ b/armi/reactor/composites.py @@ -673,7 +673,7 @@ def hasFlags(self, typeID: TypeSpec, exact=False): Determine if this object is of a certain type. .. impl:: Composites have queriable flags. - :id: I_ARMI_CMP_FLAG + :id: I_ARMI_CMP_FLAG0 :implements: R_ARMI_CMP_FLAG Parameters @@ -767,6 +767,10 @@ def setType(self, typ, flags: Optional[Flags] = None): """ Set the object type. + .. impl:: Composites have modifiable flags. + :id: I_ARMI_CMP_FLAG1 + :implements: R_ARMI_CMP_FLAG + Parameters ---------- typ : str diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py index d0a18eaa6..257a2d400 100644 --- a/armi/reactor/tests/test_blocks.py +++ b/armi/reactor/tests/test_blocks.py @@ -1248,9 +1248,11 @@ def test_getComponentByName(self): ) self.assertIsNotNone(self.block.getComponentByName("annular void")) - def test_getSortedComponentsInsideOfComponent(self): + def test_getSortedComponentsInsideOfComponentClad(self): """Test that components can be sorted within a block and returned in the correct order. + For an arbitrary example: a clad component. + .. test:: Get children by name. :id: T_ARMI_CMP_BY_NAME1 :tests: R_ARMI_CMP_BY_NAME @@ -1272,12 +1274,10 @@ def test_getSortedComponentsInsideOfComponent(self): actual = self.block.getSortedComponentsInsideOfComponent(clad) self.assertListEqual(actual, expected) - def test_getSortedComponentsInsideOfComponentSpecifiedTypes(self): + def test_getSortedComponentsInsideOfComponentDuct(self): """Test that components can be sorted within a block and returned in the correct order. - .. test:: Get children by name. - :id: T_ARMI_CMP_BY_NAME2 - :tests: R_ARMI_CMP_BY_NAME + For an arbitrary example: a duct. """ expected = [ self.block.getComponentByName(c) @@ -1290,9 +1290,12 @@ def test_getSortedComponentsInsideOfComponentSpecifiedTypes(self): "gap2", "outer liner", "gap3", + "clad", + "wire", + "coolant", ] ] - clad = self.block.getComponent(Flags.CLAD) + clad = self.block.getComponent(Flags.DUCT) actual = self.block.getSortedComponentsInsideOfComponent(clad) self.assertListEqual(actual, expected) diff --git a/armi/reactor/tests/test_composites.py b/armi/reactor/tests/test_composites.py index 26fccc9a8..2d9308623 100644 --- a/armi/reactor/tests/test_composites.py +++ b/armi/reactor/tests/test_composites.py @@ -113,10 +113,6 @@ def setUp(self): def test_composite(self): """Test basic Composite things. - .. test:: Composites are a physical part of the reactor. - :id: T_ARMI_CMP0 - :tests: R_ARMI_CMP - .. test:: Composites are part of a hierarchical model. :id: T_ARMI_CMP_CHILDREN0 :tests: R_ARMI_CMP_CHILDREN @@ -136,10 +132,6 @@ def test_iterComponents(self): def test_getChildren(self): """Test the get children method. - .. test:: Composites are a physical part of the reactor. - :id: T_ARMI_CMP1 - :tests: R_ARMI_CMP - .. test:: Composites are part of a hierarchical model. :id: T_ARMI_CMP_CHILDREN1 :tests: R_ARMI_CMP_CHILDREN @@ -680,24 +672,90 @@ def test_setMass(self): group.setMass("U235", 5) self.assertAlmostEqual(group.getMass("U235"), 5) + # ad a second block, and confirm it works + group.add(loadTestBlock()) + self.assertGreater(group.getMass("U235"), 5) + self.assertAlmostEqual(group.getMass("U235"), 1364.28376185) + def test_getNumberDensities(self): """Get number densities from composite. .. test:: Number density of composite is retrievable. :id: T_ARMI_CMP_GET_NDENS0 :tests: R_ARMI_CMP_GET_NDENS + """ + # verify the number densities from the composite + ndens = self.obj.getNumberDensities() + self.assertAlmostEqual(0.0001096, ndens["SI"], 7) + self.assertAlmostEqual(0.0000368, ndens["W"], 7) + + ndens = self.obj.getNumberDensity("SI") + self.assertAlmostEqual(0.0001096, ndens, 7) + + # sum nuc densities from children components + totalVolume = self.obj.getVolume() + childDensities = {} + for o in self.obj.getChildren(): + m = o.getVolume() + d = o.getNumberDensities() + for nuc, val in d.items(): + if nuc not in childDensities: + childDensities[nuc] = val * (m / totalVolume) + else: + childDensities[nuc] += val * (m / totalVolume) + + # verify the children match this composite + for nuc in ["FE", "SI"]: + self.assertAlmostEqual( + self.obj.getNumberDensity(nuc), childDensities[nuc], 4, msg=nuc + ) + + def test_getNumberDensitiesWithExpandedFissionProducts(self): + """Get number densities from composite. .. test:: Get number densities. :id: T_ARMI_CMP_NUC :tests: R_ARMI_CMP_NUC """ - ndens = self.obj.getNumberDensities() + # verify the number densities from the composite + ndens = self.obj.getNumberDensities(expandFissionProducts=True) self.assertAlmostEqual(0.0001096, ndens["SI"], 7) self.assertAlmostEqual(0.0000368, ndens["W"], 7) ndens = self.obj.getNumberDensity("SI") self.assertAlmostEqual(0.0001096, ndens, 7) + # set the lumped fission product mapping + fpd = getDummyLFPFile() + lfps = fpd.createLFPsFromFile() + self.obj.setLumpedFissionProducts(lfps) + + # sum nuc densities from children components + totalVolume = self.obj.getVolume() + childDensities = {} + for o in self.obj.getChildren(): + # get the number densities with and without fission products + d0 = o.getNumberDensities(expandFissionProducts=False) + d = o.getNumberDensities(expandFissionProducts=True) + + # prove that the expanded fission products have more isotopes + if len(d0) > 0: + self.assertGreater(len(d), len(d0)) + + # sum the child nuclide densites (weighted by mass fraction) + m = o.getVolume() + for nuc, val in d.items(): + if nuc not in childDensities: + childDensities[nuc] = val * (m / totalVolume) + else: + childDensities[nuc] += val * (m / totalVolume) + + # verify the children match this composite + for nuc in ["FE", "SI"]: + self.assertAlmostEqual( + self.obj.getNumberDensity(nuc), childDensities[nuc], 4, msg=nuc + ) + def test_dimensionReport(self): report = self.obj.setComponentDimensionsReport() self.assertEqual(len(report), len(self.obj)) diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 765eacdb7..d6f9960bd 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -246,10 +246,6 @@ def test_coreSfp(self): .. test:: The reactor object includes a core and an SFP. :id: T_ARMI_R_CHILDREN1 :tests: R_ARMI_R_CHILDREN - - .. test:: Components are a physical part of the reactor. - :id: T_ARMI_CMP2 - :tests: R_ARMI_CMP """ self.assertTrue(isinstance(self.r.core, reactors.Core)) self.assertTrue(isinstance(self.r.sfp, SpentFuelPool)) From 38a3f381a8dfe14da2515aed08690e06ebb7c934 Mon Sep 17 00:00:00 2001 From: Tony Alberti Date: Tue, 12 Dec 2023 09:41:24 -0800 Subject: [PATCH 094/176] add a test crumb for a param req (#1544) --- armi/reactor/tests/test_parameters.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/armi/reactor/tests/test_parameters.py b/armi/reactor/tests/test_parameters.py index 18e0668c9..e2068e8ae 100644 --- a/armi/reactor/tests/test_parameters.py +++ b/armi/reactor/tests/test_parameters.py @@ -73,6 +73,10 @@ def test_writeSomeParamsToDB(self): .. test:: Restrict parameters from DB write. :id: T_ARMI_PARAM_DB :tests: R_ARMI_PARAM_DB + + .. test:: Ensure that new parameters can be defined. + :id: T_ARMI_PARAM + :tests: R_ARMI_PARAM """ pDefs = parameters.ParameterDefinitionCollection() with pDefs.createBuilder() as pb: From 1bc3b6f22a34a07e71352bd62113ff2f921172ee Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:13:43 -0800 Subject: [PATCH 095/176] Improving testing and crumbs for blueprints (#1545) --- armi/reactor/blueprints/gridBlueprint.py | 12 ++-- .../blueprints/tests/test_blueprints.py | 60 +++++++++++++++++++ .../blueprints/tests/test_gridBlueprints.py | 8 +-- 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/armi/reactor/blueprints/gridBlueprint.py b/armi/reactor/blueprints/gridBlueprint.py index d2a12932d..22882c3c0 100644 --- a/armi/reactor/blueprints/gridBlueprint.py +++ b/armi/reactor/blueprints/gridBlueprint.py @@ -144,6 +144,10 @@ class GridBlueprint(yamlize.Object): The grids get origins either from a parent block (for pin lattices) or from a System (for Cores, SFPs, and other components). + .. impl:: Define a lattice map in reactor core. + :id: I_ARMI_BP_GRID + :implements: R_ARMI_BP_GRID + Attributes ---------- name : str @@ -163,7 +167,6 @@ class GridBlueprint(yamlize.Object): gridContents : dict A {(i,j): str} dictionary mapping spatialGrid indices in 2-D to string specifiers of what's supposed to be in the grid. - """ name = yamlize.Attribute(key="name", type=str) @@ -246,12 +249,7 @@ def readFromLatticeMap(self, value): self._readFromLatticeMap = value def construct(self): - """Build a Grid from a grid definition. - - .. impl:: Define a lattice map in reactor core. - :id: I_ARMI_BP_GRID - :implements: R_ARMI_BP_GRID - """ + """Build a Grid from a grid definition.""" self._readGridContents() grid = self._constructSpatialGrid() return grid diff --git a/armi/reactor/blueprints/tests/test_blueprints.py b/armi/reactor/blueprints/tests/test_blueprints.py index a96fceadd..596816371 100644 --- a/armi/reactor/blueprints/tests/test_blueprints.py +++ b/armi/reactor/blueprints/tests/test_blueprints.py @@ -13,6 +13,7 @@ # limitations under the License. """Tests the blueprints (loading input) file.""" +import io import os import pathlib import unittest @@ -30,6 +31,7 @@ from armi.tests import TEST_ROOT from armi.utils import directoryChangers from armi.utils import textProcessors +from armi.reactor.blueprints.gridBlueprint import saveToStream class TestBlueprints(unittest.TestCase): @@ -64,6 +66,64 @@ def setUpClass(cls): def tearDownClass(cls): cls.directoryChanger.close() + @staticmethod + def __stubify(latticeMap): + """Little helper method to allow lattie maps to be compared free of whitespace.""" + return latticeMap.replace(" ", "").replace("-", "").replace("\n", "") + + def test_roundTripCompleteBP(self): + """Test the round-tip of reading and writing blueprint files. + + .. test:: Validates the round trip of reading and writing blueprints. + :id: T_ARMI_BP_TO_DB1 + :tests: R_ARMI_BP_TO_DB + """ + # the correct lattice map + latticeMap = """- - SH + - SH SH +- SH OC SH + SH OC OC SH + OC IC OC SH + OC IC IC OC SH + IC IC IC OC SH + IC IC PC OC SH + IC PC IC IC OC SH + LA IC IC IC OC + IC IC IC IC SH + IC LB IC IC OC + IC IC PC IC SH + LA IC IC OC + IC IC IC IC SH + IC IC IC OC + IC IC IC PC SH""" + latticeMap = self.__stubify(latticeMap) + + # validate some core elements from the blueprints + self.assertEqual(self.blueprints.gridDesigns["core"].symmetry, "third periodic") + map0 = self.__stubify(self.blueprints.gridDesigns["core"].latticeMap) + self.assertEqual(map0, latticeMap) + + # save the blueprint to a stream + stream = io.StringIO() + stream.seek(0) + self.blueprints.dump(self.blueprints) + saveToStream(stream, self.blueprints, True, True) + stream.seek(0) + + with directoryChangers.TemporaryDirectoryChanger(): + # save the stream to a file + filePath = "test_roundTripCompleteBP.yaml" + with open(filePath, "w") as fout: + fout.write(stream.read()) + + # load the blueprint from that file again + bp = blueprints.Blueprints.load(open(filePath, "r").read()) + + # re-validate some core elements from the blueprints + self.assertEqual(bp.gridDesigns["core"].symmetry, "third periodic") + map1 = self.__stubify(bp.gridDesigns["core"].latticeMap) + self.assertEqual(map1, latticeMap) + def test_nuclides(self): """Tests the available sets of nuclides work as expected.""" actives = set(self.blueprints.activeNuclides) diff --git a/armi/reactor/blueprints/tests/test_gridBlueprints.py b/armi/reactor/blueprints/tests/test_gridBlueprints.py index df12aabac..9fdf1e3a1 100644 --- a/armi/reactor/blueprints/tests/test_gridBlueprints.py +++ b/armi/reactor/blueprints/tests/test_gridBlueprints.py @@ -305,7 +305,7 @@ """ -class TestRoundTrip(unittest.TestCase): +class TestGridBPRoundTrip(unittest.TestCase): def setUp(self): self.grids = Grids.load(SMALL_HEX) @@ -316,8 +316,8 @@ def test_roundTrip(self): """ Test saving blueprint data to a stream. - .. test:: Blueprints settings can be written to disk. - :id: T_ARMI_BP_TO_DB + .. test:: Grid blueprints can be written to disk. + :id: T_ARMI_BP_TO_DB0 :tests: R_ARMI_BP_TO_DB """ stream = io.StringIO() @@ -326,7 +326,7 @@ def test_roundTrip(self): gridBp = Grids.load(stream) self.assertIn("third", gridBp["core"].symmetry) - def test_tiny_map(self): + def test_tinyMap(self): """ Test that a lattice map can be defined, written, and read in from blueprint file. From 2bbd3002ad607d1bf73a5fd567abab91371177c9 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 12 Dec 2023 15:52:28 -0800 Subject: [PATCH 096/176] Adding tests and test crumbs for settings and bookkeeping (#1542) --- armi/bookkeeping/db/database3.py | 33 ++++++---- .../db/tests/test_databaseInterface.py | 48 +++++++++++++-- armi/bookkeeping/historyTracker.py | 5 +- armi/bookkeeping/tests/test_historyTracker.py | 61 ++++++++++++++++++- armi/operators/tests/test_operators.py | 6 ++ armi/reactor/tests/test_reactors.py | 2 +- armi/settings/tests/test_settings.py | 27 ++++++++ 7 files changed, 158 insertions(+), 24 deletions(-) diff --git a/armi/bookkeeping/db/database3.py b/armi/bookkeeping/db/database3.py index 3b9a30767..189896a21 100644 --- a/armi/bookkeeping/db/database3.py +++ b/armi/bookkeeping/db/database3.py @@ -201,23 +201,14 @@ def open(self): "Cannot open database with permission `{}`".format(self._permission) ) + # open the database, and write a bunch of metadata to it runLog.info("Opening database file at {}".format(os.path.abspath(filePath))) self.h5db = h5py.File(filePath, self._permission) self.h5db.attrs["successfulCompletion"] = False self.h5db.attrs["version"] = meta.__version__ self.h5db.attrs["databaseVersion"] = self.version - self.h5db.attrs["user"] = context.USER - self.h5db.attrs["python"] = sys.version - self.h5db.attrs["armiLocation"] = os.path.dirname(context.ROOT) - self.h5db.attrs["startTime"] = context.START_TIME - self.h5db.attrs["machines"] = numpy.array(context.MPI_NODENAMES).astype("S") - # store platform data - platform_data = uname() - self.h5db.attrs["platform"] = platform_data.system - self.h5db.attrs["hostname"] = platform_data.node - self.h5db.attrs["platformRelease"] = platform_data.release - self.h5db.attrs["platformVersion"] = platform_data.version - self.h5db.attrs["platformArch"] = platform_data.processor + self.writeSystemAttributes(self.h5db) + # store app and plugin data app = getApp() self.h5db.attrs["appName"] = app.name @@ -228,9 +219,25 @@ def open(self): ] ps = numpy.array([str(p[0]) + ":" + str(p[1]) for p in ps]).astype("S") self.h5db.attrs["pluginPaths"] = ps - # store the commit hash of the local repo self.h5db.attrs["localCommitHash"] = Database3.grabLocalCommitHash() + @staticmethod + def writeSystemAttributes(h5db): + """Write system attributes to the database.""" + h5db.attrs["user"] = context.USER + h5db.attrs["python"] = sys.version + h5db.attrs["armiLocation"] = os.path.dirname(context.ROOT) + h5db.attrs["startTime"] = context.START_TIME + h5db.attrs["machines"] = numpy.array(context.MPI_NODENAMES).astype("S") + + # store platform data + platform_data = uname() + h5db.attrs["platform"] = platform_data.system + h5db.attrs["hostname"] = platform_data.node + h5db.attrs["platformRelease"] = platform_data.release + h5db.attrs["platformVersion"] = platform_data.version + h5db.attrs["platformArch"] = platform_data.processor + @staticmethod def grabLocalCommitHash(): """ diff --git a/armi/bookkeeping/db/tests/test_databaseInterface.py b/armi/bookkeeping/db/tests/test_databaseInterface.py index 860d32362..17ae0ecdb 100644 --- a/armi/bookkeeping/db/tests/test_databaseInterface.py +++ b/armi/bookkeeping/db/tests/test_databaseInterface.py @@ -135,13 +135,41 @@ def setUp(self): def tearDown(self): self.td.__exit__(None, None, None) + def test_writeSystemAttributes(self): + """Test the writeSystemAttributes method. + + .. test:: Validate that we can directly write system attributes to a database file. + :id: T_ARMI_DB_QA0 + :tests: R_ARMI_DB_QA + """ + with h5py.File("test_writeSystemAttributes.h5", "w") as h5: + Database3.writeSystemAttributes(h5) + + with h5py.File("test_writeSystemAttributes.h5", "r") as h5: + self.assertIn("user", h5.attrs) + self.assertIn("python", h5.attrs) + self.assertIn("armiLocation", h5.attrs) + self.assertIn("startTime", h5.attrs) + self.assertIn("machines", h5.attrs) + self.assertIn("platform", h5.attrs) + self.assertIn("hostname", h5.attrs) + self.assertIn("platformRelease", h5.attrs) + self.assertIn("platformVersion", h5.attrs) + self.assertIn("platformArch", h5.attrs) + def test_metaData_endSuccessfully(self): - def goodMethod(cycle, node): - pass + """Test databases have the correct metadata in them. + .. test:: Validate that databases have system attributes written to them during the usual workflow. + :id: T_ARMI_DB_QA1 + :tests: R_ARMI_DB_QA + """ # the power should start at zero self.assertEqual(self.r.core.p.power, 0) + def goodMethod(cycle, node): + pass + self.o.interfaces.append(MockInterface(self.o.r, self.o.cs, goodMethod)) with self.o: self.o.operate() @@ -152,15 +180,23 @@ def goodMethod(cycle, node): with h5py.File(self.o.cs.caseTitle + ".h5", "r") as h5: self.assertTrue(h5.attrs["successfulCompletion"]) self.assertEqual(h5.attrs["version"], version) + + self.assertIn("caseTitle", h5.attrs) + self.assertIn("geomFile", h5["inputs"]) + self.assertIn("settings", h5["inputs"]) + self.assertIn("blueprints", h5["inputs"]) + + # validate system attributes self.assertIn("user", h5.attrs) self.assertIn("python", h5.attrs) self.assertIn("armiLocation", h5.attrs) self.assertIn("startTime", h5.attrs) self.assertIn("machines", h5.attrs) - self.assertIn("caseTitle", h5.attrs) - self.assertIn("geomFile", h5["inputs"]) - self.assertIn("settings", h5["inputs"]) - self.assertIn("blueprints", h5["inputs"]) + self.assertIn("platform", h5.attrs) + self.assertIn("hostname", h5.attrs) + self.assertIn("platformRelease", h5.attrs) + self.assertIn("platformVersion", h5.attrs) + self.assertIn("platformArch", h5.attrs) # after operating, the power will be greater than zero self.assertGreater(self.r.core.p.power, 1e9) diff --git a/armi/bookkeeping/historyTracker.py b/armi/bookkeeping/historyTracker.py index 766b1e444..ff9193990 100644 --- a/armi/bookkeeping/historyTracker.py +++ b/armi/bookkeeping/historyTracker.py @@ -95,6 +95,10 @@ class HistoryTrackerInterface(interfaces.Interface): """ Makes reports of the state that individual assemblies encounter. + .. impl:: This interface allows users to retrieve run data from somewhere other than the database. + :id: I_ARMI_HIST_TRACK + :implements: R_ARMI_HIST_TRACK + Attributes ---------- detailAssemblyNames : list @@ -102,7 +106,6 @@ class HistoryTrackerInterface(interfaces.Interface): time : list list of reactor time in years - """ name = "history" diff --git a/armi/bookkeeping/tests/test_historyTracker.py b/armi/bookkeeping/tests/test_historyTracker.py index 8a2bbb0a2..a75beb943 100644 --- a/armi/bookkeeping/tests/test_historyTracker.py +++ b/armi/bookkeeping/tests/test_historyTracker.py @@ -107,6 +107,10 @@ def test_calcMGFluence(self): armi.bookeeping.db.hdf.hdfDB.readBlocksHistory requires historical_values[historical_indices] to be cast as a list to read more than the first energy group. This test shows that this behavior is preserved. + + .. test:: Demonstrate that a parameter stored at differing time nodes can be recovered. + :id: T_ARMI_HIST_TRACK0 + :tests: R_ARMI_HIST_TRACK """ o = self.o b = o.r.core.childrenByLocator[o.r.core.spatialGrid[0, 0, 0]].getFirstBlock( @@ -115,9 +119,8 @@ def test_calcMGFluence(self): bVolume = b.getVolume() bName = b.name - hti = o.getInterface("history") - # duration is None in this DB + hti = o.getInterface("history") timesInYears = [duration or 1.0 for duration in hti.getTimeSteps()] timeStepsToRead = [ utils.getCycleNodeFromCumulativeNode(i, self.o.cs) @@ -128,7 +131,7 @@ def test_calcMGFluence(self): mgFluence = None for ts, years in enumerate(timesInYears): cycle, node = utils.getCycleNodeFromCumulativeNode(ts, self.o.cs) - # b.p.mgFlux is vol integrated + # b.p.mgFlux is vol integrated mgFlux = hti.getBlockHistoryVal(bName, "mgFlux", (cycle, node)) / bVolume timeInSec = years * 365 * 24 * 3600 if mgFluence is None: @@ -143,6 +146,58 @@ def test_calcMGFluence(self): hti.unloadBlockHistoryVals() self.assertIsNone(hti._preloadedBlockHistory) + def test_historyParameters(self): + """Retrieve various paramaters from the history. + + .. test:: Demonstrate that various parameters stored at differing time nodes can be recovered. + :id: T_ARMI_HIST_TRACK1 + :tests: R_ARMI_HIST_TRACK + """ + o = self.o + b = o.r.core.childrenByLocator[o.r.core.spatialGrid[0, 0, 0]].getFirstBlock( + Flags.FUEL + ) + b.getVolume() + bName = b.name + + # duration is None in this DB + hti = o.getInterface("history") + timesInYears = [duration or 1.0 for duration in hti.getTimeSteps()] + timeStepsToRead = [ + utils.getCycleNodeFromCumulativeNode(i, self.o.cs) + for i in range(len(timesInYears)) + ] + hti.preloadBlockHistoryVals([bName], ["power"], timeStepsToRead) + + # read some parameters + params = {} + for param in ["height", "pdens", "power"]: + params[param] = [] + for ts, years in enumerate(timesInYears): + cycle, node = utils.getCycleNodeFromCumulativeNode(ts, self.o.cs) + + params[param].append( + hti.getBlockHistoryVal(bName, param, (cycle, node)) + ) + + # verify the height parameter doesn't change over time + self.assertGreater(params["height"][0], 0) + self.assertEqual(params["height"][0], params["height"][1]) + + # verify the power parameter is retrievable from the history + self.assertEqual(o.cs["power"], 1000000000.0) + self.assertAlmostEqual(params["power"][0], 360, delta=0.1) + self.assertEqual(params["power"][0], params["power"][1]) + + # verify the power density parameter is retrievable from the history + self.assertAlmostEqual(params["pdens"][0], 0.0785, delta=0.001) + self.assertEqual(params["pdens"][0], params["pdens"][1]) + + # test that unloadBlockHistoryVals() is working + self.assertIsNotNone(hti._preloadedBlockHistory) + hti.unloadBlockHistoryVals() + self.assertIsNone(hti._preloadedBlockHistory) + def test_historyReport(self): """ Test generation of history report. diff --git a/armi/operators/tests/test_operators.py b/armi/operators/tests/test_operators.py index e389e8955..0f17ea0aa 100644 --- a/armi/operators/tests/test_operators.py +++ b/armi/operators/tests/test_operators.py @@ -438,6 +438,12 @@ def setUp(self): self.detailedOperator = Operator(self.standaloneDetailedCS) def test_getPowerFractions(self): + """Test that the power fractions are calculated correctly. + + .. test:: Test the powerFractions are retrieved correctly for multiple cycles. + :id: T_ARMI_SETTINGS_POWER1 + :tests: R_ARMI_SETTINGS_POWER + """ self.assertEqual( self.detailedOperator.powerFractions, self.powerFractionsSolution ) diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index d6f9960bd..6560ea20f 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -293,7 +293,7 @@ def test_getSetParameters(self): :tests: R_ARMI_PARAM_PART .. test:: Ensure there is a setting for total core power. - :id: T_ARMI_SETTINGS_POWER + :id: T_ARMI_SETTINGS_POWER0 :tests: R_ARMI_SETTINGS_POWER """ # Test at reactor level diff --git a/armi/settings/tests/test_settings.py b/armi/settings/tests/test_settings.py index bb90a5277..1d62ae894 100644 --- a/armi/settings/tests/test_settings.py +++ b/armi/settings/tests/test_settings.py @@ -107,6 +107,33 @@ def test_updateEnvironmentSettingsFrom(self): self.cs.updateEnvironmentSettingsFrom(newEnv) self.assertEqual(self.cs["verbosity"], "9") + def test_metaData(self): + """Test we can get and set the important settings metadata. + + .. test:: Test getting and setting import settings metadata. + :id: T_ARMI_SETTINGS_META + :tests: R_ARMI_SETTINGS_META + """ + # test get/set on caseTitle + self.assertEqual(self.cs.caseTitle, "armi") + testTitle = "test_metaData" + self.cs.caseTitle = testTitle + self.assertEqual(self.cs.caseTitle, testTitle) + + # test get/set on comment + self.assertEqual(self.cs["comment"], "") + testComment = "Comment: test_metaData" + self.cs = self.cs.modified(newSettings={"comment": testComment}) + self.assertEqual(self.cs["comment"], testComment) + + # test get/set on version + self.assertEqual(len(self.cs["versions"]), 0) + self.cs = self.cs.modified(newSettings={"versions": {"something": 1.234}}) + + d = self.cs["versions"] + self.assertEqual(len(d), 1) + self.assertEqual(d["something"], 1.234) + class TestAddingOptions(unittest.TestCase): def setUp(self): From 8a332c9c1f85c1b0f4152a4014043b68049f5135 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 12 Dec 2023 16:29:13 -0800 Subject: [PATCH 097/176] Improving tests and crumbs to Components (#1547) --- armi/reactor/components/__init__.py | 8 ++-- armi/reactor/components/component.py | 16 ++++++- armi/reactor/tests/test_components.py | 65 ++++++++++++++++----------- 3 files changed, 56 insertions(+), 33 deletions(-) diff --git a/armi/reactor/components/__init__.py b/armi/reactor/components/__init__.py index a7fef9038..2c9aab3c9 100644 --- a/armi/reactor/components/__init__.py +++ b/armi/reactor/components/__init__.py @@ -88,17 +88,17 @@ def _removeDimensionNameSpaces(attrs): class NullComponent(Component): - r"""Returns zero for all dimensions. is none.""" + """Returns zero for all dimensions.""" def __cmp__(self, other): - r"""Be smaller than everything.""" + """Be smaller than everything.""" return -1 def __lt__(self, other): return True def __bool__(self): - r"""Handles truth testing.""" + """Handles truth testing.""" return False __nonzero__ = __bool__ # Python2 compatibility @@ -326,7 +326,7 @@ def computeVolume(self): """Cannot compute volume until it is derived. .. impl:: The volume of a DerivedShape depends on the solid shapes surrounding them. - :id: I_ARMI_COMP_FLUID + :id: I_ARMI_COMP_FLUID0 :implements: R_ARMI_COMP_FLUID """ return self._deriveVolumeAndArea() diff --git a/armi/reactor/components/component.py b/armi/reactor/components/component.py index 2ce5b6e18..c79e0e19c 100644 --- a/armi/reactor/components/component.py +++ b/armi/reactor/components/component.py @@ -177,6 +177,9 @@ class Component(composites.Composite, metaclass=ComponentType): :id: I_ARMI_COMP_ORDER :implements: R_ARMI_COMP_ORDER + This is done via the __lt__() method, which is used to control sort() as the + standard approach in Python. However, __lt__() does not show up in the API. + Attributes ---------- temperatureInC : float @@ -286,7 +289,12 @@ def _linkAndStoreDimensions(self, components, **dims): self.resolveLinkedDims(components) def resolveLinkedDims(self, components): - """Convert dimension link strings to actual links.""" + """Convert dimension link strings to actual links. + + .. impl:: The volume of some defined shapes depend on the solid components surrounding them. + :id: I_ARMI_COMP_FLUID1 + :implements: R_ARMI_COMP_FLUID + """ for dimName in self.DIMENSION_NAMES: value = self.p[dimName] if not isinstance(value, str): @@ -794,6 +802,10 @@ def setDimension(self, key, val, retainLink=False, cold=True): """ Set a single dimension on the component. + .. impl:: Set a component dimension, considering thermal expansion. + :id: I_ARMI_COMP_EXPANSION1 + :implements: R_ARMI_COMP_EXPANSION + Parameters ---------- key : str @@ -904,7 +916,7 @@ def getThermalExpansionFactor(self, Tc=None, T0=None): Retrieves the material thermal expansion fraction. .. impl:: Calculates radial thermal expansion factor. - :id: I_ARMI_COMP_EXPANSION + :id: I_ARMI_COMP_EXPANSION0 :implements: R_ARMI_COMP_EXPANSION Parameters diff --git a/armi/reactor/tests/test_components.py b/armi/reactor/tests/test_components.py index d89bd4da5..429f90a3b 100644 --- a/armi/reactor/tests/test_components.py +++ b/armi/reactor/tests/test_components.py @@ -174,10 +174,6 @@ def test_initializeComponentMaterial(self): .. test:: Components are made of one material. :id: T_ARMI_COMP_1MAT0 :tests: R_ARMI_COMP_1MAT - - .. test:: Define a component. - :id: T_ARMI_COMP_DEF0 - :tests: R_ARMI_COMP_DEF """ expectedName = "TestComponent" actualName = self.component.getName() @@ -241,12 +237,7 @@ class TestNullComponent(TestGeneralComponents): componentCls = NullComponent def test_cmp(self): - """Test null component. - - .. test:: Define a component. - :id: T_ARMI_COMP_DEF1 - :tests: R_ARMI_COMP_DEF - """ + """Test null component.""" cur = self.component ref = DerivedShape("DerivedShape", "Material", 0, 0) self.assertLess(cur, ref) @@ -263,7 +254,8 @@ def test_getDimension(self): :id: T_ARMI_COMP_DIMS0 :tests: R_ARMI_COMP_DIMS """ - self.assertEqual(self.component.getDimension(""), 0.0) + for temp in range(400, 901, 25): + self.assertEqual(self.component.getDimension("", Tc=temp), 0.0) class TestUnshapedComponent(TestGeneralComponents): @@ -496,10 +488,6 @@ class TestCircle(TestShapedComponent): def test_getThermalExpansionFactorConservedMassByLinearExpansionPercent(self): """Test that when ARMI thermally expands a circle, mass is conserved. - .. test:: Circle shaped component - :id: T_ARMI_COMP_SHAPES0 - :tests: R_ARMI_COMP_SHAPES - .. test:: Calculate thermal expansion. :id: T_ARMI_COMP_EXPANSION0 :tests: R_ARMI_COMP_EXPANSION @@ -523,10 +511,10 @@ def test_getDimension(self): :id: T_ARMI_COMP_EXPANSION1 :tests: R_ARMI_COMP_EXPANSION """ - hotTemp = 700.0 - ref = self._od * self.component.getThermalExpansionFactor(Tc=hotTemp) - cur = self.component.getDimension("od", Tc=hotTemp) - self.assertAlmostEqual(cur, ref) + for hotTemp in range(600, 901, 25): + ref = self._od * self.component.getThermalExpansionFactor(Tc=hotTemp) + cur = self.component.getDimension("od", Tc=hotTemp) + self.assertAlmostEqual(cur, ref) def test_thermallyExpands(self): """Test that ARMI can thermally expands a circle.""" @@ -555,6 +543,7 @@ def test_getArea(self): :id: T_ARMI_COMP_VOL1 :tests: R_ARMI_COMP_VOL """ + # show we can calculate the area once od = self.component.getDimension("od") idd = self.component.getDimension("id") mult = self.component.getDimension("mult") @@ -562,8 +551,20 @@ def test_getArea(self): cur = self.component.getArea() self.assertAlmostEqual(cur, ref) + # show we can clear the cache, change the temp, and correctly re-calc the area + for newTemp in range(500, 690, 19): + self.component.clearCache() + + # re-calc area + self.component.temperatureInC = newTemp + od = self.component.getDimension("od", Tc=newTemp) + idd = self.component.getDimension("id", Tc=newTemp) + ref = math.pi * ((od / 2) ** 2 - (idd / 2) ** 2) * mult + cur = self.component.getArea() + self.assertAlmostEqual(cur, ref) + def test_componentInteractionsLinkingByDimensions(self): - r"""Tests linking of components by dimensions.""" + """Tests linking of components by dimensions.""" nPins = 217 fuelDims = {"Tinput": 25.0, "Thot": 430.0, "od": 0.9, "id": 0.0, "mult": nPins} cladDims = {"Tinput": 25.0, "Thot": 430.0, "od": 1.1, "id": 1.0, "mult": nPins} @@ -916,6 +917,13 @@ def test_getBoundingCircleOuterDiameter(self): cur = self.component.getBoundingCircleOuterDiameter(cold=True) self.assertAlmostEqual(ref, cur) + # verify the area of the rectangle is correct + ref = self.componentDims["lengthOuter"] * self.componentDims["widthOuter"] + ref -= self.componentDims["lengthInner"] * self.componentDims["widthInner"] + ref *= self.componentDims["mult"] + cur = self.component.getArea(cold=True) + self.assertAlmostEqual(cur, ref) + def test_getCircleInnerDiameter(self): cur = self.component.getCircleInnerDiameter(cold=True) self.assertAlmostEqual(math.sqrt(25.0), cur) @@ -965,12 +973,7 @@ class TestSolidRectangle(TestShapedComponent): } def test_getBoundingCircleOuterDiameter(self): - """Test get bounding circle of the outer diameter. - - .. test:: Define a component. - :id: T_ARMI_COMP_DEF2 - :tests: R_ARMI_COMP_DEF - """ + """Test get bounding circle of the outer diameter.""" ref = math.sqrt(50) cur = self.component.getBoundingCircleOuterDiameter(cold=True) self.assertAlmostEqual(ref, cur) @@ -1042,6 +1045,14 @@ def test_getBoundingCircleOuterDiameter(self): cur = self.component.getBoundingCircleOuterDiameter(cold=True) self.assertAlmostEqual(ref, cur) + # verify the area of the circle is correct + ref = ( + self.componentDims["widthOuter"] ** 2 + - self.componentDims["widthInner"] ** 2 + ) + cur = self.component.getComponentArea(cold=True) + self.assertAlmostEqual(cur, ref) + def test_getCircleInnerDiameter(self): ref = math.sqrt(8.0) cur = self.component.getCircleInnerDiameter(cold=True) @@ -1507,7 +1518,7 @@ class TestSphere(TestShapedComponent): def test_getVolume(self): """Calculate area of sphere. - .. test:: Calculate area of sphere. + .. test:: Calculate volume of sphere. :id: T_ARMI_COMP_VOL12 :tests: R_ARMI_COMP_VOL """ From 0977ee48953e2099d434c6df32a1b184c7ae259f Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Wed, 13 Dec 2023 12:50:37 -0800 Subject: [PATCH 098/176] Fixing issues found in our docstrings (#1549) --- armi/bookkeeping/db/database3.py | 7 +++++- .../tests/test_globalFluxInterface.py | 25 +++++-------------- armi/settings/caseSettings.py | 7 +++++- armi/settings/fwSettings/globalSettings.py | 4 +++ doc/user/physics_coupling.rst | 4 +-- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/armi/bookkeeping/db/database3.py b/armi/bookkeeping/db/database3.py index 189896a21..15f6224c5 100644 --- a/armi/bookkeeping/db/database3.py +++ b/armi/bookkeeping/db/database3.py @@ -223,7 +223,12 @@ def open(self): @staticmethod def writeSystemAttributes(h5db): - """Write system attributes to the database.""" + """Write system attributes to the database. + + .. impl:: Add system attributes to the database. + :id: I_ARMI_DB_QA + :implements: R_ARMI_DB_QA + """ h5db.attrs["user"] = context.USER h5db.attrs["python"] = sys.version h5db.attrs["armiLocation"] = os.path.dirname(context.ROOT) diff --git a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py index 01530d241..5b34fbdd1 100644 --- a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py @@ -111,11 +111,8 @@ def test_readFromSettings(self): """Test reading global flux options from case settings. .. test:: Tests GlobalFluxOptions. - :id: T_ARMI_FLUX_OPTIONS_FROM_CASE_SETTINGS + :id: T_ARMI_FLUX_OPTIONS_CS :tests: R_ARMI_FLUX_OPTIONS - :acceptance_criteria: This test is satisfied when it demonstrates - the ability of the globalFluxInterface to read options from a case - settings object. """ cs = settings.Settings() opts = globalFluxInterface.GlobalFluxOptions("neutronics-run") @@ -126,11 +123,8 @@ def test_readFromReactors(self): """Test reading global flux options from reactor objects. .. test:: Tests GlobalFluxOptions. - :id: T_ARMI_FLUX_OPTIONS_FROM_REACTOR + :id: T_ARMI_FLUX_OPTIONS_R :tests: R_ARMI_FLUX_OPTIONS - :acceptance_criteria: This test is satisfied when it demonstrates - the ability of the globalFluxInterface to read options from a - reactor object. """ reactor = MockReactor() opts = globalFluxInterface.GlobalFluxOptions("neutronics-run") @@ -231,12 +225,9 @@ def setUp(self): def test_executerInteraction(self, mockGeometryTransform, mockExecute): """Run the global flux interface and executer though one time now. - .. test:: Run the global flux interface to check that the mesh - converter is called before the neutronics solver. - :id: T_ARMI_FLUX_GEOM_TRANSFORM_CHECK_CALL_ORDER + .. test:: Run the global flux interface to check that the mesh converter is called before the neutronics solver. + :id: T_ARMI_FLUX_GEOM_TRANSFORM_ORDER :tests: R_ARMI_FLUX_GEOM_TRANSFORM - :acceptance_criteria: The test is considered passing when the mesh - converter is verified to be called before the neutronics solver. """ call_order = [] mockGeometryTransform.side_effect = lambda *a, **kw: call_order.append( @@ -315,13 +306,9 @@ def test_executerInteractionNonUniformAssems(self, mockConverterFactory): This will serve as a broad end-to-end test of the interface, and also stress test the mesh issues with non-uniform assemblies. - .. test:: Run the global flux interface to show the geometry converter - is called when the nonuniform mesh option is used. - :id: T_ARMI_FLUX_GEOM_TRANSFORM_CHECK_CONVERTER_CALL + .. test:: Run the global flux interface to show the geometry converter is called when the nonuniform mesh option is used. + :id: T_ARMI_FLUX_GEOM_TRANSFORM_CONV :tests: R_ARMI_FLUX_GEOM_TRANSFORM - :acceptance_criteria: The test is satisfied when the geometry - converter is shown to have been called when a nonuniform flag is - used. """ gfi = self.gfi gfi.interactBOC() diff --git a/armi/settings/caseSettings.py b/armi/settings/caseSettings.py index 984055448..8b516e854 100644 --- a/armi/settings/caseSettings.py +++ b/armi/settings/caseSettings.py @@ -107,7 +107,12 @@ def inputDirectory(self): @property def caseTitle(self): - """Getter for settings case title.""" + """Getter for settings case title. + + .. impl:: Define a case title to go with the settings. + :id: I_ARMI_SETTINGS_META0 + :implements: R_ARMI_SETTINGS_META + """ if not self.path: return self.defaultCaseTitle else: diff --git a/armi/settings/fwSettings/globalSettings.py b/armi/settings/fwSettings/globalSettings.py index a8f07bdc9..5e521d3db 100644 --- a/armi/settings/fwSettings/globalSettings.py +++ b/armi/settings/fwSettings/globalSettings.py @@ -125,6 +125,10 @@ def defineSettings() -> List[setting.Setting]: .. impl:: There is a setting for total core power. :id: I_ARMI_SETTINGS_POWER :implements: R_ARMI_SETTINGS_POWER + + .. impl:: Define a comment and a versions list to go with the settings. + :id: I_ARMI_SETTINGS_META1 + :implements: R_ARMI_SETTINGS_META """ settings = [ setting.Setting( diff --git a/doc/user/physics_coupling.rst b/doc/user/physics_coupling.rst index 391cb5ac9..b537fcdc1 100644 --- a/doc/user/physics_coupling.rst +++ b/doc/user/physics_coupling.rst @@ -1,6 +1,6 @@ -********************** +**************** Physics Coupling -********************** +**************** Loose Coupling ---------------- From 8a8ba720e78715649d5ae466cbd2ea3b71884176 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 14 Dec 2023 13:08:15 -0800 Subject: [PATCH 099/176] Improving isDepletable test (#1553) --- .../tests/test_fissionProductModel.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/armi/physics/neutronics/fissionProductModel/tests/test_fissionProductModel.py b/armi/physics/neutronics/fissionProductModel/tests/test_fissionProductModel.py index 48408e49e..569168e7d 100644 --- a/armi/physics/neutronics/fissionProductModel/tests/test_fissionProductModel.py +++ b/armi/physics/neutronics/fissionProductModel/tests/test_fissionProductModel.py @@ -138,6 +138,15 @@ def test_nuclidesInModelAllDepletableBlocks(self): self.assertGreater(len(fuelBlocks), 0) self.assertGreater(len(controlBlocks), 0) + # prove that the control blocks are not depletable + for b in controlBlocks: + self.assertFalse(isDepletable(b)) + + # as a corrolary of the above, prove that no components in the control blocks are depletable + for b in controlBlocks: + for c in b.getComponents(): + self.assertFalse(isDepletable(c)) + # Force the the first component in the control blocks # to be labeled as depletable for checking that explicit # fission products can be assigned. @@ -145,6 +154,19 @@ def test_nuclidesInModelAllDepletableBlocks(self): c = b.getComponents()[0] c.p.flags |= Flags.DEPLETABLE + # now each control block should be depletable + for b in controlBlocks: + self.assertTrue(isDepletable(b)) + + # as a corrolary of the above, prove that only the first component in each control block is depletable + for b in controlBlocks: + comps = list(b.getComponents()) + for i, c in enumerate(comps): + if i == 0: + self.assertTrue(isDepletable(c)) + else: + self.assertFalse(isDepletable(c)) + # Run the ``interactBOL`` here to trigger setting up the fission # products in the reactor data model. self.fpModel.interactBOL() From 8857991a17221fd3fc5d22bf1759836e5ba4ba23 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 14 Dec 2023 13:31:07 -0800 Subject: [PATCH 100/176] Improving Reactor tests and crumbs (#1552) --- armi/reactor/reactors.py | 17 ++++------ armi/reactor/tests/test_reactors.py | 43 ++++++++++++++++++++++++-- armi/reactor/tests/test_rz_reactors.py | 7 +---- 3 files changed, 48 insertions(+), 19 deletions(-) diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index 117034bd4..93da2c553 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -172,10 +172,6 @@ def loadFromCs(cs) -> Reactor: """ Load a Reactor based on the input settings. - .. impl:: Users can create a reactor from an input yaml file. - :id: I_ARMI_R_CORE - :implements: R_ARMI_R_CORE - Parameters ---------- cs: CaseSettings @@ -241,6 +237,10 @@ class Core(composites.Composite): This has the bulk of the data management operations. + .. impl:: Represent a reactor core as a composite object. + :id: I_ARMI_R_CORE + :implements: R_ARMI_R_CORE + Attributes ---------- params : dict @@ -347,7 +347,7 @@ def symmetry(self) -> geometry.SymmetryType: """Getter for symmetry type. .. impl:: Get core symmetry. - :id: I_ARMI_R_SYMM0 + :id: I_ARMI_R_SYMM :implements: R_ARMI_R_SYMM """ if not self.spatialGrid: @@ -356,12 +356,7 @@ def symmetry(self) -> geometry.SymmetryType: @symmetry.setter def symmetry(self, val: str): - """Setter for symmetry type. - - .. impl:: Set core symmetry. - :id: I_ARMI_R_SYMM1 - :implements: R_ARMI_R_SYMM - """ + """Setter for symmetry type.""" self.spatialGrid.symmetry = str(val) self.clearCache() diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 6560ea20f..509cf8904 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -16,6 +16,7 @@ import logging import os import unittest +from math import sqrt from unittest.mock import patch from numpy.testing import assert_allclose, assert_equal @@ -244,7 +245,7 @@ def test_coreSfp(self): :tests: R_ARMI_R .. test:: The reactor object includes a core and an SFP. - :id: T_ARMI_R_CHILDREN1 + :id: T_ARMI_R_CHILDREN :tests: R_ARMI_R_CHILDREN """ self.assertTrue(isinstance(self.r.core, reactors.Core)) @@ -720,16 +721,19 @@ def test_getMinimumPercentFluxInFuel(self): def test_getAssemblyWithLoc(self): """ - Get assembly by location. + Get assembly by location, in a couple different ways to ensure they all work. .. test:: Get assembly by location. :id: T_ARMI_R_GET_ASSEM_LOC :tests: R_ARMI_R_GET_ASSEM_LOC """ + a0 = self.r.core.getAssemblyWithStringLocation("003-001") a1 = self.r.core.getAssemblyWithAssemNum(assemNum=10) a2 = self.r.core.getAssembly(locationString="003-001") + self.assertEqual(a0, a2) self.assertEqual(a1, a2) + self.assertEqual(a1.getLocation(), "003-001") def test_getAssemblyWithName(self): """ @@ -743,6 +747,7 @@ def test_getAssemblyWithName(self): a2 = self.r.core.getAssembly(assemblyName="A0010") self.assertEqual(a1, a2) + self.assertEqual(a1.name, "A0010") def test_restoreReactor(self): """Restore a reactor after growing it from third to full core. @@ -1247,6 +1252,40 @@ def test_setPowerIfNecessary(self): self.r.core.setPowerIfNecessary() self.assertAlmostEqual(self.r.core.p.power, 3e9) + def test_findAllMeshPoints(self): + """Test findAllMeshPoints(). + + .. test:: Test that the reactor can calculate its core block mesh. + :id: T_ARMI_R_MESH + :tests: R_ARMI_R_MESH + """ + # lets do some basic sanity checking of the meshpoints + x, y, z = self.r.core.findAllMeshPoints() + + # no two meshpoints should be the same, and they should all be monotonically increasing + for xx in range(1, len(x)): + self.assertGreater(x[xx], x[xx - 1], msg=f"x={xx}") + + for yy in range(1, len(y)): + self.assertGreater(y[yy], y[yy - 1], msg=f"y={yy}") + + for zz in range(1, len(z)): + self.assertGreater(z[zz], z[zz - 1], msg=f"z={zz}") + + # the z-index should start at zero (the bottom) + self.assertEqual(z[0], 0) + + # ensure the X and Y mesh spacing is correct (for a hex core) + pitch = self.r.core.spatialGrid.pitch + + xPitch = sqrt(3) * pitch / 2 + for xx in range(1, len(x)): + self.assertAlmostEqual(x[xx] - x[xx - 1], xPitch, delta=0.0001) + + yPitch = pitch / 2 + for yy in range(1, len(y)): + self.assertAlmostEqual(y[yy] - y[yy - 1], yPitch, delta=0.001) + class CartesianReactorTests(ReactorTests): def setUp(self): diff --git a/armi/reactor/tests/test_rz_reactors.py b/armi/reactor/tests/test_rz_reactors.py index b99b6c6fb..8f46627b2 100644 --- a/armi/reactor/tests/test_rz_reactors.py +++ b/armi/reactor/tests/test_rz_reactors.py @@ -38,12 +38,7 @@ def test_loadRZT(self): self.assertTrue(all(aziMesh == 8 for aziMesh in aziMeshes)) def test_findAllMeshPoints(self): - """Test findAllMeshPoints(). - - .. test:: Test that the reactor can calculate its core block mesh. - :id: T_ARMI_R_MESH - :tests: R_ARMI_R_MESH - """ + """Test findAllMeshPoints().""" i, _, _ = self.r.core.findAllMeshPoints() self.assertLess(i[-1], 2 * math.pi) From 6387c7fa864eee04b099a82970eb20f7b5eb47ff Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 14 Dec 2023 13:49:13 -0800 Subject: [PATCH 101/176] Fixing broken docstring (#1554) --- armi/reactor/reactors.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index 93da2c553..068f9578c 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -773,6 +773,8 @@ def getNumRings(self, indexBased=False): """ Returns the number of rings in this reactor. Based on location so indexing will start at 1. + Circular ring shuffling changes the interpretation of this result. + .. impl:: Retrieve number of rings in core. :id: I_ARMI_R_NUM_RINGS :implements: R_ARMI_R_NUM_RINGS @@ -785,9 +787,6 @@ def getNumRings(self, indexBased=False): ---------- indexBased : bool, optional If true, will force location-index interpretation, even if "circular shuffling" is enabled. - - When circular ring shuffling is activated, this changes interpretation. - Developers plan on making this another method for the secondary interpretation. """ if self.circularRingList and not indexBased: return max(self.circularRingList) From ae1a330680c62a3e433e9ff2e6ee6e5221ccd9b8 Mon Sep 17 00:00:00 2001 From: Tony Alberti Date: Fri, 15 Dec 2023 09:26:11 -0800 Subject: [PATCH 102/176] Updating testing for geometry converters reqs (#1548) --- armi/reactor/converters/geometryConverters.py | 12 +- .../tests/test_geometryConverters.py | 110 ++++++++++++++---- armi/reactor/tests/test_reactors.py | 30 ++++- 3 files changed, 122 insertions(+), 30 deletions(-) diff --git a/armi/reactor/converters/geometryConverters.py b/armi/reactor/converters/geometryConverters.py index 418f320a9..b40a6480c 100644 --- a/armi/reactor/converters/geometryConverters.py +++ b/armi/reactor/converters/geometryConverters.py @@ -435,8 +435,16 @@ def convert(self, r): r : Reactor object The reactor to convert. + .. impl:: Tool to convert a hex core to an RZTheta core. + :id: I_ARMI_CONV_3DHEX_TO_2DRZ + :implements: R_ARMI_CONV_3DHEX_TO_2DRZ + Notes ----- + The linked requirement technically points to a child class of this class, + HexToRZConverter. However, this is the method where the conversion actually happens + and thus the implementation tag is noted here. + As a part of the RZT mesh converters it is possible to obtain a radial mesh that has repeated ring numbers. For instance, if there are fuel assemblies and control assemblies within the same radial hex ring then it's possible that a radial mesh @@ -1199,10 +1207,6 @@ class HexToRZConverter(HexToRZThetaConverter): This is a subclass of the HexToRZThetaConverter. See the HexToRZThetaConverter for explanation and setup of the converterSettings. - - .. impl:: Tool to convert a hex core to an RZTheta core. - :id: I_ARMI_CONV_3DHEX_TO_2DRZ - :implements: R_ARMI_CONV_3DHEX_TO_2DRZ """ _GEOMETRY_TYPE = geometry.GeomType.RZ diff --git a/armi/reactor/converters/tests/test_geometryConverters.py b/armi/reactor/converters/tests/test_geometryConverters.py index 5a1b52c97..0e31b84c8 100644 --- a/armi/reactor/converters/tests/test_geometryConverters.py +++ b/armi/reactor/converters/tests/test_geometryConverters.py @@ -16,7 +16,6 @@ import math import os import unittest - from numpy.testing import assert_allclose from armi import runLog @@ -27,7 +26,7 @@ from armi.reactor.converters import uniformMesh 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 directoryChangers @@ -142,7 +141,14 @@ def tearDown(self): del self.r def test_convert(self): - """Test the HexToRZConverter. + """Test HexToRZConverter.convert(). + + Notes + ----- + Ensure the converted reactor has 1) nuclides and nuclide masses that match the + original reactor, 2) for a given (r,z,theta) location the expected block type exists, + 3) the converted reactor has the right (r,z,theta) coordinates, and 4) the converted + reactor blocks all have a single (homogenized) component. .. test:: Convert a 3D hex reactor core to an RZ-Theta core. :id: T_ARMI_CONV_3DHEX_TO_2DRZ @@ -287,32 +293,47 @@ def test_edgeAssemblies(self): :id: T_ARMI_ADD_EDGE_ASSEMS :tests: R_ARMI_ADD_EDGE_ASSEMS """ + + def getAssemByRingPos(ringPos: tuple): + for a in self.r.core.getAssemblies(): + if a.spatialLocator.getRingPos() == ringPos: + return a + return None + + numAssemsOrig = len(self.r.core.getAssemblies()) + # assert that there is no assembly in the (3, 4) (ring, position). + self.assertIsNone(getAssemByRingPos((3, 4))) + # add the assembly converter = geometryConverters.EdgeAssemblyChanger() converter.addEdgeAssemblies(self.r.core) + numAssemsWithEdgeAssem = len(self.r.core.getAssemblies()) + # assert that there is an assembly in the (3, 4) (ring, position). + self.assertIsNotNone(getAssemByRingPos((3, 4))) + self.assertTrue(numAssemsWithEdgeAssem > numAssemsOrig) + + # try to add the assembly again (you can't) + with mockRunLogs.BufferLog() as mock: + converter.addEdgeAssemblies(self.r.core) + self.assertIn("Skipping addition of edge assemblies", mock.getStdout()) + self.assertTrue(numAssemsWithEdgeAssem, len(self.r.core.getAssemblies())) # must be added after geom transform for b in self.o.r.core.getBlocks(): b.p.power = 1.0 - - numAssems = len(self.r.core.getAssemblies()) converter.scaleParamsRelatedToSymmetry(self.r) - a = self.r.core.getAssembliesOnSymmetryLine(grids.BOUNDARY_0_DEGREES)[0] self.assertTrue(all(b.p.power == 2.0 for b in a), "Powers were not scaled") + # remove the assembly that was added converter.removeEdgeAssemblies(self.r.core) - self.assertTrue(numAssems > len(self.r.core.getAssemblies())) - converter.addEdgeAssemblies(self.r.core) - self.assertEqual(numAssems, len(self.r.core.getAssemblies())) - # make sure it can be called twice. - converter.addEdgeAssemblies(self.r.core) - self.assertEqual(numAssems, len(self.r.core.getAssemblies())) + self.assertIsNone(getAssemByRingPos((3, 4))) + self.assertEqual(numAssemsOrig, len(self.r.core.getAssemblies())) class TestThirdCoreHexToFullCoreChanger(unittest.TestCase): def setUp(self): self.o, self.r = loadTestReactor(TEST_ROOT) - reduceTestReactorRings(self.r, self.o.cs, 2) + reduceTestReactorRings(self.r, self.o.cs, 3) # initialize the block powers to a uniform power profile, accounting for # the loaded reactor being 1/3 core @@ -344,6 +365,14 @@ def test_growToFullCoreFromThirdCore(self): :id: T_ARMI_THIRD_TO_FULL_CORE0 :tests: R_ARMI_THIRD_TO_FULL_CORE """ + + def getLTAAssems(): + aList = [] + for a in self.r.core.getAssemblies(): + if a.getType == "lta fuel": + aList.append(a) + return aList + # Check the initialization of the third core model self.assertFalse(self.r.core.isFullCore) self.assertEqual( @@ -353,7 +382,10 @@ def test_growToFullCoreFromThirdCore(self): ), ) initialNumBlocks = len(self.r.core.getBlocks()) - + assems = getLTAAssems() + expectedLoc = [(3, 2)] + for i, a in enumerate(assems): + self.assertEqual(a.spatialLocator.getRingPos(), expectedLoc[i]) self.assertAlmostEqual( self.r.core.getTotalBlockParam("power"), self.o.cs["power"] / 3, places=5 ) @@ -370,6 +402,10 @@ def test_growToFullCoreFromThirdCore(self): self.assertTrue(self.r.core.isFullCore) self.assertGreater(len(self.r.core.getBlocks()), initialNumBlocks) self.assertEqual(self.r.core.symmetry.domain, geometry.DomainType.FULL_CORE) + assems = getLTAAssems() + expectedLoc = [(3, 2), (3, 6), (3, 10)] + for i, a in enumerate(assems): + self.assertEqual(a.spatialLocator.getRingPos(), expectedLoc[i]) # ensure that block power is handled correctly self.assertAlmostEqual( @@ -394,6 +430,10 @@ def test_growToFullCoreFromThirdCore(self): self.assertAlmostEqual( self.r.core.getTotalBlockParam("power"), self.o.cs["power"] / 3, places=5 ) + assems = getLTAAssems() + expectedLoc = [(3, 2)] + for i, a in enumerate(assems): + self.assertEqual(a.spatialLocator.getRingPos(), expectedLoc[i]) def test_initNewFullReactor(self): """Test that initNewReactor will growToFullCore if necessary.""" @@ -410,7 +450,13 @@ def test_initNewFullReactor(self): self.assertEqual(newR.core.symmetry.domain, geometry.DomainType.FULL_CORE) def test_skipGrowToFullCoreWhenAlreadyFullCore(self): - """Test that hex core is not modified when third core to full core changer is called on an already full core geometry.""" + """Test that hex core is not modified when third core to full core changer is called on an already full core geometry. + + .. test: Convert a one-third core to full core and restore back to one-third core. + :id: T_ARMI_THIRD_TO_FULL_CORE2 + :tests: R_ARMI_THIRD_TO_FULL_CORE + + """ # Check the initialization of the third core model and convert to a full core self.assertFalse(self.r.core.isFullCore) self.assertEqual( @@ -419,15 +465,31 @@ def test_skipGrowToFullCoreWhenAlreadyFullCore(self): geometry.DomainType.THIRD_CORE, geometry.BoundaryType.PERIODIC ), ) + numBlocksThirdCore = len(self.r.core.getBlocks()) + # convert the third core to full core changer = geometryConverters.ThirdCoreHexToFullCoreChanger(self.o.cs) - changer.convert(self.r) - # Check that the changer does not affect the full core model on converting and restoring - initialNumBlocks = len(self.r.core.getBlocks()) + with mockRunLogs.BufferLog() as mock: + changer.convert(self.r) + self.assertIn("Expanding to full core geometry", mock.getStdout()) + numBlocksFullCore = len(self.r.core.getBlocks()) self.assertEqual(self.r.core.symmetry.domain, geometry.DomainType.FULL_CORE) - changer = geometryConverters.ThirdCoreHexToFullCoreChanger(self.o.cs) - changer.convert(self.r) - self.assertEqual(self.r.core.symmetry.domain, geometry.DomainType.FULL_CORE) - self.assertEqual(initialNumBlocks, len(self.r.core.getBlocks())) - changer.restorePreviousGeometry(self.r) - self.assertEqual(initialNumBlocks, len(self.r.core.getBlocks())) + # try to convert to full core again (it shouldn't do anything) + with mockRunLogs.BufferLog() as mock: + changer.convert(self.r) + self.assertIn( + "Detected that full core reactor already exists. Cannot expand.", + mock.getStdout(), + ) self.assertEqual(self.r.core.symmetry.domain, geometry.DomainType.FULL_CORE) + self.assertEqual(numBlocksFullCore, len(self.r.core.getBlocks())) + # restore back to 1/3 core + with mockRunLogs.BufferLog() as mock: + changer.restorePreviousGeometry(self.r) + self.assertIn("revert from full to 1/3 core", mock.getStdout()) + self.assertEqual(numBlocksThirdCore, len(self.r.core.getBlocks())) + self.assertEqual( + self.r.core.symmetry, + geometry.SymmetryType( + geometry.DomainType.THIRD_CORE, geometry.BoundaryType.PERIODIC + ), + ) diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 509cf8904..ddadce688 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -756,10 +756,36 @@ def test_restoreReactor(self): :id: T_ARMI_THIRD_TO_FULL_CORE1 :tests: R_ARMI_THIRD_TO_FULL_CORE """ - aListLength = len(self.r.core.getAssemblies()) + numOfAssembliesOneThird = len(self.r.core.getAssemblies()) + self.assertFalse(self.r.core.isFullCore) + self.assertEqual( + self.r.core.symmetry, + geometry.SymmetryType( + geometry.DomainType.THIRD_CORE, geometry.BoundaryType.PERIODIC + ), + ) + # grow to full core converter = self.r.core.growToFullCore(self.o.cs) + self.assertTrue(self.r.core.isFullCore) + self.assertGreater(len(self.r.core.getAssemblies()), numOfAssembliesOneThird) + self.assertEqual(self.r.core.symmetry.domain, geometry.DomainType.FULL_CORE) + # restore back to 1/3 core converter.restorePreviousGeometry(self.r) - self.assertEqual(aListLength, len(self.r.core.getAssemblies())) + self.assertEqual(numOfAssembliesOneThird, len(self.r.core.getAssemblies())) + self.assertEqual( + self.r.core.symmetry, + geometry.SymmetryType( + geometry.DomainType.THIRD_CORE, geometry.BoundaryType.PERIODIC + ), + ) + self.assertFalse(self.r.core.isFullCore) + self.assertEqual(numOfAssembliesOneThird, len(self.r.core.getAssemblies())) + self.assertEqual( + self.r.core.symmetry, + geometry.SymmetryType( + geometry.DomainType.THIRD_CORE, geometry.BoundaryType.PERIODIC + ), + ) def test_differentNuclideModels(self): self.assertEqual(self.o.cs[CONF_XS_KERNEL], "MC2v3") From b6f10538c44e873bccdb6749bbd8b37dd6e6342e Mon Sep 17 00:00:00 2001 From: Tony Alberti Date: Fri, 15 Dec 2023 09:44:07 -0800 Subject: [PATCH 103/176] Updating testing for converters (#1555) --- .../tests/test_axialExpansionChanger.py | 3 ++ .../converters/tests/test_blockConverter.py | 9 +++--- .../converters/tests/test_uniformMesh.py | 29 ++++++++++--------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/armi/reactor/converters/tests/test_axialExpansionChanger.py b/armi/reactor/converters/tests/test_axialExpansionChanger.py index 8f81b2eea..49a2d7cfd 100644 --- a/armi/reactor/converters/tests/test_axialExpansionChanger.py +++ b/armi/reactor/converters/tests/test_axialExpansionChanger.py @@ -863,6 +863,9 @@ def test_coldAssemblyExpansion(self): Notes ----- + For R_ARMI_INP_COLD_HEIGHT, the action of axial expansion occurs in setUp() during core + construction, specifically in :py:meth:`constructAssem ` + Two assertions here: 1. total assembly height should be preserved (through use of top dummy block) 2. in armi.tests.detailedAxialExpansion.refSmallReactorBase.yaml, diff --git a/armi/reactor/converters/tests/test_blockConverter.py b/armi/reactor/converters/tests/test_blockConverter.py index e3f612c23..9218a73f0 100644 --- a/armi/reactor/converters/tests/test_blockConverter.py +++ b/armi/reactor/converters/tests/test_blockConverter.py @@ -116,14 +116,10 @@ def test_convert(self): .core.getAssemblies(Flags.FUEL)[2] .getFirstBlock(Flags.FUEL) ) - block.spatialGrid = grids.HexGrid.fromPitch(1.0) - area = block.getArea() converter = blockConverters.HexComponentsToCylConverter(block) converter.convert() - self.assertAlmostEqual(area, converter.convertedBlock.getArea()) - self.assertAlmostEqual(area, block.getArea()) for compType in [Flags.FUEL, Flags.CLAD, Flags.DUCT]: self.assertAlmostEqual( @@ -136,7 +132,12 @@ def test_convert(self): ] ), ) + for c in converter.convertedBlock.getComponents(compType): + self.assertEqual( + block.getComponent(compType).temperatureInC, c.temperatureInC + ) + self.assertEqual(block.getHeight(), converter.convertedBlock.getHeight()) self._checkAreaAndComposition(block, converter.convertedBlock) self._checkCiclesAreInContact(converter.convertedBlock) diff --git a/armi/reactor/converters/tests/test_uniformMesh.py b/armi/reactor/converters/tests/test_uniformMesh.py index febdc182e..11f781536 100644 --- a/armi/reactor/converters/tests/test_uniformMesh.py +++ b/armi/reactor/converters/tests/test_uniformMesh.py @@ -254,9 +254,9 @@ def test_filterMesh(self): """ Test that the mesh can be correctly filtered. - .. test:: Preserve the boundaries of fuel and control material. - :id: T_ARMI_UMC_NON_UNIFORM1 - :tests: R_ARMI_UMC_NON_UNIFORM + .. test:: Produce a uniform mesh with a size no smaller than a user-specified value. + :id: T_ARMI_UMC_MIN_MESH1 + :tests: R_ARMI_UMC_MIN_MESH """ meshList = [1.0, 3.0, 4.0, 7.0, 9.0, 12.0, 16.0, 19.0, 20.0] anchorPoints = [4.0, 16.0] @@ -305,7 +305,7 @@ def test_generateCommonMesh(self): Covers generateCommonmesh() and _decuspAxialMesh(). .. test:: Produce a uniform mesh with a size no smaller than a user-specified value. - :id: T_ARMI_UMC_MIN_MESH + :id: T_ARMI_UMC_MIN_MESH0 :tests: R_ARMI_UMC_MIN_MESH .. test:: Preserve the boundaries of fuel and control material. @@ -418,9 +418,8 @@ def test_convertNumberDensities(self): :tests: R_ARMI_UMC """ refMass = self.r.core.getMass("U235") - applyNonUniformHeightDistribution( - self.r - ) # this changes the mass of everything in the core + # perturb the heights of the assemblies -> changes the mass of everything in the core + applyNonUniformHeightDistribution(self.r) perturbedCoreMass = self.r.core.getMass("U235") self.assertNotEqual(refMass, perturbedCoreMass) self.converter.convert(self.r) @@ -428,12 +427,16 @@ def test_convertNumberDensities(self): uniformReactor = self.converter.convReactor uniformMass = uniformReactor.core.getMass("U235") - self.assertAlmostEqual( - perturbedCoreMass, uniformMass - ) # conversion conserved mass - self.assertAlmostEqual( - self.r.core.getMass("U235"), perturbedCoreMass - ) # conversion didn't change source reactor mass + # conversion conserved mass + self.assertAlmostEqual(perturbedCoreMass, uniformMass) + # conversion didn't change source reactor mass + self.assertAlmostEqual(self.r.core.getMass("U235"), perturbedCoreMass) + # conversion results in uniform axial mesh + refAssemMesh = self.converter.convReactor.core.refAssem.getAxialMesh() + for a in self.converter.convReactor.core: + mesh = a.getAxialMesh() + for ref, check in zip(refAssemMesh, mesh): + self.assertEqual(ref, check) def test_applyStateToOriginal(self): """ From ec0fa39068b2df8ab4e8a3ccd38cdbb1172d3017 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Fri, 15 Dec 2023 10:21:49 -0800 Subject: [PATCH 104/176] Fixing broken impl tag in geom converters (#1556) --- armi/reactor/converters/geometryConverters.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/armi/reactor/converters/geometryConverters.py b/armi/reactor/converters/geometryConverters.py index b40a6480c..18fac9e99 100644 --- a/armi/reactor/converters/geometryConverters.py +++ b/armi/reactor/converters/geometryConverters.py @@ -430,15 +430,15 @@ def convert(self, r): """ Run the conversion to 3 dimensional R-Z-Theta. + .. impl:: Tool to convert a hex core to an RZTheta core. + :id: I_ARMI_CONV_3DHEX_TO_2DRZ + :implements: R_ARMI_CONV_3DHEX_TO_2DRZ + Attributes ---------- r : Reactor object The reactor to convert. - .. impl:: Tool to convert a hex core to an RZTheta core. - :id: I_ARMI_CONV_3DHEX_TO_2DRZ - :implements: R_ARMI_CONV_3DHEX_TO_2DRZ - Notes ----- The linked requirement technically points to a child class of this class, From b11743019588267117ee0bc78c73d08407a41ea6 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Mon, 18 Dec 2023 10:14:16 -0800 Subject: [PATCH 105/176] Adding code coverage to FuelHandlers and Core (#1557) --- armi/physics/fuelCycle/fuelHandlers.py | 36 ++++++++----- .../fuelCycle/tests/test_fuelHandlers.py | 54 +++++++++++++++++++ armi/reactor/tests/test_reactors.py | 31 +++++++++++ 3 files changed, 108 insertions(+), 13 deletions(-) diff --git a/armi/physics/fuelCycle/fuelHandlers.py b/armi/physics/fuelCycle/fuelHandlers.py index 8e2e5483a..ac463bf5e 100644 --- a/armi/physics/fuelCycle/fuelHandlers.py +++ b/armi/physics/fuelCycle/fuelHandlers.py @@ -124,7 +124,10 @@ def outage(self, factor=1.0): # The user can choose the algorithm method name directly in the settings if hasattr(rotAlgos, self.cs[CONF_ASSEMBLY_ROTATION_ALG]): rotationMethod = getattr(rotAlgos, self.cs[CONF_ASSEMBLY_ROTATION_ALG]) - rotationMethod() + try: + rotationMethod() + except TypeError: + rotationMethod(self) else: raise RuntimeError( "FuelHandler {0} does not have a rotation algorithm called {1}.\n" @@ -541,17 +544,9 @@ def getParamWithBlockLevelMax(a, paramName): # this assembly is in the excluded location list. skip it. continue - # only continue of the Assembly is in a Zone - if zoneList: - found = False # guilty until proven innocent - for zone in zoneList: - if a.getLocation() in zone: - # great! it's in there, so we'll accept this assembly - found = True # innocent - break - if not found: - # this assembly is not in any of the zones in the zone list. skip it. - continue + # only process of the Assembly is in a Zone + if not self.isAssemblyInAZone(zoneList, a): + continue # Now find the assembly with the param closest to the target val. if param: @@ -618,6 +613,21 @@ def getParamWithBlockLevelMax(a, paramName): else: return minDiff[1] + @staticmethod + def isAssemblyInAZone(zoneList, a): + """Does the given assembly in one of these zones.""" + if zoneList: + # ruff: noqa: SIM110 + for zone in zoneList: + if a.getLocation() in zone: + # Success! + return True + + return False + else: + # A little counter-intuitively, if there are no zones, we return True. + return True + def _getAssembliesInRings( self, ringList, @@ -626,7 +636,7 @@ def _getAssembliesInRings( exclusions=None, circularRingFlag=False, ): - r""" + """ find assemblies in particular rings. Parameters diff --git a/armi/physics/fuelCycle/tests/test_fuelHandlers.py b/armi/physics/fuelCycle/tests/test_fuelHandlers.py index 41f7e7fb7..d8026c8b4 100644 --- a/armi/physics/fuelCycle/tests/test_fuelHandlers.py +++ b/armi/physics/fuelCycle/tests/test_fuelHandlers.py @@ -20,12 +20,14 @@ import collections import copy import unittest +from unittest.mock import patch import numpy as np from armi.physics.fuelCycle import fuelHandlers, settings from armi.physics.fuelCycle.settings import ( CONF_ASSEM_ROTATION_STATIONARY, + CONF_ASSEMBLY_ROTATION_ALG, CONF_PLOT_SHUFFLE_ARROWS, CONF_RUN_LATTICE_BEFORE_SHUFFLING, ) @@ -36,6 +38,7 @@ from armi.reactor import assemblies, blocks, components, grids from armi.reactor.flags import Flags from armi.reactor.tests import test_reactors +from armi.reactor.zones import Zone from armi.settings import caseSettings from armi.tests import ArmiTestHelper from armi.tests import mockRunLogs @@ -177,6 +180,53 @@ def test_findHighBu(self): ) self.assertIs(a, a1) + @patch("armi.physics.fuelCycle.fuelHandlers.FuelHandler.chooseSwaps") + def test_outage(self, mockChooseSwaps): + # mock up a fuel handler + fh = fuelHandlers.FuelHandler(self.o) + mockChooseSwaps.return_value = list(self.r.core.getAssemblies()) + + # edge case: cannot perform two outages on the same FuelHandler + fh.moved = [self.r.core.getFirstAssembly()] + with self.assertRaises(ValueError): + fh.outage(factor=1.0) + + # edge case: fail if the shuffle file is missing + fh.moved = [] + self.o.cs = self.o.cs.modified( + newSettings={"explicitRepeatShuffles": "fakePath"} + ) + with self.assertRaises(RuntimeError): + fh.outage(factor=1.0) + + # a successful run + fh.moved = [] + self.o.cs = self.o.cs.modified( + newSettings={ + "explicitRepeatShuffles": "", + "fluxRecon": True, + CONF_ASSEMBLY_ROTATION_ALG: "simpleAssemblyRotation", + } + ) + fh.outage(factor=1.0) + self.assertEqual(len(fh.moved), 0) + + def test_isAssemblyInAZone(self): + # build a fuel handler + fh = fuelHandlers.FuelHandler(self.o) + + # test the default value if there are no zones + a = self.r.core.getFirstAssembly() + self.assertTrue(fh.isAssemblyInAZone(None, a)) + + # If our assembly isn't in one of the supplied zones + z = Zone("test_isAssemblyInAZone") + self.assertFalse(fh.isAssemblyInAZone([z], a)) + + # If our assembly IS in one of the supplied zones + z.addLoc(a.getLocation()) + self.assertTrue(fh.isAssemblyInAZone([z], a)) + def test_width(self): """Tests the width capability of findAssembly.""" fh = fuelHandlers.FuelHandler(self.o) @@ -465,6 +515,10 @@ def test_readMoves(self): self.assertEqual(sfpMove[1], "005-003") self.assertEqual(sfpMove[4], "A0073") # name of assem in SFP + # make sure we fail hard if the file doesn't exist + with self.assertRaises(RuntimeError): + fh.readMoves("totall_fictional_file.txt") + def test_processMoveList(self): fh = fuelHandlers.FuelHandler(self.o) moves = fh.readMoves("armiRun-SHUFFLES.txt") diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index ddadce688..c0851cdb1 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -462,6 +462,18 @@ def test_getMaxNumPins(self): numPins = self.r.core.getMaxNumPins() self.assertEqual(169, numPins) + def test_addMultipleCores(self): + """Test the catch that a reactor can only have one core.""" + with self.assertRaises(RuntimeError): + self.r.add(self.r.core) + + def test_getReactor(self): + """The Core object can return its Reactor parent; test that getter.""" + self.assertTrue(isinstance(self.r.core.r, reactors.Reactor)) + + self.r.core.parent = None + self.assertIsNone(self.r.core.r) + def test_addMoreNodes(self): originalMesh = self.r.core.p.axialMesh bigMesh = list(originalMesh) @@ -1015,6 +1027,25 @@ def test_removeAssembliesInRingByCount(self): self.r.core.removeAssembliesInRing(9, self.o.cs) self.assertEqual(self.r.core.getNumRings(), 8) + def test_getNumRings(self): + self.assertEqual(len(self.r.core.circularRingList), 0) + self.assertEqual(self.r.core.getNumRings(indexBased=True), 9) + self.assertEqual(self.r.core.getNumRings(indexBased=False), 9) + + self.r.core.circularRingList = {1, 2, 3} + self.assertEqual(len(self.r.core.circularRingList), 3) + self.assertEqual(self.r.core.getNumRings(indexBased=True), 9) + self.assertEqual(self.r.core.getNumRings(indexBased=False), 3) + + @patch("armi.reactor.reactors.Core.getAssemblies") + def test_whenNoAssemblies(self, mockGetAssemblies): + """Test various edge cases when there are no assemblies.""" + mockGetAssemblies.return_value = [] + + self.assertEqual(self.r.core.countBlocksWithFlags(Flags.FUEL), 0) + self.assertEqual(self.r.core.countFuelAxialBlocks(), 0) + self.assertGreater(self.r.core.getFirstFuelBlockAxialNode(), 9e9) + def test_removeAssembliesInRingHex(self): """ Since the test reactor is hex, we need to use the overrideCircularRingMode option From ddce755e7509b6209b4479fe274b25327ee7d8e0 Mon Sep 17 00:00:00 2001 From: Arrielle Opotowsky Date: Mon, 18 Dec 2023 17:25:56 -0600 Subject: [PATCH 106/176] Adding sys info printout for all nodes (#1559) --- armi/bookkeeping/report/reportingUtils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/armi/bookkeeping/report/reportingUtils.py b/armi/bookkeeping/report/reportingUtils.py index ad0fed510..5a37f69e3 100644 --- a/armi/bookkeeping/report/reportingUtils.py +++ b/armi/bookkeeping/report/reportingUtils.py @@ -21,6 +21,7 @@ import os import pathlib import re +import subprocess import sys import tabulate import textwrap @@ -170,6 +171,7 @@ def _writeMachineInformation(): processorNames = context.MPI_NODENAMES uniqueNames = set(processorNames) nodeMappingData = [] + sysInfo = "" for uniqueName in uniqueNames: matchingProcs = [ str(rank) @@ -180,6 +182,13 @@ def _writeMachineInformation(): nodeMappingData.append( (uniqueName, numProcessors, ", ".join(matchingProcs)) ) + # If this is on Windows: run sys info on each unique node too + if "win" in sys.platform: + sysInfoCmd = 'systeminfo | findstr /B /C:"OS Name" /B /C:"OS Version" /B /C:"Processor" && systeminfo | findstr /E /C:"Mhz"' + out = subprocess.run( + sysInfoCmd, capture_output=True, text=True, shell=True + ) + sysInfo += out.stdout runLog.header("=========== Machine Information ===========") runLog.info( tabulate.tabulate( @@ -188,6 +197,9 @@ def _writeMachineInformation(): tablefmt="armi", ) ) + if sysInfo: + runLog.header("=========== System Information ===========") + runLog.info(sysInfo) def _writeReactorCycleInformation(o, cs): """Verify that all the operating parameters are defined for the same number of cycles.""" From 7856a84f0c7b27618d018c89c4390ed8117ee037 Mon Sep 17 00:00:00 2001 From: Tony Alberti Date: Tue, 19 Dec 2023 10:59:16 -0800 Subject: [PATCH 107/176] Adding Operator.getActiveinterfaces() (#1560) --- armi/operators/operator.py | 119 ++++++++++++++++--------- armi/operators/tests/test_operators.py | 41 +++++++++ 2 files changed, 118 insertions(+), 42 deletions(-) diff --git a/armi/operators/operator.py b/armi/operators/operator.py index 4cf08a1d7..c17875d56 100644 --- a/armi/operators/operator.py +++ b/armi/operators/operator.py @@ -27,6 +27,7 @@ import re import shutil import time +from typing import Tuple from armi import context from armi import interfaces @@ -43,6 +44,8 @@ CONF_TIGHT_COUPLING, CONF_TIGHT_COUPLING_MAX_ITERS, CONF_CYCLES_SKIP_TIGHT_COUPLING_INTERACTION, + CONF_DEFERRED_INTERFACE_NAMES, + CONF_DEFERRED_INTERFACES_CYCLE, ) from armi.utils import codeTiming from armi.utils import ( @@ -574,8 +577,7 @@ def _debugDB(self, interactionName, interfaceName, statePointIndex=0): def interactAllInit(self): """Call interactInit on all interfaces in the stack after they are initialized.""" - allInterfaces = self.interfaces[:] # copy just in case - self._interactAll("Init", allInterfaces) + self._interactAll("Init", self.getInterfaces()) def interactAllBOL(self, excludedInterfaceNames=()): """ @@ -583,30 +585,15 @@ def interactAllBOL(self, excludedInterfaceNames=()): All enabled or bolForce interfaces will be called excluding interfaces with excludedInterfaceNames. """ - activeInterfaces = [ - ii - for ii in self.interfaces - if (ii.enabled() or ii.bolForce()) and ii.name not in excludedInterfaceNames - ] - activeInterfaces = [ - ii - for ii in activeInterfaces - if ii.name not in self.cs["deferredInterfaceNames"] - ] + activeInterfaces = self.getActiveInterfaces("BOL", excludedInterfaceNames) self._interactAll("BOL", activeInterfaces) def interactAllBOC(self, cycle): """Interact at beginning of cycle of all enabled interfaces.""" - activeInterfaces = [ii for ii in self.interfaces if ii.enabled()] - if cycle < self.cs["deferredInterfacesCycle"]: - activeInterfaces = [ - ii - for ii in activeInterfaces - if ii.name not in self.cs["deferredInterfaceNames"] - ] + activeInterfaces = self.getActiveInterfaces("BOC", cycle=cycle) return self._interactAll("BOC", activeInterfaces, cycle) - def interactAllEveryNode(self, cycle, tn, excludedInterfaceNames=None): + def interactAllEveryNode(self, cycle, tn, excludedInterfaceNames=()): """ Call the interactEveryNode hook for all enabled interfaces. @@ -621,22 +608,12 @@ def interactAllEveryNode(self, cycle, tn, excludedInterfaceNames=None): excludedInterfaceNames : list, optional Names of interface names that will not be interacted with. """ - excludedInterfaceNames = excludedInterfaceNames or () - activeInterfaces = [ - ii - for ii in self.interfaces - if ii.enabled() and ii.name not in excludedInterfaceNames - ] + activeInterfaces = self.getActiveInterfaces("EveryNode", excludedInterfaceNames) self._interactAll("EveryNode", activeInterfaces, cycle, tn) - def interactAllEOC(self, cycle, excludedInterfaceNames=None): + def interactAllEOC(self, cycle, excludedInterfaceNames=()): """Interact end of cycle for all enabled interfaces.""" - excludedInterfaceNames = excludedInterfaceNames or () - activeInterfaces = [ - ii - for ii in self.interfaces - if ii.enabled() and ii.name not in excludedInterfaceNames - ] + activeInterfaces = self.getActiveInterfaces("EOC", excludedInterfaceNames) self._interactAll("EOC", activeInterfaces, cycle) def interactAllEOL(self): @@ -648,11 +625,8 @@ def interactAllEOL(self): If the interfaces are flagged to be reversed at EOL, they are separated from the main stack and appended at the end in reverse order. This allows, for example, an interface that must run first to also run last. """ - activeInterfaces = [ii for ii in self.interfaces if ii.enabled()] - interfacesAtEOL = [ii for ii in activeInterfaces if not ii.reverseAtEOL] - activeReverseInterfaces = [ii for ii in activeInterfaces if ii.reverseAtEOL] - interfacesAtEOL.extend(reversed(activeReverseInterfaces)) - self._interactAll("EOL", interfacesAtEOL) + activeInterfaces = self.getActiveInterfaces("EOL") + self._interactAll("EOL", activeInterfaces) def interactAllCoupled(self, coupledIteration): """ @@ -670,8 +644,7 @@ def interactAllCoupled(self, coupledIteration): :id: I_ARMI_OPERATOR_PHYSICS1 :implements: R_ARMI_OPERATOR_PHYSICS """ - activeInterfaces = [ii for ii in self.interfaces if ii.enabled()] - + activeInterfaces = self.getActiveInterfaces("Coupled") # Store the previous iteration values before calling interactAllCoupled # for each interface. for interface in activeInterfaces: @@ -679,7 +652,6 @@ def interactAllCoupled(self, coupledIteration): interface.coupler.storePreviousIterationValue( interface.getTightCouplingValue() ) - self._interactAll("Coupled", activeInterfaces, coupledIteration) return self._checkTightCouplingConvergence(activeInterfaces) @@ -934,7 +906,13 @@ def getInterface(self, name=None, function=None): return candidateI def interfaceIsActive(self, name): - """True if named interface exists and is active.""" + """True if named interface exists and is enabled. + + Notes + ----- + This logic is significantly simpler that getActiveInterfaces. This logic only + touches the enabled() flag, but doesn't take into account the case settings. + """ i = self.getInterface(name) return i and i.enabled() @@ -952,6 +930,63 @@ def getInterfaces(self): """ return self.interfaces[:] + def getActiveInterfaces( + self, + interactState: str, + excludedInterfaceNames: Tuple[str] = (), + cycle: int = 0, + ): + """Retrieve the interfaces which are active for a given interaction state. + + Parameters + ---------- + interactState: str + A string dictating which interaction state the interfaces should be pulled for. + excludedInterfaceNames: Tuple[str] + A tuple of strings dictating which interfaces should be manually skipped. + cycle: int + The given cycle. 0 by default. + + Returns + ------- + activeInterfaces: List[Interfaces] + The interfaces deemed active for the given interactState. + """ + # Validate the inputs + if excludedInterfaceNames is None: + excludedInterfaceNames = () + + if interactState not in ("BOL", "BOC", "EveryNode", "EOC", "EOL", "Coupled"): + raise ValueError(f"{interactState} is an unknown interaction state!") + + # Ensure the interface is enabled. + enabled = lambda i: i.enabled() + if interactState == "BOL": + enabled = lambda i: i.enabled() or i.bolForce() + + # Ensure the name of the interface isn't in some exclusion list. + nameCheck = lambda i: True + if interactState == "EveryNode" or interactState == "EOC": + nameCheck = lambda i: i.name not in excludedInterfaceNames + elif interactState == "BOC" and cycle < self.cs[CONF_DEFERRED_INTERFACES_CYCLE]: + nameCheck = lambda i: i.name not in self.cs[CONF_DEFERRED_INTERFACE_NAMES] + elif interactState == "BOL": + nameCheck = ( + lambda i: i.name not in self.cs[CONF_DEFERRED_INTERFACE_NAMES] + and i.name not in excludedInterfaceNames + ) + + # Finally, find the active interfaces. + activeInterfaces = [i for i in self.interfaces if enabled(i) and nameCheck(i)] + + # Special Case: At EOL we reverse the order of some interfaces. + if interactState == "EOL": + actInts = [ii for ii in activeInterfaces if not ii.reverseAtEOL] + actInts.extend(reversed([ii for ii in activeInterfaces if ii.reverseAtEOL])) + activeInterfaces = actInts + + return activeInterfaces + def reattach(self, r, cs=None): """Add links to globally-shared objects to this operator and all interfaces. diff --git a/armi/operators/tests/test_operators.py b/armi/operators/tests/test_operators.py index 0f17ea0aa..581a2a923 100644 --- a/armi/operators/tests/test_operators.py +++ b/armi/operators/tests/test_operators.py @@ -35,6 +35,8 @@ CONF_TIGHT_COUPLING, CONF_CYCLES_SKIP_TIGHT_COUPLING_INTERACTION, CONF_TIGHT_COUPLING_SETTINGS, + CONF_DEFERRED_INTERFACE_NAMES, + CONF_DEFERRED_INTERFACES_CYCLE, ) from armi.tests import mockRunLogs from armi.utils import directoryChangers @@ -171,6 +173,45 @@ def test_interfaceIsActive(self): self.assertTrue(self.o.interfaceIsActive("main")) self.assertFalse(self.o.interfaceIsActive("Fake-o")) + def test_getActiveInterfaces(self): + """Ensure that the right interfaces are returned for a given interaction state.""" + self.o.cs[CONF_DEFERRED_INTERFACES_CYCLE] = 1 + self.o.cs[CONF_DEFERRED_INTERFACE_NAMES] = ["history"] + + # Test invalid inputs. + with self.assertRaises(ValueError): + self.o.getActiveInterfaces("notAnInterface") + + # Test BOL + interfaces = self.o.getActiveInterfaces( + "BOL", excludedInterfaceNames=("xsGroups") + ) + interfaceNames = [interface.name for interface in interfaces] + self.assertNotIn("xsGroups", interfaceNames) + self.assertNotIn("history", interfaceNames) + + # Test BOC + interfaces = self.o.getActiveInterfaces("BOC", cycle=0) + interfaceNames = [interface.name for interface in interfaces] + self.assertNotIn("history", interfaceNames) + + # Test EveryNode and EOC + interfaces = self.o.getActiveInterfaces( + "EveryNode", excludedInterfaceNames=("xsGroups") + ) + interfaceNames = [interface.name for interface in interfaces] + self.assertIn("history", interfaceNames) + self.assertNotIn("xsGroups", interfaceNames) + + # Test EOL + interfaces = self.o.getActiveInterfaces("EOL") + self.assertEqual(interfaces[-1].name, "main") + + # Test Coupled + interfaces = self.o.getActiveInterfaces("Coupled") + for test, ref in zip(interfaces, self.activeInterfaces): + self.assertEqual(test.name, ref.name) + def test_loadStateError(self): """The ``loadTestReactor()`` test tool does not have any history in the DB to load from.""" # a first, simple test that this method fails correctly From 86f55a6b93a690aafb74a64f187c3eb349db2859 Mon Sep 17 00:00:00 2001 From: Chris Keckler Date: Tue, 19 Dec 2023 13:49:02 -0600 Subject: [PATCH 108/176] Editorial fixes for various rxcoeff params (#1522) * Remove stray closing parenthesis from various rxcoeff params * Update capitalization of params * Update punctuation * Update units of rxFuelAxialExpansionCoeffPerPercent --- armi/reactor/blockParameters.py | 78 +++++++++++++++---------------- armi/reactor/reactorParameters.py | 4 +- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/armi/reactor/blockParameters.py b/armi/reactor/blockParameters.py index 0bd3952b5..fe1eb895c 100644 --- a/armi/reactor/blockParameters.py +++ b/armi/reactor/blockParameters.py @@ -443,82 +443,82 @@ def xsTypeNum(self, value): pb.defParam( "rxFuelDensityCoeffPerMass", units=f"{units.REACTIVITY}/{units.KG}", - description="Fuel Density Coefficient", + description="Fuel density coefficient", ) pb.defParam( "rxFuelDopplerConstant", units=f"{units.REACTIVITY}*{units.DEGK}^(n-1)", - description="Fuel Doppler Constant", + description="Fuel Doppler constant", ) pb.defParam( "rxFuelVoidedDopplerConstant", units=f"{units.REACTIVITY}*{units.DEGK}^(n-1)", - description="Fuel Voided-Coolant Constant", + description="Fuel voided-coolant Doppler constant", ) pb.defParam( "rxFuelTemperatureCoeffPerMass", units=f"{units.REACTIVITY}/{units.KG}", - description="Fuel Temperature Coefficient", + description="Fuel temperature coefficient", ) pb.defParam( "rxFuelVoidedTemperatureCoeffPerMass", units=f"{units.REACTIVITY}/{units.KG}", - description="Fuel Voided-Coolant Temperature Coefficient", + description="Fuel voided-coolant temperature coefficient", ) # CLAD COEFFICIENTS pb.defParam( "rxCladDensityCoeffPerMass", units=f"{units.REACTIVITY}/{units.KG}", - description="Clad Density Coefficient", + description="Clad density coefficient", ) pb.defParam( "rxCladDopplerConstant", units=f"{units.REACTIVITY}*{units.DEGK}^(n-1)", - description="Clad Doppler Constant", + description="Clad Doppler constant", ) pb.defParam( "rxCladTemperatureCoeffPerMass", units=f"{units.REACTIVITY}/{units.KG}", - description="Clad Temperature Coefficient", + description="Clad temperature coefficient", ) # STRUCTURE COEFFICIENTS pb.defParam( "rxStructureDensityCoeffPerMass", units=f"{units.REACTIVITY}/{units.KG}", - description="Structure Density Coefficient", + description="Structure density coefficient", ) pb.defParam( "rxStructureDopplerConstant", units=f"{units.REACTIVITY}*{units.DEGK}^(n-1)", - description="Structure Doppler Constant", + description="Structure Doppler constant", ) pb.defParam( "rxStructureTemperatureCoeffPerMass", units=f"{units.REACTIVITY}/{units.KG}", - description="Structure Temperature Coefficient", + description="Structure temperature coefficient", ) # COOLANT COEFFICIENTS pb.defParam( "rxCoolantDensityCoeffPerMass", units=f"{units.REACTIVITY}/{units.KG}", - description="Coolant Density Coefficient", + description="Coolant density coefficient", ) pb.defParam( "rxCoolantTemperatureCoeffPerMass", units=f"{units.REACTIVITY}/{units.KG}", - description="Coolant Temperature Coefficient", + description="Coolant temperature coefficient", ) with pDefs.createBuilder( @@ -534,83 +534,83 @@ def xsTypeNum(self, value): # FUEL COEFFICIENTS pb.defParam( "rxFuelDensityCoeffPerTemp", - units=f"{units.REACTIVITY}/{units.DEGK})", - description="Fuel Density Coefficient", + units=f"{units.REACTIVITY}/{units.DEGK}", + description="Fuel density coefficient", ) pb.defParam( "rxFuelDopplerCoeffPerTemp", - units=f"{units.REACTIVITY}/{units.DEGK})", - description="Fuel Doppler Coefficient", + units=f"{units.REACTIVITY}/{units.DEGK}", + description="Fuel Doppler coefficient", ) pb.defParam( "rxFuelVoidedDopplerCoeffPerTemp", - units=f"{units.REACTIVITY}/{units.DEGK})", - description="Fuel Voided-Coolant Doppler Coefficient", + units=f"{units.REACTIVITY}/{units.DEGK}", + description="Fuel voided-coolant Doppler coefficient", ) pb.defParam( "rxFuelTemperatureCoeffPerTemp", - units=f"{units.REACTIVITY}/{units.DEGK})", - description="Fuel Temperature Coefficient", + units=f"{units.REACTIVITY}/{units.DEGK}", + description="Fuel temperature coefficient", ) pb.defParam( "rxFuelVoidedTemperatureCoeffPerTemp", - units=f"{units.REACTIVITY}/{units.DEGK})", - description="Fuel Voided-Coolant Temperature Coefficient", + units=f"{units.REACTIVITY}/{units.DEGK}", + description="Fuel voided-coolant temperature coefficient", ) # CLAD COEFFICIENTS pb.defParam( "rxCladDensityCoeffPerTemp", - units=f"{units.REACTIVITY}/{units.DEGK})", - description="Clad Density Coefficient", + units=f"{units.REACTIVITY}/{units.DEGK}", + description="Clad density coefficient", ) pb.defParam( "rxCladDopplerCoeffPerTemp", - units=f"{units.REACTIVITY}/{units.DEGK})", - description="Clad Doppler Coefficient", + units=f"{units.REACTIVITY}/{units.DEGK}", + description="Clad Doppler coefficient", ) pb.defParam( "rxCladTemperatureCoeffPerTemp", - units=f"{units.REACTIVITY}/{units.DEGK})", - description="Clad Temperature Coefficient", + units=f"{units.REACTIVITY}/{units.DEGK}", + description="Clad temperature coefficient", ) # STRUCTURE COEFFICIENTS pb.defParam( "rxStructureDensityCoeffPerTemp", - units=f"{units.REACTIVITY}/{units.DEGK})", - description="Structure Density Coefficient", + units=f"{units.REACTIVITY}/{units.DEGK}", + description="Structure density coefficient", ) pb.defParam( "rxStructureDopplerCoeffPerTemp", - units=f"{units.REACTIVITY}/{units.DEGK})", - description="Structure Doppler Coefficient", + units=f"{units.REACTIVITY}/{units.DEGK}", + description="Structure Doppler coefficient", ) pb.defParam( "rxStructureTemperatureCoeffPerTemp", - units=f"{units.REACTIVITY}/{units.DEGK})", - description="Structure Temperature Coefficient", + units=f"{units.REACTIVITY}/{units.DEGK}", + description="Structure temperature coefficient", ) # COOLANT COEFFICIENTS pb.defParam( "rxCoolantDensityCoeffPerTemp", - units=f"{units.REACTIVITY}/{units.DEGK})", - description="Coolant Density Coefficient", + units=f"{units.REACTIVITY}/{units.DEGK}", + description="Coolant density coefficient", ) pb.defParam( "rxCoolantTemperatureCoeffPerTemp", - units=f"{units.REACTIVITY}/{units.DEGK})", - description="Coolant Temperature Coefficient", + units=f"{units.REACTIVITY}/{units.DEGK}", + description="Coolant temperature coefficient", ) with pDefs.createBuilder(default=0.0) as pb: diff --git a/armi/reactor/reactorParameters.py b/armi/reactor/reactorParameters.py index 4822d7b45..cc9fe3454 100644 --- a/armi/reactor/reactorParameters.py +++ b/armi/reactor/reactorParameters.py @@ -537,7 +537,7 @@ def defineCoreParameters(): pb.defParam( "betaComponents", units=units.UNITLESS, - description="Group-wise delayed neutron fractions.", + description="Group-wise delayed neutron fractions", default=None, ) @@ -574,7 +574,7 @@ def defineCoreParameters(): pb.defParam( "rxFuelAxialExpansionCoeffPerPercent", - units="dk/kk'-%", + units=f"{units.REACTIVITY}/{units.PERCENT}", description="Fuel Axial Expansion Coefficient", ) From 87c61b903584eeb9efbf5129e080e9cd635af48c Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 19 Dec 2023 14:55:29 -0800 Subject: [PATCH 109/176] Improving parallel coding documentation (#1562) --- doc/developer/parallel_coding.rst | 93 ++++++++++++++++--------------- doc/developer/reports.rst | 1 - 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/doc/developer/parallel_coding.rst b/doc/developer/parallel_coding.rst index ad3d5b779..d6c14520d 100644 --- a/doc/developer/parallel_coding.rst +++ b/doc/developer/parallel_coding.rst @@ -27,23 +27,28 @@ are expected to do next. Here is an example:: - if rank == 0: + from armi import context + + cmd = f"val{context.MPI_RANK}" + + if context.MPI_RANK == 0: # The primary node will send the string 'bob' to all others - cmd = 'bob' - comm.bcast(cmd, root=0) + cmd = "bob" + context.MPI_COMM.bcast(cmd, root=0) else: # these are the workers. They receive a value and set it to the variable cmd - cmd = comm.bcast(None, root=0) + context.MPI_COMM = comm.bcast(None, root=0) Note that the ``comm`` object is from the ``mpi4py`` module that deals with the MPI drivers. The value of cmd on the worker before and after the ``bcast`` command are shown in the table. -============ ===== ===== ===== ===== - Proc1 Proc2 Proc3 Proc4 -============ ===== ===== ===== ===== -Before bcast 'bob' 4 'sam' 3.14 -After bcast 'bob' 'bob' 'bob' 'bob' -============ ===== ===== ===== ===== ++--------------+-------+--------+--------+--------+ +| | Proc0 | Proc1 | Proc2 | Proc3 | ++--------------+-------+--------+--------+--------+ +| Before bcast | "bob" | "val1" | "val2" | "val3" | ++--------------+-------+--------+--------+--------+ +| After bcast | "bob" | "bob" | "bob" | "bob" | ++--------------+-------+--------+--------+--------+ The second important type of communication is the ``scatter``/``gather`` combo. These are used when you have a big list of work you'd like to get done in parallel and you want to farm it off to a bunch of processors. To do @@ -63,51 +68,45 @@ transmition to each CPU). This is called *load balancing*. ARMI has utilities that can help called :py:func:`armi.utils.iterables.chunk` and :py:func:`armi.utils.iterables.flatten`. Given an arbitrary list, ``chunk`` breaks it up into a certain number of chunks and ``unchunk`` does the -opposite to reassemble the original list after processing. Check it out:: +opposite to reassemble the original list after processing. Let's look at an example script:: + """mpi_example.py""" + import random + + from armi import context from armi.utils import iterables - if rank == 0: - # primary. Make data and send it. - workListLoadBalanced = iterables.split(workList, nCpu, padWith=()) - # this list looks like: - # [[v1,v2,v3,v4...], [v5,v6,v7,v8,...], ...] - # And there's one set of values for each processor - myValsToAdd = comm.scatter(workListLoadBalanced, root=0) - # now myValsToAdd is the first entry from the work list, or [v1,v2,v3,v4,...]. + # Generate a list of random number pairs: [[(v1,v2),(v3,v4),...]] + workList = [(random.random(), random.random()) for _i in range(1000)] + + if context.MPI_RANK == 0: + # Primary Process: Split the data and send it to the workers + workListLoadBalanced = iterables.split(workList, context.MPI_SIZE, padWith=()) + myValsToAdd = context.MPI_COMM.scatter(workListLoadBalanced, root=0) else: - # workers. Receive data. Pass a dummy variable to scatter (None) - myValsToAdd = comm.scatter(None, root=0) - # now for the first worker, myValsToAdd==[v5,v6,v7,v8,...] - # and for the second worker, it is [v9,v10,v11,v12,...] and so on. - # Recall that in this example, each vn is a tuple like (randomnum, randomnum) + # Worker Process: Receive data, pass a dummy value to scatter (None) + myValsToAdd = context.MPI_COMM.scatter(None, root=0) - # all processors do their bit of the work + # All processes do their bit of this work (adding) results = [] for num1, num2 in myValsToAdd: results.append(num1 + num2) - # now results is a list of results with one entry per myValsToAdd, or - # [r1,r2,r3,r4,...] - - # all processors call gather to send their results back. it all assembles on the primary processor. - allResultsLoadBalanced = comm.gather(results, root=0) - # So we now have a list of lists of results, like this: - # [[r1,r2,r3,r4,...], [r5,r6,r7,r8,...], ...] + # All processes call gather to send their results back to the root process. + # (The result lists above are simply added to make one list with MPI_SIZE sub-lists.) + allResultsLoadBalanced = context.MPI_COMM.gather(results, root=0) - # primary processor does stuff with the results, like print them out. - if rank == 0: - # first take the individual result lists and reassemble them back into the big list. - # These results correspond exactly to workList from above. All ordering has been preserved. + # Primary Process: Flatten the multiple lists (from each process), and sum them. + if context.MPI_RANK == 0: + # Flatten the MPI_SIZE number of sub lists into one list allResults = iterables.flatten(allResultsLoadBalanced) - # allResults now looks like: [r1,r2,r3,r4,r5,r6,r7,...] - print('The total sum is: {0:10.5f}'.format(sum(allResults))) + # Sum the final list, and print the result + print("The total sum is: {0:10.5f}".format(sum(allResults))) -Remember that this code is running on all processors. So it's just the ``if rank == 0`` statements that differentiate -between the primary and the workers. Try writing this program as a script and submitting it to a cluster via the command -line to see if you really understand what's going on. You will have to add some MPI imports before you can do that -(see :py:mod:`twr_shuffle.py ` in the ARMI code for a major hint!). +Remember that this code is running on all processors. So it's just the ``if rank == 0`` statements that differentiate between the primary and the workers. To really understand what this script is doing, try to run it in parallel and see what it prints out:: + + mpiexec -n 4 python mpi_example.py MPI Communication within ARMI @@ -140,13 +139,15 @@ Example using ``bcast`` Some actions that perform the same task are best distributed through a broadcast. This makes sense for if your are parallelizing code that is a function of an individual assembly, or block. In the following example, the interface simply -creates an ``Action`` and broadcasts it as appropriate.:: +creates an ``Action`` and broadcasts it as appropriate:: + + from armi import context class SomeInterface(interfaces.Interface): def interactEverNode(self, cycle, node): action = BcastAction() - armi.MPI_COMM.bcast(action) + context.MPI_COMM.bcast(action) results = action.invoke(self.o, self.r, self.cs) # allResults is a list of len(self.r) @@ -183,7 +184,7 @@ Example using ``scatter`` ************************* When trying two independent actions at the same time, you can use ``scatter`` to distribute the work. The following example -shows how different operations can be performed in parallel.:: +shows how different operations can be performed in parallel:: class SomeInterface(interfaces.Interface): @@ -223,7 +224,7 @@ A simplified approach ********************* Transferring state to and from a Reactor can be complicated and add a lot of code. An alternative approachis to ensure -that the reactor state is synchronized across all nodes, and then use the reactor instead of raw data.:: +that the reactor state is synchronized across all nodes, and then use the reactor instead of raw data:: class SomeInterface(interfaces.Interface): diff --git a/doc/developer/reports.rst b/doc/developer/reports.rst index 814e8d777..6dda1df78 100644 --- a/doc/developer/reports.rst +++ b/doc/developer/reports.rst @@ -29,7 +29,6 @@ The Hook: getReportContents() +---------------+--------------------------------------------------------------------------------------------------------------------------+ - ReportContent, at its core, is a transient represention of the report itself (until it is fully collected and converted to an html page), so in the call to getReportContents() additions are made to this object. Generally, you would want to break up the added contents into stages within the function getReportContents() as so:: From 09a5e0ab2e369c16917abc71928ca358dd22d874 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 21 Dec 2023 10:42:05 -0800 Subject: [PATCH 110/176] Releasing ARMI v0.3.0! (#1558) --- doc/release/0.2.rst | 21 --------------------- doc/release/0.3.rst | 21 +++++++++++++++++++++ pyproject.toml | 2 +- 3 files changed, 22 insertions(+), 22 deletions(-) create mode 100644 doc/release/0.3.rst diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst index c86167e60..410a13c96 100644 --- a/doc/release/0.2.rst +++ b/doc/release/0.2.rst @@ -2,27 +2,6 @@ ARMI v0.2 Release Notes ======================= -ARMI v0.2.10 -============ -Release Date: TBD - -What's new in ARMI ------------------- -#. Many new references to requirement tests and implementations were added to docstrings. -#. The ``ruff`` linter is now mandatory on all ARMI PRs. -#. The ``_copyInputsHelper()`` gives relative path and not absolute after copy. (`PR#1416 `_) -#. ARMI now mandates ``ruff`` linting. (`PR#1419 `_) -#. Removed all old ARMI requirements, to start the work fresh. (`PR#1438 `_) -#. Downgrading Draft PRs as policy. (`PR#1444 `_) -#. Attempt to set representative block number densities by component if possible. (`PR#1412 `_) -#. Use functools to preserve function attributes when wrapping with codeTiming.timed (`PR#1466 `_) -#. Remove a number of deprecated block, assembly, and core parameters related to a defunct internal plugin -#. TBD - -Bug fixes ---------- -#. TBD - ARMI v0.2.9 =========== Release Date: 2023-09-27 diff --git a/doc/release/0.3.rst b/doc/release/0.3.rst new file mode 100644 index 000000000..070fc4a39 --- /dev/null +++ b/doc/release/0.3.rst @@ -0,0 +1,21 @@ +======================= +ARMI v0.3 Release Notes +======================= + +ARMI v0.3.0 +============ +Release Date: 2023-12-21 + +What's new in ARMI +------------------ +#. The ``_copyInputsHelper()`` gives relative path and not absolute after copy. (`PR#1416 `_) +#. Attempt to set representative block number densities by component if possible. (`PR#1412 `_) +#. Use ``functools`` to preserve function attributes when wrapping with ``codeTiming.timed`` (`PR#1466 `_) +#. Remove a number of deprecated block, assembly, and core parameters related to a defunct internal plugin. + +Quality Work +------------ +#. ARMI now mandates ``ruff`` linting. (`PR#1419 `_) +#. Many new references to requirement tests and implementations were added to docstrings. +#. Removed all old ARMI requirements, to start the work fresh. (`PR#1438 `_) +#. Downgrading Draft PRs as policy. (`PR#1444 `_) diff --git a/pyproject.toml b/pyproject.toml index c84985d7f..1fc314f27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ build-backend = "setuptools.build_meta" [project] name = "armi" -version = "0.2.9" +version = "0.3.0" description = "An open-source nuclear reactor analysis automation framework that helps design teams increase efficiency and quality." license = {file = "LICENSE.md"} requires-python = ">3.6" From 1d6c7ad686f92b61d16012aae0348b8ac3cee39d Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 21 Dec 2023 13:10:11 -0800 Subject: [PATCH 111/176] Improving Documentation (#1564) --- .github/pull_request_template.md | 2 +- doc/developer/documenting.rst | 4 +++- doc/developer/entrypoints.rst | 3 ++- doc/developer/first_time_contributors.rst | 8 ++++---- doc/developer/parallel_coding.rst | 4 ++-- doc/developer/profiling.rst | 1 + doc/developer/reports.rst | 4 +++- doc/developer/tooling.rst | 14 ++++++++------ doc/release/0.3.rst | 22 ++++++++++++++++++++-- doc/user/accessingEntryPoints.rst | 4 ++-- doc/user/assembly_parameters_report.rst | 4 +++- doc/user/block_parameters_report.rst | 4 +++- doc/user/component_parameters_report.rst | 4 +++- doc/user/core_parameters_report.rst | 4 +++- doc/user/radial_and_axial_expansion.rst | 4 ++-- doc/user/reactor_parameters_report.rst | 4 +++- doc/user/user_install.rst | 1 + 17 files changed, 64 insertions(+), 27 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c13378ea6..10fd10092 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -29,5 +29,5 @@ - [ ] The [release notes](https://terrapower.github.io/armi/release/index.html) (location `doc/release/0.X.rst`) are up-to-date with any important changes. - [ ] The [documentation](https://terrapower.github.io/armi/developer/tooling.html#document-it) is still up-to-date in the `doc` folder. -- [ ] No [requirements](https://terrapower.github.io/armi/developer/tooling.html#watch-for-requirements) were altered. +- [ ] If any [requirements](https://terrapower.github.io/armi/developer/tooling.html#watch-for-requirements) were affected, mention it in the [release notes](https://terrapower.github.io/armi/release/index.html). - [ ] The dependencies are still up-to-date in `pyproject.toml`. diff --git a/doc/developer/documenting.rst b/doc/developer/documenting.rst index 5544000e6..d059847a1 100644 --- a/doc/developer/documenting.rst +++ b/doc/developer/documenting.rst @@ -1,5 +1,7 @@ +**************** Documenting ARMI -================ +**************** + ARMI uses the `Sphinx `_ documentation system to compile the web-based documentation from in-code docstrings and hand-created `ReStructedText files `_. diff --git a/doc/developer/entrypoints.rst b/doc/developer/entrypoints.rst index 5e0673487..81987ba34 100644 --- a/doc/developer/entrypoints.rst +++ b/doc/developer/entrypoints.rst @@ -1,5 +1,6 @@ +************ Entry Points -============ +************ **Entry Points** are like the verbs that your App can *do*. The :py:mod:`built-in entry points ` diff --git a/doc/developer/first_time_contributors.rst b/doc/developer/first_time_contributors.rst index f85c80120..8e5cf0713 100644 --- a/doc/developer/first_time_contributors.rst +++ b/doc/developer/first_time_contributors.rst @@ -9,7 +9,7 @@ Although fewer laws apply to open source materials because they are publicly-ava must comply with all applicable laws and regulations. Help Wanted -=========== +----------- There are a lot of places you can get started to help the ARMI project and team: @@ -22,7 +22,7 @@ There are a lot of places you can get started to help the ARMI project and team: Naturally, you can also look at the open `ARMI issues `_ to see what work needs to be done. In particular, check out the `help wanted tickets `_ and `good first issue tickets `_. Testing -======= +------- Any contribution must pass all included unit tests. You will frequently have to fix tests your code changes break. And you should definitely add tests to cover anything @@ -42,7 +42,7 @@ Or the tests can also be run using ``pytest`` directly:: $ pytest -n 4 armi Submitting Changes -================== +------------------ To submit a change to ARMI, you will have to open a Pull Request (PR) on GitHub.com. @@ -64,7 +64,7 @@ See our published documentation for a complete guide to our :doc:`coding standar Also, please check out our (quick) synopsis on :doc:`good commit messages `. Licensing of Tools -================== +------------------ Be careful when including any dependency in ARMI (say in the ``pyproject.toml`` file) not to include anything with a license that superceeds our Apache license. For instance, diff --git a/doc/developer/parallel_coding.rst b/doc/developer/parallel_coding.rst index d6c14520d..4c7f2b050 100644 --- a/doc/developer/parallel_coding.rst +++ b/doc/developer/parallel_coding.rst @@ -1,6 +1,6 @@ -##################### +********************* Parallel Code in ARMI -##################### +********************* ARMI simulations can be parallelized using the `mpi4py `_ module. You should go there and read about collective and point-to-point communication if you want to diff --git a/doc/developer/profiling.rst b/doc/developer/profiling.rst index 8fd2e4af0..02a00717c 100644 --- a/doc/developer/profiling.rst +++ b/doc/developer/profiling.rst @@ -1,6 +1,7 @@ ************** Profiling ARMI ************** + Python in slow, so it's important to profile code to keep it running reasonbly quickly. Using the basic `Python profiler `_ is the best way to get started. Once you have a ``.stats`` file, however, we highly recommend using a visualizer. diff --git a/doc/developer/reports.rst b/doc/developer/reports.rst index 6dda1df78..f3d8cea8d 100644 --- a/doc/developer/reports.rst +++ b/doc/developer/reports.rst @@ -1,5 +1,7 @@ +*************** Reports in ARMI -================ +*************** + .. note:: The resulting report itself is an HTML page with table of contents on the left. ARMI provides the ability to make a variety of plots and tables describing the state of the reactor. diff --git a/doc/developer/tooling.rst b/doc/developer/tooling.rst index 1cc8127f0..8ef61a866 100644 --- a/doc/developer/tooling.rst +++ b/doc/developer/tooling.rst @@ -1,5 +1,6 @@ +************************** Tooling and Infrastructure -========================== +************************** Good Commit Messages -------------------- @@ -87,18 +88,19 @@ Such breadcrumbs will look like: .. code-block:: """ - .. req: This is a requirement breadcrumb. + .. test: This is a requirement test breadcrumb. - .. test: This is a test breadcrumb. - - .. impl: This is an implementation breadcrumb. + .. impl: This is an requirement implementation breadcrumb. """ -If you touch any code that has such a docstring, even in a file, you are going to be +If you touch any code that has such a docstring, even at the top of the file, you are going to be responsible for not breaking that code/functionality. And you will be required to explicitly call out that you touch such a code in your PR. +Your PR reviewer will take an extra look at any PR that touches a requirement test or implementation. +And you will need to add a special release note under the "Changes that Affect Requirements" section header. + Packaging and dependency management ----------------------------------- The process of packaging Python projects and managing their dependencies is somewhat diff --git a/doc/release/0.3.rst b/doc/release/0.3.rst index 070fc4a39..7b85cd262 100644 --- a/doc/release/0.3.rst +++ b/doc/release/0.3.rst @@ -2,12 +2,30 @@ ARMI v0.3 Release Notes ======================= +ARMI v0.3.1 +============ +Release Date: TBD + +What's new in ARMI? +------------------- +#. TBD + +Bug Fixes +--------- +#. TBD + +Changes that Affect Requirements +-------------------------------- + +#. TBD + + ARMI v0.3.0 ============ Release Date: 2023-12-21 -What's new in ARMI ------------------- +What's new in ARMI? +------------------- #. The ``_copyInputsHelper()`` gives relative path and not absolute after copy. (`PR#1416 `_) #. Attempt to set representative block number densities by component if possible. (`PR#1412 `_) #. Use ``functools`` to preserve function attributes when wrapping with ``codeTiming.timed`` (`PR#1466 `_) diff --git a/doc/user/accessingEntryPoints.rst b/doc/user/accessingEntryPoints.rst index ade8b1e33..b58ff8d79 100644 --- a/doc/user/accessingEntryPoints.rst +++ b/doc/user/accessingEntryPoints.rst @@ -1,6 +1,6 @@ +********************** Accessing Entry Points -====================== - +********************** Reports Entry Point ------------------- diff --git a/doc/user/assembly_parameters_report.rst b/doc/user/assembly_parameters_report.rst index efa998fc9..b51cc0964 100644 --- a/doc/user/assembly_parameters_report.rst +++ b/doc/user/assembly_parameters_report.rst @@ -1,5 +1,7 @@ +******************* Assembly Parameters -=================== +******************* + This document lists all of the Assembly Parameters that are provided by the ARMI Framework. .. exec:: diff --git a/doc/user/block_parameters_report.rst b/doc/user/block_parameters_report.rst index 20549d892..38ff2a77b 100644 --- a/doc/user/block_parameters_report.rst +++ b/doc/user/block_parameters_report.rst @@ -1,5 +1,7 @@ +**************** Block Parameters -================ +**************** + This document lists all of the Block Parameters that are provided by the ARMI Framework. .. exec:: diff --git a/doc/user/component_parameters_report.rst b/doc/user/component_parameters_report.rst index 62ef2b142..463d4f62d 100644 --- a/doc/user/component_parameters_report.rst +++ b/doc/user/component_parameters_report.rst @@ -1,5 +1,7 @@ +******************** Component Parameters -==================== +******************** + This document lists all of the Component Parameters that are provided by the ARMI Framework. .. exec:: diff --git a/doc/user/core_parameters_report.rst b/doc/user/core_parameters_report.rst index 9f5633bf4..2f09a3650 100644 --- a/doc/user/core_parameters_report.rst +++ b/doc/user/core_parameters_report.rst @@ -1,5 +1,7 @@ +*************** Core Parameters -=============== +*************** + This document lists all of the Core Parameters that are provided by the ARMI Framework. .. exec:: diff --git a/doc/user/radial_and_axial_expansion.rst b/doc/user/radial_and_axial_expansion.rst index aa3498f6f..8d9cb707f 100644 --- a/doc/user/radial_and_axial_expansion.rst +++ b/doc/user/radial_and_axial_expansion.rst @@ -1,6 +1,6 @@ -******************************************* +****************************************** Radial and Axial Expansion and Contraction -******************************************* +****************************************** ARMI natively supports linear expansion in both the radial and axial dimensions. These expansion types function independently of one another and each have their own set of underlying assumptions and use-cases. The remainder of this section is described as follows: in Section :ref:`thermalExpansion` the methodology used for thermal expansion within ARMI is described; in Sections :ref:`radialExpansion` and :ref:`axialExpansion`, we describe the design, limitations, and intended functionality of radial and axial expansion, respectively. diff --git a/doc/user/reactor_parameters_report.rst b/doc/user/reactor_parameters_report.rst index 1f3ab6980..1dd8ed062 100644 --- a/doc/user/reactor_parameters_report.rst +++ b/doc/user/reactor_parameters_report.rst @@ -1,5 +1,7 @@ +****************** Reactor Parameters -================== +****************** + This document lists all of the Reactor Parameters that are provided by the ARMI Framework. .. exec:: diff --git a/doc/user/user_install.rst b/doc/user/user_install.rst index 4313d6528..269d9df92 100644 --- a/doc/user/user_install.rst +++ b/doc/user/user_install.rst @@ -1,6 +1,7 @@ ************ Installation ************ + This section will guide you through installing the ARMI Framework on your machine. Prerequisites From dc78053cec3017cd7e6b7c2a73b0099454534918 Mon Sep 17 00:00:00 2001 From: Arrielle Opotowsky Date: Wed, 27 Dec 2023 13:28:38 -0600 Subject: [PATCH 112/176] Fixing CLI description printout (#1566) --- armi/cli/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/armi/cli/__init__.py b/armi/cli/__init__.py index 19a7384c7..7b2f13bdd 100644 --- a/armi/cli/__init__.py +++ b/armi/cli/__init__.py @@ -101,9 +101,9 @@ def print_help(self, file=None): class ArmiCLI: """ ARMI CLI -- The main entry point into ARMI. There are various commands - available, to get help for the individual commands, run again with - ` --help`. Generically, the CLI implements functions that already - exists within ARMI. + available. To get help for the individual commands, run again with + ` --help`. Typically, the CLI implements functions that already + exist within ARMI. .. impl:: The basic ARMI CLI, for running a simulation. :id: I_ARMI_CLI_CS @@ -128,7 +128,7 @@ def __init__(self): parser = ArmiParser( prog=context.APP_NAME, - description=self.__doc__, + description=self.__doc__.split(".. impl")[0], usage="%(prog)s [-h] [-l | command [args]]", ) From 78042b22b9f3a05d4f25244d15711824b7b7b423 Mon Sep 17 00:00:00 2001 From: Arrielle Opotowsky Date: Tue, 2 Jan 2024 11:42:46 -0600 Subject: [PATCH 113/176] Cleaning up test crumbs (#1567) --- armi/bookkeeping/db/tests/test_databaseInterface.py | 6 ++++++ armi/bookkeeping/report/tests/test_report.py | 10 ++++++++++ armi/nuclearDataIO/tests/test_xsLibraries.py | 9 +++++---- armi/physics/fuelCycle/tests/test_fuelHandlers.py | 8 ++++++++ armi/reactor/tests/test_reactors.py | 4 +++- armi/tests/test_lwrInputs.py | 5 +++-- armi/tests/test_notebooks.py | 3 +++ armi/utils/tests/test_directoryChangers.py | 3 +-- pyproject.toml | 5 +++-- 9 files changed, 42 insertions(+), 11 deletions(-) diff --git a/armi/bookkeeping/db/tests/test_databaseInterface.py b/armi/bookkeeping/db/tests/test_databaseInterface.py index 17ae0ecdb..342f5345d 100644 --- a/armi/bookkeeping/db/tests/test_databaseInterface.py +++ b/armi/bookkeeping/db/tests/test_databaseInterface.py @@ -27,6 +27,7 @@ from armi.bookkeeping.db.database3 import Database3 from armi.bookkeeping.db.databaseInterface import DatabaseInterface from armi.cases import case +from armi.context import PROJECT_ROOT from armi.physics.neutronics.settings import CONF_LOADING_FILE from armi.reactor import grids from armi.reactor.flags import Flags @@ -92,6 +93,11 @@ def tearDown(self): self.db.close() self.stateRetainer.__exit__() self.td.__exit__(None, None, None) + # test_interactBOL leaves behind some dirt (accessible after db close) that the + # TempDirChanger is not catching + bolDirt = os.path.join(PROJECT_ROOT, "armiRun.h5") + if os.path.exists(bolDirt): + os.remove(bolDirt) def test_interactEveryNodeReturn(self): """Test that the DB is NOT written to if cs["tightCoupling"] = True.""" diff --git a/armi/bookkeeping/report/tests/test_report.py b/armi/bookkeeping/report/tests/test_report.py index 577b97276..73f0f5a5f 100644 --- a/armi/bookkeeping/report/tests/test_report.py +++ b/armi/bookkeeping/report/tests/test_report.py @@ -32,6 +32,7 @@ ) from armi.reactor.tests.test_reactors import loadTestReactor from armi.tests import mockRunLogs +from armi.utils.directoryChangers import TemporaryDirectoryChanger class TestReport(unittest.TestCase): @@ -158,6 +159,15 @@ def test_writeWelcomeHeaders(self): class TestReportInterface(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.td = TemporaryDirectoryChanger() + cls.td.__enter__() + + @classmethod + def tearDownClass(cls): + cls.td.__exit__(None, None, None) + def test_printReports(self): """Testing printReports method.""" repInt = reportInterface.ReportInterface(None, None) diff --git a/armi/nuclearDataIO/tests/test_xsLibraries.py b/armi/nuclearDataIO/tests/test_xsLibraries.py index 29c843619..35d7f6066 100644 --- a/armi/nuclearDataIO/tests/test_xsLibraries.py +++ b/armi/nuclearDataIO/tests/test_xsLibraries.py @@ -54,7 +54,7 @@ UFG_FLUX_EDIT = os.path.join(FIXTURE_DIR, "mc2v3-AA.flux_ufg") -class TempFileMixin: +class TempFileMixin(unittest.TestCase): """really a test case.""" def setUp(self): @@ -67,12 +67,12 @@ def tearDown(self): @property def testFileName(self): return os.path.join( - THIS_DIR, + self.td.destination, "{}-{}.nucdata".format(self.__class__.__name__, self._testMethodName), ) -class TestXSLibrary(unittest.TestCase, TempFileMixin): +class TestXSLibrary(TempFileMixin): @classmethod def setUpClass(cls): cls.isotxsAA = isotxs.readBinary(ISOTXS_AA) @@ -289,7 +289,7 @@ def assert_contains_only(self, container, shouldBeThere, shouldNotBeThere): # NOTE: This is just a base class, so it isn't run directly. -class TestXSlibraryMerging(unittest.TestCase, TempFileMixin): +class TestXSlibraryMerging(TempFileMixin): """A shared class that defines tests that should be true for all IsotxsLibrary merging.""" @classmethod @@ -311,6 +311,7 @@ def tearDownClass(cls): del cls.libLumped def setUp(self): + TempFileMixin.setUp(self) # load a library that is in the ARMI tree. This should # be a small library with LFPs, Actinides, structure, and coolant for attrName, path in [ diff --git a/armi/physics/fuelCycle/tests/test_fuelHandlers.py b/armi/physics/fuelCycle/tests/test_fuelHandlers.py index d8026c8b4..c905d28d7 100644 --- a/armi/physics/fuelCycle/tests/test_fuelHandlers.py +++ b/armi/physics/fuelCycle/tests/test_fuelHandlers.py @@ -19,6 +19,7 @@ """ import collections import copy +import os import unittest from unittest.mock import patch @@ -490,6 +491,13 @@ def test_repeatShuffles(self): for a in self.r.sfp.getChildren(): self.assertEqual(a.getLocation(), "SFP") + # Do some cleanup, since the fuelHandler Interface has code that gets + # around the TempDirectoryChanger + os.remove("armiRun2-SHUFFLES.txt") + os.remove("armiRun2.shuffles_0.png") + os.remove("armiRun2.shuffles_1.png") + os.remove("armiRun2.shuffles_2.png") + def test_readMoves(self): """ Depends on the ``shuffleLogic`` created by ``repeatShuffles``. diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index c0851cdb1..72b1daf17 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -44,6 +44,7 @@ from armi.tests import ARMI_RUN_PATH, mockRunLogs, TEST_ROOT from armi.utils import directoryChangers +THIS_DIR = os.path.dirname(__file__) TEST_REACTOR = None # pickled string of test reactor (for fast caching) @@ -862,7 +863,8 @@ class MockLib: for b in self.r.core.getBlocks(): b.p.mgFlux = range(5) b.p.adjMgFlux = range(5) - self.r.core.saveAllFlux() + with directoryChangers.TemporaryDirectoryChanger(root=THIS_DIR): + self.r.core.saveAllFlux() def test_getFluxVector(self): class MockLib: diff --git a/armi/tests/test_lwrInputs.py b/armi/tests/test_lwrInputs.py index 1da99bf03..93c10a111 100644 --- a/armi/tests/test_lwrInputs.py +++ b/armi/tests/test_lwrInputs.py @@ -90,8 +90,9 @@ def loadLocs(o, locs): o = armi_init(fName=TEST_INPUT_TITLE + ".yaml") locsInput, locsDB = {}, {} loadLocs(o, locsInput) - o.operate() - o2 = db.loadOperator(TEST_INPUT_TITLE + ".h5", 0, 0) + with directoryChangers.TemporaryDirectoryChanger(): + o.operate() + o2 = db.loadOperator(TEST_INPUT_TITLE + ".h5", 0, 0) loadLocs(o2, locsDB) for indices, coordsInput in sorted(locsInput.items()): diff --git a/armi/tests/test_notebooks.py b/armi/tests/test_notebooks.py index b4eb4e31a..e1ce2848a 100644 --- a/armi/tests/test_notebooks.py +++ b/armi/tests/test_notebooks.py @@ -36,6 +36,9 @@ def test_runParamSweep(self): def test_runDataModel(self): runNotebook(os.path.join(TUTORIALS, "data_model.ipynb")) + # Do some cleanup because some code run in the notebook doesn't honor the + # TempDirectoryChanger + os.remove(os.path.join(TUTORIALS, "anl-afci-177.h5")) def runNotebook(filename): diff --git a/armi/utils/tests/test_directoryChangers.py b/armi/utils/tests/test_directoryChangers.py index 5a42b1f95..30aec94fc 100644 --- a/armi/utils/tests/test_directoryChangers.py +++ b/armi/utils/tests/test_directoryChangers.py @@ -143,8 +143,7 @@ def f(name): self.assertTrue(os.path.exists(os.path.join("temp", f("file1.txt")))) self.assertTrue(os.path.exists(os.path.join("temp", f("file2.txt")))) - os.remove(os.path.join("temp", f("file1.txt"))) - os.remove(os.path.join("temp", f("file2.txt"))) + shutil.rmtree("temp") def test_file_retrieval_missing_file(self): """Tests that the directory changer still returns a subset of files even if all do not exist.""" diff --git a/pyproject.toml b/pyproject.toml index 1fc314f27..6b68f14b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +81,6 @@ test = [ "black==22.6.0", "docutils", "ipykernel", - "jupyter-contrib-nbextensions", "jupyter_client", "nbconvert", "pytest", @@ -114,7 +113,9 @@ docs = [ "ipykernel==6.25.1", # iPython kernel to run Jupyter notebooks "pylint==2.17.5", # Generates UML diagrams "Jinja2==3.0.3", # Used in numpydoc and nbconvert - "sphinxcontrib-jquery==4.1", # Handle missing jquery errors + "sphinxcontrib-jquery==4.1", # Handle missing jquery errors + "jupyter-contrib-nbextensions", + "lxml<5.0.0", # Needed because the dep above is no longer an active project ] [project.scripts] From 24e36fe447914b1824f7bca01fdf0a648602b357 Mon Sep 17 00:00:00 2001 From: Ashlita6 <124939675+Ashlita6@users.noreply.github.com> Date: Tue, 2 Jan 2024 11:23:58 -0800 Subject: [PATCH 114/176] Remove Parent tests/impls/reqs (#1565) --- armi/cases/tests/test_cases.py | 4 ++++ armi/reactor/composites.py | 8 ++++---- armi/reactor/converters/axialExpansionChanger.py | 4 ---- armi/reactor/reactors.py | 4 ---- armi/reactor/tests/test_components.py | 16 ++++++++++++++-- armi/reactor/tests/test_composites.py | 8 ++++---- armi/reactor/tests/test_reactors.py | 4 ---- armi/tests/test_plugins.py | 4 ++++ 8 files changed, 30 insertions(+), 22 deletions(-) diff --git a/armi/cases/tests/test_cases.py b/armi/cases/tests/test_cases.py index a62ba2441..27c0cac0a 100644 --- a/armi/cases/tests/test_cases.py +++ b/armi/cases/tests/test_cases.py @@ -205,6 +205,10 @@ def test_run(self): .. test:: There is a generic mechanism to allow simulation runs. :id: T_ARMI_CASE :tests: R_ARMI_CASE + + .. test:: Test case settings object is created, settings can be edited, and case can run. + :id: T_ARMI_SETTING + :tests: R_ARMI_SETTING """ with directoryChangers.TemporaryDirectoryChanger(): cs = settings.Settings(ARMI_RUN_PATH) diff --git a/armi/reactor/composites.py b/armi/reactor/composites.py index 84f3d42f1..763b5cb9d 100644 --- a/armi/reactor/composites.py +++ b/armi/reactor/composites.py @@ -2669,7 +2669,7 @@ class Composite(ArmiObject): reactors. .. impl:: Composites are a physical part of the reactor in a hierarchical data model. - :id: I_ARMI_CMP + :id: I_ARMI_CMP0 :implements: R_ARMI_CMP """ @@ -2776,9 +2776,9 @@ def getChildren( """ Return the children objects of this composite. - .. impl:: Composites have children, in the hierarchical data model. - :id: I_ARMI_CMP_CHILDREN - :implements: R_ARMI_CMP_CHILDREN + .. impl:: Composites have children in the hierarchical data model. + :id: I_ARMI_CMP1 + :implements: R_ARMI_CMP Parameters ---------- diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index 3b5f391bc..60c1cde49 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -110,10 +110,6 @@ class AxialExpansionChanger: """ Axially expand or contract assemblies or an entire core. - .. impl:: Performing axial expansion on solid components within a compatible ARMI assembly. - :id: I_ARMI_AXIAL_EXP - :implements: R_ARMI_AXIAL_EXP - .. impl:: Preserve the total height of an ARMI assembly, during expansion. :id: I_ARMI_ASSEM_HEIGHT_PRES :implements: R_ARMI_ASSEM_HEIGHT_PRES diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index 068f9578c..31efd23f8 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -74,10 +74,6 @@ class Reactor(composites.Composite): .. impl:: The user-specified reactor. :id: I_ARMI_R :implements: R_ARMI_R - - .. impl:: The reactor includes a core and a spent fuel pool. - :id: I_ARMI_R_CHILDREN - :implements: R_ARMI_R_CHILDREN """ pDefs = reactorParameters.defineReactorParameters() diff --git a/armi/reactor/tests/test_components.py b/armi/reactor/tests/test_components.py index 429f90a3b..63ed62512 100644 --- a/armi/reactor/tests/test_components.py +++ b/armi/reactor/tests/test_components.py @@ -75,6 +75,12 @@ def getCircleFuelDict(self): ) def test_factory(self): + """Creating and verifying void and fuel components. + + .. test:: Example void and fuel components are initialized. + :id: T_ARMI_COMP_DEF0 + :tests: R_ARMI_COMP_DEF + """ voidAttrs = self.getCircleVoidDict() voidComp = components.factory(voidAttrs.pop("shape"), [], voidAttrs) fuelAttrs = self.getCircleFuelDict() @@ -85,6 +91,12 @@ def test_factory(self): self.assertIsInstance(fuelComp.material, materials.UZr) def test_componentInitializationAndDuplication(self): + """Initialize and duplicate a component, veifying the parameters. + + .. test:: Verify the parameters of an initialized component. + :id: T_ARMI_COMP_DEF1 + :tests: R_ARMI_COMP_DEF + """ # populate the class/signature dict, and create a basis attrs attrs = self.getCircleVoidDict() del attrs["shape"] @@ -186,7 +198,7 @@ def test_setNumberDensity(self): """Test setting a single number density. .. test:: Users can set Component number density. - :id: T_ARMI_COMP_NUCLIDE_FRASCS0 + :id: T_ARMI_COMP_NUCLIDE_FRACS0 :tests: R_ARMI_COMP_NUCLIDE_FRACS """ component = self.component @@ -198,7 +210,7 @@ def test_setNumberDensities(self): """Test setting multiple number densities. .. test:: Users can set Component number densities. - :id: T_ARMI_COMP_NUCLIDE_FRASCS1 + :id: T_ARMI_COMP_NUCLIDE_FRACS1 :tests: R_ARMI_COMP_NUCLIDE_FRACS """ component = self.component diff --git a/armi/reactor/tests/test_composites.py b/armi/reactor/tests/test_composites.py index 2d9308623..bd5c65d6b 100644 --- a/armi/reactor/tests/test_composites.py +++ b/armi/reactor/tests/test_composites.py @@ -114,8 +114,8 @@ def test_composite(self): """Test basic Composite things. .. test:: Composites are part of a hierarchical model. - :id: T_ARMI_CMP_CHILDREN0 - :tests: R_ARMI_CMP_CHILDREN + :id: T_ARMI_CMP0 + :tests: R_ARMI_CMP """ container = self.container @@ -133,8 +133,8 @@ def test_getChildren(self): """Test the get children method. .. test:: Composites are part of a hierarchical model. - :id: T_ARMI_CMP_CHILDREN1 - :tests: R_ARMI_CMP_CHILDREN + :id: T_ARMI_CMP1 + :tests: R_ARMI_CMP """ # There are 5 leaves and 1 composite in container. The composite has one leaf. firstGen = self.container.getChildren() diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 72b1daf17..085525bfd 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -244,10 +244,6 @@ def test_coreSfp(self): .. test:: The reactor object is a composite. :id: T_ARMI_R :tests: R_ARMI_R - - .. test:: The reactor object includes a core and an SFP. - :id: T_ARMI_R_CHILDREN - :tests: R_ARMI_R_CHILDREN """ self.assertTrue(isinstance(self.r.core, reactors.Core)) self.assertTrue(isinstance(self.r.sfp, SpentFuelPool)) diff --git a/armi/tests/test_plugins.py b/armi/tests/test_plugins.py index 2ee25e866..d63b4957f 100644 --- a/armi/tests/test_plugins.py +++ b/armi/tests/test_plugins.py @@ -63,6 +63,10 @@ def test_defineFlags(self): .. test:: Define a new, unique flag through the plugin pathway. :id: T_ARMI_FLAG_EXTEND1 :tests: R_ARMI_FLAG_EXTEND + + .. test:: Load a plugin into an app and show it is loaded. + :id: T_ARMI_PLUGIN_REGISTER + :tests: R_ARMI_PLUGIN """ app = getApp() From 6e8088d60369ae706236826836bcbd44ccbb4465 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Wed, 3 Jan 2024 15:45:06 -0800 Subject: [PATCH 115/176] Cleaning up ARMI Docs (#1569) This commit does three things: - The User and Developer manuals are changed from being multiple RST files to just one. - A TON of headers had to be fixed, because they were the wrong level. - Some :doc: statements needed to be replaced with :ref: --- doc/developer/documenting.rst | 16 +- doc/developer/first_time_contributors.rst | 16 +- doc/developer/guide.rst | 13 +- doc/developer/making_armi_based_apps.rst | 13 +- doc/developer/parallel_coding.rst | 12 +- doc/developer/reports.rst | 25 +- doc/developer/standards_and_practices.rst | 2 + doc/developer/tooling.rst | 28 +- doc/release/0.1.rst | 4 +- doc/release/0.2.rst | 4 +- doc/release/0.3.rst | 4 +- doc/release/index.rst | 3 +- doc/tutorials/index.rst | 2 + doc/tutorials/making_your_first_app.rst | 8 +- doc/tutorials/walkthrough_inputs.rst | 8 +- doc/tutorials/walkthrough_lwr_inputs.rst | 4 +- doc/user/accessingEntryPoints.rst | 2 +- doc/user/assembly_parameters_report.rst | 2 + doc/user/block_parameters_report.rst | 2 + doc/user/component_parameters_report.rst | 2 + doc/user/core_parameters_report.rst | 2 + doc/user/index.rst | 4 +- .../{inputs/blueprints.rst => inputs.rst} | 682 +++++++++++++++++- doc/user/inputs/fuel_management.rst | 194 ----- doc/user/inputs/index.rst | 39 - doc/user/inputs/settings.rst | 387 ---------- doc/user/inputs/settings_report.rst | 30 - .../looseCouplingIllustration.dot | 0 doc/user/manual_data_access.rst | 16 +- .../{outputs/database.rst => outputs.rst} | 82 ++- doc/user/outputs/index.rst | 31 - doc/user/outputs/stdout.rst | 43 -- doc/user/physics_coupling.rst | 12 +- doc/user/radial_and_axial_expansion.rst | 6 +- doc/user/reactor_parameters_report.rst | 2 + .../tightCouplingIllustration.dot | 0 doc/user/user_install.rst | 18 +- 37 files changed, 855 insertions(+), 863 deletions(-) rename doc/user/{inputs/blueprints.rst => inputs.rst} (56%) delete mode 100644 doc/user/inputs/fuel_management.rst delete mode 100644 doc/user/inputs/index.rst delete mode 100644 doc/user/inputs/settings.rst delete mode 100644 doc/user/inputs/settings_report.rst rename doc/user/{inputs => }/looseCouplingIllustration.dot (100%) rename doc/user/{outputs/database.rst => outputs.rst} (71%) delete mode 100644 doc/user/outputs/index.rst delete mode 100644 doc/user/outputs/stdout.rst rename doc/user/{inputs => }/tightCouplingIllustration.dot (100%) diff --git a/doc/developer/documenting.rst b/doc/developer/documenting.rst index d059847a1..224ed16c1 100644 --- a/doc/developer/documenting.rst +++ b/doc/developer/documenting.rst @@ -1,3 +1,5 @@ +.. _armi-docing: + **************** Documenting ARMI **************** @@ -19,7 +21,7 @@ We use some special Sphinx plugins that run the tutorial jupyter notebooks durin build with the most up-to-date code. Building the documentation --------------------------- +========================== Before building documentation, ensure that you have installed the test requirements into your ARMI virtual environment with:: @@ -58,11 +60,11 @@ files to a clone of the `documentation repository rsync -ahv --delete _build/html/ path/to/terrapower.github.io/armi Documentation for ARMI plugins ------------------------------- +============================== The following subsections apply to documentation for ARMI plugins. Linking to ARMI documentation from plugins -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +------------------------------------------ ARMI plugin documentation can feature rich hyperlinks to the ARMI API documentation with the help of the `intersphinx Sphinx plugin `_. The @@ -81,7 +83,7 @@ Now you can link to the ARMI documentation with links like:: Automatically building apidocs of namespace packages -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +---------------------------------------------------- Activating the ``"sphinxcontrib.apidoc",`` `Sphinx plugin `_ enables plugin API documentation to be built with the standard ``make html`` Sphinx workflow. If @@ -90,15 +92,15 @@ your ARMI plugin is a namespace package, the following extra config is required: apidoc_extra_args = ["--implicit-namespaces"] Updating the Gallery --------------------- -The :doc:`ARMI example gallery ` is a great way to quickly +==================== +The :ref:`sphx_glr_gallery` is a great way to quickly highlight neat features and uses of ARMI. To add a new item to the gallery, add your example code (including the required docstring) to the ``doc/gallery-src`` folder in the ARMI source tree. The example will be added to the gallery during the next documentation build. Using Jupyter notebooks ------------------------ +======================= For interactive tutorials, it's convenient to build actual Jupyter notebooks and commit them to the documentation to be rendered by Sphinx using the nbsphinx plugin. When this is done, notebooks without any output should be committed to the repository diff --git a/doc/developer/first_time_contributors.rst b/doc/developer/first_time_contributors.rst index 8e5cf0713..e4130048b 100644 --- a/doc/developer/first_time_contributors.rst +++ b/doc/developer/first_time_contributors.rst @@ -9,11 +9,11 @@ Although fewer laws apply to open source materials because they are publicly-ava must comply with all applicable laws and regulations. Help Wanted ------------ +=========== There are a lot of places you can get started to help the ARMI project and team: -* Better :doc:`documentation ` +* Better :ref:`armi-docing` * Better test coverage * Many more type annotations are desired. Type issues cause lots of bugs. * Targeted speedups (e.g. informed by a profiler) @@ -22,7 +22,7 @@ There are a lot of places you can get started to help the ARMI project and team: Naturally, you can also look at the open `ARMI issues `_ to see what work needs to be done. In particular, check out the `help wanted tickets `_ and `good first issue tickets `_. Testing -------- +======= Any contribution must pass all included unit tests. You will frequently have to fix tests your code changes break. And you should definitely add tests to cover anything @@ -42,7 +42,7 @@ Or the tests can also be run using ``pytest`` directly:: $ pytest -n 4 armi Submitting Changes ------------------- +================== To submit a change to ARMI, you will have to open a Pull Request (PR) on GitHub.com. @@ -53,18 +53,18 @@ The process for opening a PR against ARMI goes something like this: 3. Make your code changes to your new branch 4. Submit a Pull Request against `ARMIs main branch `_ a. See `GitHubs general guidance on Pull Requests `_ - b. See :doc:`ARMIs specific guidance ` on what makes a "good" Pull Request. + b. See ARMIs specific guidance on what makes a "good" Pull Request: :ref:`armi-tooling`. 5. Actively engage with your PR reviewer's questions and comments. > Note that a bot will require that you sign our `Contributor License Agreement `_ before we can accept a pull request from you. -See our published documentation for a complete guide to our :doc:`coding standards and practices `. +See our published documentation for a complete guide to our coding standards and practices: :ref:`armi-stds`. -Also, please check out our (quick) synopsis on :doc:`good commit messages `. +Also, please check out our (quick) synopsis on good commit messages: :ref:`armi-tooling`. Licensing of Tools ------------------- +================== Be careful when including any dependency in ARMI (say in the ``pyproject.toml`` file) not to include anything with a license that superceeds our Apache license. For instance, diff --git a/doc/developer/guide.rst b/doc/developer/guide.rst index b513b8c6f..0d256a2b0 100644 --- a/doc/developer/guide.rst +++ b/doc/developer/guide.rst @@ -5,9 +5,8 @@ Framework Architecture Here we will discuss some big-picture elements of the ARMI architecture. Throughout, links to the API docs will lead to additional details. ------------------ The Reactor Model ------------------ +================= The :py:mod:`~armi.reactor` package is the central representation of a nuclear reactor in ARMI. All modules can be expected to want access to some element of the state data @@ -41,7 +40,7 @@ collection of parameters detailing considerations of how the reactor has progres through time to any given point. This information also constitutes the majority of what gets written to the database for evaluation and/or follow-on analysis. -Review the :doc:`/tutorials/data_model` section for examples +Review the data model :ref:`armi-tutorials` section for examples exploring a populated instance of the **Reactor** model. Finding objects in a model @@ -113,9 +112,8 @@ defined. During a run, they can be used to create new instances of reactor model such as when a new assembly is fabricated during a fuel management operation in a later cycle. ---------- Operators ---------- +========= Operators conduct the execution sequence of an ARMI run. They basically contain the main loop. When any operator is instantiated, several actions occur: @@ -265,13 +263,12 @@ as deemed necessary to have the interfaces work properly. To use interfaces in parallel, please refer to :py:mod:`armi.mpiActions`. -------- Plugins -------- +======= Plugins are higher-level objects that can bring in one or more Interfaces, settings definitions, parameters, validations, etc. They are documented in -:doc:`/developer/making_armi_based_apps` and :py:mod:`armi.plugins`. +:ref:`armi-app-making` and :py:mod:`armi.plugins`. Entry Points diff --git a/doc/developer/making_armi_based_apps.rst b/doc/developer/making_armi_based_apps.rst index 0c3c2b6e7..8af292b1f 100644 --- a/doc/developer/making_armi_based_apps.rst +++ b/doc/developer/making_armi_based_apps.rst @@ -1,3 +1,5 @@ +.. _armi-app-making: + ********************** Making ARMI-based Apps ********************** @@ -8,8 +10,7 @@ interfaces to automate your work is the next step to unlocking ARMI's potential. .. admonition:: Heads up - A full :doc:`tutorial on making an ARMI-based app is here - `. + A full tutorial on :ref:`armi-make-first-app` is here. To really make ARMI your own, you will need to understand a couple of concepts that enable developers to adapt and extend ARMI to their liking: @@ -32,9 +33,8 @@ enable developers to adapt and extend ARMI to their liking: Both of these concepts are discussed in depth below. ------------- ARMI Plugins ------------- +============ An ARMI Plugin is the primary means by which a developer or qualified analyst can go about building specific capability on top of the ARMI Framework. Even some of the @@ -45,6 +45,7 @@ getting started to get an idea of what is available. Some implementation details --------------------------- + One can just monkey-see-monkey-do their own plugins without fully understanding the following. However, having a deeper understanding of what is going on may be useful. Feel free to skip this section. @@ -101,9 +102,9 @@ some guidance. Once you have a plugin together, continue reading to see how to plug it into the ARMI Framework as part of an Application. ------------------------ ARMI-Based Applications ------------------------ +======================= + On its own, ARMI doesn't *do* much. Plugins provide more functionality, but even they aren't particularly useful on their own either. The magic really happens when you collect a handful of Plugins and plug them into the ARMI Framework. Such a collection is diff --git a/doc/developer/parallel_coding.rst b/doc/developer/parallel_coding.rst index 4c7f2b050..154ad9f2c 100644 --- a/doc/developer/parallel_coding.rst +++ b/doc/developer/parallel_coding.rst @@ -17,7 +17,7 @@ these instructions, you can have your code working in parallel in no time. In AR you need them to in parallel. MPI communication crash course ------------------------------- +============================== First, let's do a crash course in MPI communications. We'll only discuss a few important ideas, you can read about more on the ``mpi4py`` web page. The first method of communication is called the ``broadcast``, which happens when the primary processor sends information to all others. An example of this would be when you want to @@ -110,8 +110,8 @@ Remember that this code is running on all processors. So it's just the ``if rank MPI Communication within ARMI ------------------------------ -Now that you understand the basics, here's how you should get your :doc:`code interfaces ` +============================= +Now that you understand the basics, here's how you should get your :py:class:`armi.interfaces.Interface` to run things in parallel in ARMI. You don't have to worry too much about the ranks, etc. because ARMI will set that up for you. Basically, @@ -135,7 +135,7 @@ mechanisms that can help you get the data back to the primary reactor. Example using ``bcast`` -*********************** +----------------------- Some actions that perform the same task are best distributed through a broadcast. This makes sense for if your are parallelizing code that is a function of an individual assembly, or block. In the following example, the interface simply @@ -181,7 +181,7 @@ creates an ``Action`` and broadcasts it as appropriate:: Example using ``scatter`` -************************* +------------------------- When trying two independent actions at the same time, you can use ``scatter`` to distribute the work. The following example shows how different operations can be performed in parallel:: @@ -221,7 +221,7 @@ shows how different operations can be performed in parallel:: A simplified approach -********************* +--------------------- Transferring state to and from a Reactor can be complicated and add a lot of code. An alternative approachis to ensure that the reactor state is synchronized across all nodes, and then use the reactor instead of raw data:: diff --git a/doc/developer/reports.rst b/doc/developer/reports.rst index f3d8cea8d..17b666708 100644 --- a/doc/developer/reports.rst +++ b/doc/developer/reports.rst @@ -14,7 +14,7 @@ from a specific plugin. Currently it is implemented in the bookkeeping and neutronics plugin. The Hook: getReportContents() ------------------------------ +============================= getReportContents takes in 5 arguments (r, cs, report, stage, blueprint) @@ -53,7 +53,7 @@ so one can imagine the functionality of the below to allow for ``Standard`` addi ReportContent acts as a dicionary of ``Section``'s behind the scenes and the further description of these objects will be found in the following topics. What is ReportContent? ----------------------- +====================== At the start of any report creation, creation of the ReportContent object is key. ReportContent when created, needs the name of the reactor for the title of the report. @@ -83,7 +83,7 @@ A major component of the ReportContent class is the function to ``writeReports() Overall, the important functionality examples for ``ReportContent`` additions are summarized below. Sections --------- +======== The first level of ``ReportContent``'s is made up of ``Section``'s. ``Section``'s have a ``title``, and themselves a dictionary of contents (``.childContents``), but again the ability to just directly access a sections children like ``report[Comprehensive][Setting]`` exists, as long as the section already exists, (if not, a key error persists, and so it is safer to do ``report[Comprehensive].get(Setting, Table(...)))``, where ``Table`` would be the default. @@ -117,7 +117,7 @@ It is also possible to do the following through dictionary access for the same r Tables ------- +====== Making sure a ``Table`` isn't already created is important. Due to the repeated call to ``getReportContents()`` at different cycles/nodes of the reactor life cycle, some sections may have already been called before, and we want to be careful about not overwriting a ``Table``/``TimeSeries``. (most ``Image``'s may only be called at a single time and not dependent on multiple plugins, so those cases have less to worry about at this time) @@ -150,7 +150,6 @@ Suppose in Bookkeeping a ``Table`` is accessed with the following code:: - Similarily that same ``Table`` is accessed within Neutronics for additional settings additions:: >>> section = report[newReportUtils.COMPREHENSIVE_REPORT] @@ -182,9 +181,8 @@ The result (with some additional Bookkeeping additions) is outlined in this imag :align: center - Images ------- +====== Images may generally be things to add at stage = Beg, or stage = End. (For example, a core map at BOL would be inserted at stage = Beg) Images require a ``caption`` and a ``filename`` and have an optional ``title`` argument. (They would also have a call to another function before hand to create the image file (for example)) @@ -210,10 +208,9 @@ In this case, Block Diagrams is the Section Title, and it is expandable, for eas TimeSeries ----------- -This is where information for later graphing is collected. The TimeSeries contains many elements. A ``title`` a ``rname`` (reactor name), ``labels`` list, ``yaxis`` title, and ``filename``. - +========== +This is where information for later graphing is collected. The TimeSeries contains many elements. A ``title`` a ``rname`` (reactor name), ``labels`` list, ``yaxis`` title, and ``filename``. Like ``Table``, these objects need to have a check on whether they already exist. In this case, you could just check and create the object when ``stage`` is set to Begin (and then when ``stage`` is Standard always know it exists to add content to), but for good measure, you may also just check if the Plot already exists in the Section, and if not, add it. @@ -237,22 +234,20 @@ Here is code for adding to a K-effective plot:: labels[0], r.p.time, r.core.p.keff, r.core.p.keffUnc >>> ) - Here, only one label exists, so we only add one line for ``label[0]``. There are further examples of this in the docstring of ``TimeSeries`` for information on adding multiple lines. In summary, to add multiple lines (say, for different assembly types on a Peak DPA plot), the label would be the assembly type and the data would be the dpa at the time for that type. The ``uncertainty`` value --> which in general denotes an error bar on the graph---> would be None or 0, for each point if there is no uncertainty. HTML Elements -------------- +============= One may also want to add just plain prose. To do this, Sections also allow for the addition of htmltree elements so you can add paragraphs, divs, etc, as outlined in htmltree. These parts however will not be titled unless wrapped within a Section, and similarily will not have a direct link in the table of contents without a Section wrap as well (due to their inherent lack of title). However, thier addition may add beneficial information to reports in between Tables and Images that could prove useful to the user and any readers. - - Summary -------- +======= + ``ReportContent`` is made up of many different types of elements (``Sections``, ``Tables``, ``Images``, ``HtmlElements``, ``TimeSeries``), that when ``writeReports()`` is called on the ``ReportContent`` object, have the ability to be rendered through their ``render()`` method in order to be translated to html for the resulting document. This document is saved in a new folder titled reportsOutputFiles. diff --git a/doc/developer/standards_and_practices.rst b/doc/developer/standards_and_practices.rst index 3d7464a29..4d099db0a 100644 --- a/doc/developer/standards_and_practices.rst +++ b/doc/developer/standards_and_practices.rst @@ -1,3 +1,5 @@ +.. _armi-stds: + ********************************** Standards and Practices for Coding ********************************** diff --git a/doc/developer/tooling.rst b/doc/developer/tooling.rst index 8ef61a866..b46d83fa4 100644 --- a/doc/developer/tooling.rst +++ b/doc/developer/tooling.rst @@ -1,9 +1,11 @@ +.. _armi-tooling: + ************************** Tooling and Infrastructure ************************** Good Commit Messages --------------------- +==================== The ARMI project follows a few basic rules for "good" commit messages: * The purpose of the message is to explain to the changes you made to a stranger 5 years from now. @@ -24,14 +26,14 @@ The ARMI project follows a few basic rules for "good" commit messages: * optional. Good Pull Requests ------------------- +================== A good commit is like a sentence; it expresses one complete thought. In that context, a good Pull Request (PR) is like a paragraph; it contains a few sentences that contain one larger thought. A good PR is *not* a chapter or an entire book! It should not contain multiple independent ideas. One Idea = One PR -^^^^^^^^^^^^^^^^^ +----------------- .. important :: If you *can* break a PR into smaller PRs, containing unrelated changes, please do. @@ -41,7 +43,8 @@ They are busy people, and it will save them time and effort if your PR only has If your PRs are smaller, you will notice a great increase in the quality of the reviews you get. Don't open until it is ready -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +---------------------------- + .. important :: Wait until your PR is complete to open it. @@ -54,7 +57,7 @@ prefer to keep the PR list as short as possible. A good rule of thumb is: don't you think it is ready for final review. Test It -^^^^^^^ +------- .. important :: If a PR doesn't have any changes to testing, it probably isn't complete. @@ -69,7 +72,8 @@ If the changes in the PR are worth the time to make, they are worth the time to reviewer by proving your code works. Document It -^^^^^^^^^^^ +----------- + .. important :: If it isn't documented, it doesn't exist. @@ -80,7 +84,7 @@ Also consider (if you are making a major change) that you might be making someth out-of-date. Watch for Requirements -^^^^^^^^^^^^^^^^^^^^^^ +---------------------- When you are touching code in ARMI, watch out for the docstrings in the methods, classes, or modules you are editing. These docstrings might have bread crumbs that link back to requirements. Such breadcrumbs will look like: @@ -102,14 +106,14 @@ Your PR reviewer will take an extra look at any PR that touches a requirement te And you will need to add a special release note under the "Changes that Affect Requirements" section header. Packaging and dependency management ------------------------------------ +=================================== The process of packaging Python projects and managing their dependencies is somewhat challenging and nuanced. The contents of our ``pyproject.toml`` follow existing conventions as much as possible. In particular, we follow `the official Python packaging guidance `_. pyproject.toml -^^^^^^^^^^^^^^ +-------------- As much as possible, the ARMI team will try to centralize our installation and build systems through the top-level ``pyproject.toml`` file. The only exception will be our documentation, which has much customization done through the Sphinx ``doc/conf.py`` file. @@ -121,7 +125,7 @@ packages that are not strictly required, but if installed enable extra functiona like unit testing or building documentation. Third-Party Licensing -^^^^^^^^^^^^^^^^^^^^^ +--------------------- Be careful when including any dependency in ARMI (say in the ``pyproject.toml`` file) not to include anything with a license that superceeds our Apache license. For instance, any third-party Python library included in ARMI with a GPL license will make the whole @@ -132,7 +136,7 @@ For that reason, it is generally considered best-practice in the ARMI ecosystem only use third-party Python libraries that have MIT or BSD licenses. Releasing a New Version of ARMI -------------------------------- +=============================== We use the common ``major.minor.bump`` version scheme where a version string might look like ``0.1.7``, ``1.0.0``, or ``12.3.123``. Each number has a specific meaning: @@ -166,7 +170,7 @@ Every release should follow this process: 7. Tell everyone! Module-Level Logging --------------------- +==================== In most of the modules in ``armi``, you will see logging using the ``runLog`` module. This is a custom, global logging object provided by the import: diff --git a/doc/release/0.1.rst b/doc/release/0.1.rst index 01f5a1eb9..fb6d3fc26 100644 --- a/doc/release/0.1.rst +++ b/doc/release/0.1.rst @@ -1,6 +1,6 @@ -======================= +*********************** ARMI v0.1 Release Notes -======================= +*********************** ARMI v0.1.7 =========== diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst index 410a13c96..356265788 100644 --- a/doc/release/0.2.rst +++ b/doc/release/0.2.rst @@ -1,6 +1,6 @@ -======================= +*********************** ARMI v0.2 Release Notes -======================= +*********************** ARMI v0.2.9 =========== diff --git a/doc/release/0.3.rst b/doc/release/0.3.rst index 7b85cd262..2f5ff1b45 100644 --- a/doc/release/0.3.rst +++ b/doc/release/0.3.rst @@ -1,6 +1,6 @@ -======================= +*********************** ARMI v0.3 Release Notes -======================= +*********************** ARMI v0.3.1 ============ diff --git a/doc/release/index.rst b/doc/release/index.rst index 5a56678a4..efb4e4938 100644 --- a/doc/release/index.rst +++ b/doc/release/index.rst @@ -1,5 +1,6 @@ +############# Release Notes -============= +############# Each ARMI release has a set of corresponding notes, found within this section. diff --git a/doc/tutorials/index.rst b/doc/tutorials/index.rst index b3cdb1f7c..9e6881317 100644 --- a/doc/tutorials/index.rst +++ b/doc/tutorials/index.rst @@ -1,3 +1,5 @@ +.. _armi-tutorials: + ######### Tutorials ######### diff --git a/doc/tutorials/making_your_first_app.rst b/doc/tutorials/making_your_first_app.rst index a5f62b0de..4509bd97c 100644 --- a/doc/tutorials/making_your_first_app.rst +++ b/doc/tutorials/making_your_first_app.rst @@ -2,9 +2,11 @@ Note that this file makes use of Python files in a ``armi-example-app`` folder so that they can be put under testing. -================================ +.. _armi-make-first-app: + +******************************** Making your first ARMI-based App -================================ +******************************** In this tutorial we will build a nuclear analysis application that runs (dummy) neutron flux and thermal/hydraulics calculations. Applications that do real analysis can be @@ -385,7 +387,7 @@ from 360 |deg|\ C to 510 |deg|\ C (as expected given our simple TH solver). program to the data in the primary ARMI HDF5 file. However it is slightly more finicky and has slightly less support in some tools (looking at VisIT). -A generic description of the outputs is provided in :doc:`/user/outputs/index`. +A generic description of the outputs is provided in :doc:`/user/outputs`. You can add your own outputs from your plugins. diff --git a/doc/tutorials/walkthrough_inputs.rst b/doc/tutorials/walkthrough_inputs.rst index c4302d31c..f2e7ffa87 100644 --- a/doc/tutorials/walkthrough_inputs.rst +++ b/doc/tutorials/walkthrough_inputs.rst @@ -1,13 +1,15 @@ -======================================= +.. _walkthrough-inputs: + +*************************************** Building input files for a fast reactor -======================================= +*************************************** The true power of ARMI comes when you have a reactor at your fingertips. To get this, you must describe the reactor via input files. This tutorial will walk you through building input files from scratch for a reactor. We will model the CR=1.0 sodium-cooled fast reactor documented in `ANL-AFCI-177 `_. The full :doc:`documentation -for input files is available here `. +for input files is available here `. .. tip:: The full inputs created in this tutorial are available for download at the bottom of this page. diff --git a/doc/tutorials/walkthrough_lwr_inputs.rst b/doc/tutorials/walkthrough_lwr_inputs.rst index 693a32457..84e405338 100644 --- a/doc/tutorials/walkthrough_lwr_inputs.rst +++ b/doc/tutorials/walkthrough_lwr_inputs.rst @@ -1,6 +1,6 @@ -========================================== +****************************************** Building input files for a thermal reactor -========================================== +****************************************** In the :doc:`previous tutorial `, we introduced the basic input files and made a full diff --git a/doc/user/accessingEntryPoints.rst b/doc/user/accessingEntryPoints.rst index b58ff8d79..5ab5c7a12 100644 --- a/doc/user/accessingEntryPoints.rst +++ b/doc/user/accessingEntryPoints.rst @@ -3,7 +3,7 @@ Accessing Entry Points ********************** Reports Entry Point -------------------- +=================== There are two ways to access the reports entry point in ARMI. diff --git a/doc/user/assembly_parameters_report.rst b/doc/user/assembly_parameters_report.rst index b51cc0964..8bd709d32 100644 --- a/doc/user/assembly_parameters_report.rst +++ b/doc/user/assembly_parameters_report.rst @@ -1,3 +1,5 @@ +.. _assembly-parameters-report: + ******************* Assembly Parameters ******************* diff --git a/doc/user/block_parameters_report.rst b/doc/user/block_parameters_report.rst index 38ff2a77b..e55aaff3a 100644 --- a/doc/user/block_parameters_report.rst +++ b/doc/user/block_parameters_report.rst @@ -1,3 +1,5 @@ +.. _block-parameters-report: + **************** Block Parameters **************** diff --git a/doc/user/component_parameters_report.rst b/doc/user/component_parameters_report.rst index 463d4f62d..9c5901146 100644 --- a/doc/user/component_parameters_report.rst +++ b/doc/user/component_parameters_report.rst @@ -1,3 +1,5 @@ +.. _component-parameters-report: + ******************** Component Parameters ******************** diff --git a/doc/user/core_parameters_report.rst b/doc/user/core_parameters_report.rst index 2f09a3650..f928d482d 100644 --- a/doc/user/core_parameters_report.rst +++ b/doc/user/core_parameters_report.rst @@ -1,3 +1,5 @@ +.. _core-parameters-report: + *************** Core Parameters *************** diff --git a/doc/user/index.rst b/doc/user/index.rst index 5c5df9eb6..360dea653 100644 --- a/doc/user/index.rst +++ b/doc/user/index.rst @@ -13,8 +13,8 @@ analyzing ARMI output files, etc. :numbered: user_install - inputs/index - outputs/index + inputs + outputs manual_data_access reactor_parameters_report core_parameters_report diff --git a/doc/user/inputs/blueprints.rst b/doc/user/inputs.rst similarity index 56% rename from doc/user/inputs/blueprints.rst rename to doc/user/inputs.rst index 810682c36..02fa47375 100644 --- a/doc/user/inputs/blueprints.rst +++ b/doc/user/inputs.rst @@ -1,6 +1,419 @@ -************************* +****** +Inputs +****** + +ARMI input files define the initial state of the reactor model and tell ARMI what kind of analysis should be +performed on it. + +.. note:: We have a :ref:`walkthrough-inputs` tutorial for a quick + overview of the inputs. + +There are several input files: + +Settings file + Contains simulation parameters (like full power, cycle length, and which physics modules to + activate) and all kind of modeling approximation settings (e.g. convergence criteria) + +Blueprints file + Contains dimensions and composition of the components/blocks/assemblies in your reactor systems, from fuel + pins to heat exchangers + +Fuel management file + Describes how fuel moves around during a simulation + + +Depending on the type of analysis, there may be additional inputs required. These include things like +control logic, ex-core models for transients and shielding, etc. + +The core map input files can be graphically manipulated with the +:py:mod:`Grid editor `. + + +The Settings Input File +======================= +The **settings** input file defines a series of key/value pairs the define various information about the system you are +modeling as well as which modules to run and various modeling/approximation settings. For example, it includes: + +* The case title +* The reactor power +* The number of cycles to run +* Which physics solvers to activate +* Whether or not to perform a critical control search +* Whether or not to do tight coupling iterations +* What neutronics approximations specific to the chosen physics solver to apply +* Environment settings (paths to external codes) +* How many CPUs to use on a computer cluster + +This file is a YAML file that you can edit manually with a text editor or with the ARMI GUI. + +Here is an excerpt from a settings file: + +.. literalinclude:: ../../../armi/tests/armiRun.yaml + :language: yaml + :lines: 3-15 + +A full listing of settings available in the framework may be found in the `Table of all global settings <#settings-report>`_ . + +Many settings are provided by the ARMI Framework, and others are defined by various plugins. + +.. _armi-gui: + +The ARMI GUI +------------ +The ARMI GUI may be used to manipulate many common settings (though the GUI can't change all of the settings). The GUI +also enables the graphical manipulation of a reactor core map, and convenient automation of commands required to submit to a +cluster. The GUI is a front-end to +these files. You can choose to use the GUI or not, ARMI doesn't know or care --- it just reads these files and runs them. + +Note that one settings input file is required for each ARMI case, though many ARMI cases can refer to the same +Blueprints, Core Map, and Fuel Management inputs. + +.. tip:: The ARMI GUI is not yet included in the open-source ARMI framework + +The assembly clicker +^^^^^^^^^^^^^^^^^^^^ +The assembly clicker (in the ``grids`` editor) allows users to define the 2-D layout of the assemblies defined in the +:ref:`bp-input-file`. This can be done in hexagon or cartesian. The results of this arrangement get written to +grids in blueprints. Click on the assembly palette on the right and click on the locations where you want to put the +assembly. By default, the input assumes a 1/3 core model, but you can create a full core model through the menu. + +If you want one assembly type to fill all positions in a ring, right click it once it is placed and choose ``Make ring +like this hex``. Once you submit the job or save the settings file (File -> Save), you will be prompted for a new name +of the geometry file before the settings file is saved. The geometry setting in the main tab will also be updated. + +The ARMI Environment Tab +^^^^^^^^^^^^^^^^^^^^^^^^ +The environment tab contains important settings about which version of ARMI you will run +and with which version of Python, etc. Most important is the ``ARMI location`` setting. This +points to the codebase that will run. If you want to run the released version of ARMI, +ensure that it is set in this setting. If you want to run a developer version, then be sure +to update this setting. + +Other settings on this tab may need to be updated depending on your computational environment. +Talk to your system admins to determine which settings are best. + +Some special settings +--------------------- +A few settings warrant additional discussion. + +.. _detail-assems: + +Detail assemblies +^^^^^^^^^^^^^^^^^ +Many plugins perform more detailed analysis on certain regions of the reactor. Since the analyses +often take longer, ARMI has a feature, called *detail assemblies* to help. Different plugins +may treat detail assemblies differently, so it's important to read the plugin documentation +as well. For example, a depletion plugin may perform pin-level depletion and rotation analysis +only on the detail assemblies. Or perhaps CFD thermal/hydraulics will be run on detail assemblies, +while subchannel T/H is run on the others. + +Detail assemblies are specified by the user in a variety of ways, +through the GUI or the settings system. + +.. warning:: The Detail Assemblies mechanism has begun to be too broad of a brush + for serious multiphysics calculations with each plugin treating them differently. + It is likely that this feature will be extended to be more flexible and less + surprising in the future. + +Detail Assembly Locations BOL + The ``detailAssemLocationsBOL`` setting is a list of assembly location strings + (e.g. ``004-003`` for ring 4, position 3). Assemblies that are in these locations at the + beginning-of-life will be activated as detail assemblies. + +Detail assembly numbers + The ``detailAssemNums`` setting is a list of ``assemNum``\ s that can be inferred from a previous + case and specified, regardless of when the assemblies enter the core. This is useful for + activating detailed treatment of assemblies that enter the core at a later cycle. + +Detail all assemblies + The ``detailAllAssems`` setting makes all assemblies in the problem detail assemblies + +.. _kinetics-settings: + +Kinetics settings +^^^^^^^^^^^^^^^^^ +In reactor physics analyses it is standard practice to represent reactivity +in either absolute units (i.e., dk/kk' or pcm) or in dollars or cents. To +support this functionality, the framework supplies the ``beta`` and +``decayConstants`` settings to apply the delayed neutron fraction and +precursor decay constants to the Core parameters during initialization. + +These settings come with a few caveats: + + 1. The ``beta`` setting supports two different meanings depending on + the type that is provided. If a single value is given, then this setting + is interpreted as the effective delayed neutron fraction for the + system. If a list of values is provided, then this setting is interpreted + as the group-wise (precursor family) delayed neutron fractions (useful for + reactor kinetics simulations). + + 2. The ``decayConstants`` setting is used to define the precursor + decay constants for each group. When set, it must be + provided with a corresponding ``beta`` setting that has the + same number of groups. For example, if six-group delayed neutron + fractions are provided, the decay constants must also be provided + in the same six-group structure. + + 3. If ``beta`` is interpreted as the effective delayed neutron fraction for + the system, then the ``decayConstants`` setting will not be utilized. + + 4. If both the group-wise ``beta`` and ``decayConstants`` are provided + and their number of groups are consistent, then the effective delayed + neutron fraction for the system is calculated as the summation of the + group-wise delayed neutron fractions. + +.. _cycle-history: + +Cycle history +^^^^^^^^^^^^^ +For all cases, ``nCycles`` and ``power`` must be specified by the user. +In the case that only a single state is to be examined (i.e. no burnup), the user need only additionally specify ``nCycles = 1``. + +In the case of burnup, the reactor cycle history may be specified using either the simple or detailed +option. +The simple cycle history consists of the following case settings: + + * ``power`` + * ``nCycles`` (default = 1) + * ``burnSteps`` (default = 4) + * ``availabilityFactor(s)`` (default = 1.0) + * ``cycleLength(s)`` (default = 365.2425) + +In addition, one may optionally use the ``powerFractions`` setting to change the reactor +power between each cycle. +With these settings, a user can define a history in which each cycle may vary +in power, length, and uptime. +The history is restricted, however, to each cycle having a constant power, to +each cycle having the same number of burnup nodes, and to those burnup nodes being +evenly spaced within each cycle. +An example simple cycle history might look like + +.. code-block:: yaml + + power: 1000000 + nCycles: 3 + burnSteps: 2 + cycleLengths: [100, R2] + powerFractions: [1.0, 0.5, 1.0] + availabilityFactors: [0.9, 0.3, 0.93] + +Note the use of the special shorthand list notation, where repeated values in a list can be specified using an "R" followed by the number of times the value is to be repeated. + +The above scheme would represent 3 cycles of operation: + + 1. 100% power for 90 days, split into two segments of 45 days each, followed by 10 days shutdown (i.e. 90% capacity) + + 2. 50% power for 30 days, split into two segments of 15 days each, followed by 70 days shutdown (i.e. 15% capacity) + + 3. 100% power for 93 days, split into two segments of 46.5 days each, followed by 7 days shutdown (i.e. 93% capacity) + +In each cycle, criticality calculations will be performed at 3 nodes evenly-spaced through the uptime portion of the cycle (i.e. ``availabilityFactor``*``powerFraction``), without option for changing node spacing or frequency. +This input format can be useful for quick scoping and certain types of real analyses, but clearly has its limitations. + +To overcome these limitations, the detailed cycle history, consisting of the ``cycles`` setting may be specified instead. +For each cycle, an entry to the ``cycles`` list is made with the following optional fields: + + * ``name`` + * ``power fractions`` + * ``cumulative days``, ``step days``, or ``burn steps`` + ``cycle length`` + * ``availability factor`` + +An example detailed cycle history employing all of these fields could look like + +.. code-block:: yaml + + power: 1000000 + nCycles: 4 + cycles: + - name: A + step days: [1, 1, 98] + power fractions: [0.1, 0.2, 1] + availability factor: 0.1 + - name: B + cumulative days: [2, 72, 78, 86] + power fractions: [0.2, 1.0, 0.95, 0.93] + - name: C + step days: [5, R5] + power fractions: [1, R5] + - cycle length: 100 + burn steps: 2 + availability factor: 0.9 + +Note that repeated values in a list may be again be entered using the shorthand notation for ``step days``, ``power fractions``, and ``availability factors`` (though not ``cumulative days`` because entries must be monotonically increasing). + +Such a scheme would define the following cycles: + + 1. A 2 day power ramp followed by full power operations for 98 days, with three nodes clustered during the ramp and another at the end of the cycle, followed by 900 days of shutdown + + 2. A 2 day power ramp followed by a prolonged period at full power and then a slight power reduction for the last 14 days in the cycle + + 3. Constant full-power operation for 30 days split into six even increments + + 4. Constant full-power operation for 90 days, split into two equal-length 45 day segments, followed by 10 days of downtime + +As can be seen, the detailed cycle history option provides much greated flexibility for simulating realistic operations, particularly power ramps or scenarios that call for unevenly spaced burnup nodes, such as xenon buildup in the early period of thermal reactor operations. + +.. note:: Although the detailed cycle history option allows for powers to change within each cycle, it should be noted that the power over each step is still considered to be constant. + +.. note:: The ``name`` field of the detailed cycle history is not yet used for anything, but this information will still be accessible on the operator during runtime. + +.. note:: Cycles without names will be given the name ``None`` + +.. warning:: When a detailed cycle history is combined with tight coupling, a subclass of :py:meth:`LatticePhysicsInterface.interactCoupled ` should be used. + +.. _restart-cases: + +Restart cases +^^^^^^^^^^^^^ +Oftentimes the user is interested in re-examining just a specific set of time nodes from an existing run. +In these cases, it is sometimes not necessary to rerun an entire reactor history, and one may instead use one of the following options: + + 1. Snapshot, where the reactor state is loaded from a database and just a single time node is run. + + 2. Restart, where the cycle history is loaded from a database and the calculation continues through the remaining specified time history. + +For either of these options, it is possible to alter the specific settings applied to the run by simply adjusting the case settings for the run. +For instance, a run that originally had only neutronics may incorporate thermal hydraulics during a snapshot run by adding in the relevant TH settings. + +.. note:: For either of these options, it is advisable to first create a new case settings file with a name different than the one from which you will be restarting off of, so as to not overwrite those results. + +To run a snapshot, the following settings must be added to your case settings: + + * Set ``runType`` to ``Snapshots`` + * Add a list of cycle/node pairs corresponding to the desired snapshots to ``dumpSnapshot`` formatted as ``'CCCNNN'`` + * Set ``reloadDBName`` to the existing database file that you would like to load the reactor state from + +An example of a snapshot run input: + +.. code-block:: yaml + + runType: Snapshots + reloadDBName: my-old-results.h5 + dumpSnapshot: ['000000', '001002'] # would produce 2 snapshots, at BOL and at node 2 of cycle 1 + +To run a restart, the following settings must be added to your case settings: + + * Set ``runType`` to ``Standard`` + * Set ``loadStyle`` to ``fromDB`` + * Set ``startCycle`` and ``startNode`` to the cycle/node that you would like to continue the calculation from (inclusive). ``startNode`` may use negative indexing. + * Set ``reloadDBName`` to the existing database file from which you would like to load the reactor history up to the restart point + * If you would like to change the specified reactor history (see :ref:`restart-cases`), keep the history up to the restarting cycle/node unchanged, and just alter the history after that point. This means that the cycle history specified in your restart run should include all cycles/nodes up to the end of the simulation. For complicated restarts, it may be necessary to use the detailed ``cycles`` setting, even if the original case only used the simple history option. + +A few examples of restart cases: + + - Restarting a calculation at a specific cycle/node and continuing for the remainder of the originally-specified cycle history: + .. code-block:: yaml + + # old settings + nCycles: 2 + burnSteps: 2 + cycleLengths: [100, 100] + runType: Standard + loadStyle: fromInput + loadingFile: my-blueprints.yaml + + .. code-block:: yaml + + # restart settings + nCycles: 2 + burnSteps: 2 + cycleLengths: [100, 100] + runType: Standard + loadStyle: fromDB + startCycle: 1 + startNode: 0 + reloadDBName: my-original-results.h5 + + - Add an additional cycle to the end of a case: + .. code-block:: yaml + + # old settings + nCycles: 1 + burnSteps: 2 + cycleLengths: [100] + runType: Standard + loadStyle: fromInput + loadingFile: my-blueprints.yaml + + .. code-block:: yaml + + # restart settings + nCycles: 2 + burnSteps: 2 + cycleLengths: [100, 100] + runType: Standard + loadStyle: fromDB + startCycle: 0 + startNode: -1 + reloadDBName: my-original-results.h5 + + - Restart but cut the reactor history short: + .. code-block:: yaml + + # old settings + nCycles: 3 + burnSteps: 2 + cycleLengths: [100, 100, 100] + runType: Standard + loadStyle: fromInput + loadingFile: my-blueprints.yaml + + .. code-block:: yaml + + # restart settings + nCycles: 2 + burnSteps: 2 + cycleLengths: [100, 100] + runType: Standard + loadStyle: fromDB + startCycle: 1 + startNode: 0 + reloadDBName: my-original-results.h5 + + - Restart with a different number of steps in the third cycle using the detailed ``cycles`` setting: + .. code-block:: yaml + + # old settings + nCycles: 3 + burnSteps: 2 + cycleLengths: [100, 100, 100] + runType: Standard + loadStyle: fromInput + loadingFile: my-blueprints.yaml + + .. code-block:: yaml + + # restart settings + nCycles: 3 + cycles: + - cycle length: 100 + burn steps: 2 + - cycle length: 100 + burn steps: 2 + - cycle length: 100 + burn steps: 4 + runType: Standard + loadStyle: fromDB + startCycle: 2 + startNode: 0 + reloadDBName: my-original-results.h5 + +.. note:: The ``skipCycles`` setting is related to skipping the lattice physics calculation specifically, it is not required to do a restart run. + +.. note:: The X-SHUFFLES.txt file is required to do explicit repeated fuel management. + +.. note:: The restart.dat file is required to repeat the exact fuel management methods during a branch search. These can potentially modify the reactor state in ways that cannot be captures with the SHUFFLES.txt file. + +.. note:: The ISO binary cross section libraries are required to run cases that skip the lattice physics calculation (e.g. MC^2) + +.. note:: The multigroup flux is not yet stored on the output databases. If you need to do a restart with these values (e.g. for depletion), then you need to reload from neutronics outputs. + +.. note:: Restarting a calculation with an different version of ARMI than what was used to produce the restarting database may result in undefined behavior. + +.. _bp-input-file: + The Blueprints Input File -************************* +========================= The **blueprints** input defines the dimensions of structures in the reactor, as well as their material makeup. In a typical case, pin dimensions, isotopic composition, control definitions, coolant type, etc. are @@ -33,8 +446,7 @@ ARMI models are built hierarchically, first by defining components, and then by collections of the levels of the reactor. Blueprint sections -================== - +------------------ The **blueprints** input file has several sections that corresponds to different levels of the reactor hierarchy. You will generally build inputs "bottoms up", first by defining elementary pieces (like pins) and then collecting them into the core and reactor. @@ -79,7 +491,7 @@ The ARMI data model is represented schematically below, and the blueprints are d .. _blocks-and-components: Blocks and Components -===================== +--------------------- Blocks and components are defined together in the **blueprints** input. We will start with a component, and then define the whole ``blocks:`` @@ -106,7 +518,7 @@ input. The structure will be something like:: is not fully implemented yet. Defining a Component --------------------- +^^^^^^^^^^^^^^^^^^^^ The **Components** section defines the pin (if modeling a pin-type reactor) and assembly in-plane dimensions (axial dimensions are defined in the :ref:`assemblies` input) and the material makeups of each :py:mod:`Component `. :py:mod:`Blocks ` are @@ -168,7 +580,7 @@ od .. _componentTypes: Component Types ---------------- +^^^^^^^^^^^^^^^ Each component has a variety of dimensions to define the shape and composition. All dimensions are in cm. The following is a list of included component shapes and their dimension inputs. Again, additional/custom components with arbitrary dimensions may be provided by the user via plugins. @@ -189,7 +601,7 @@ a lattice of pins. .. _componentLinks: Component Links ---------------- +^^^^^^^^^^^^^^^ Dimensions of a component may depend on the dimensions of a previously-defined component in the same block. For instance, the sodium bond between fuel and cladding. The format is simply ``.``. The dimension names are available in the table above. @@ -233,7 +645,7 @@ reduced. This is physical since, in reality, the fluid would be displaced as dim change. Pin lattices ------------- +^^^^^^^^^^^^ Pin lattices may be explicitly defined in the block/component input in conjunction with the ``grids`` input section. A block may assigned a grid name, and then each component may be assigned one or more grid specifiers. @@ -272,7 +684,7 @@ cladding as the fuel pins. :: .. _naming-flags: Flags and naming -================ +---------------- All objects in the ARMI Reactor Model possess a set of :py:class:`armi.reactor.flags.Flags`, which can be used to affect the way that the @@ -310,7 +722,7 @@ will get the ``CLAD`` flag from its name. .. _assemblies: Assemblies -========== +---------- Once components and blocks are defined, Assemblies can be created as extruded stacks of blocks from bottom to top. The assemblies use YAML anchors to refer to the blocks defined in the previous section. @@ -521,7 +933,7 @@ other structure. .. _systems: Systems -======= +------- Once assemblies are defined they can be grouped together into the Core, the spent fuel pool (SFP), etc. A complete reactor structure with a core and a SFP may be seen below:: @@ -546,7 +958,7 @@ in units of cm. This allows you to define the relative position of the various s The ``grid name`` inputs are string mappings to the grid definitions described below. Plugin Behavior ---------------- +^^^^^^^^^^^^^^^ The :meth:`armi.plugins.ArmiPlugin.defineSystemBuilders` method can be provided by plugins to control how ARMI converts the ``systems`` section into ``Composite``\ s @@ -566,7 +978,7 @@ and new mappings of values to builders. .. _grids: Grids -===== +----- Grids are described inside a blueprint file using ``lattice map`` or ``grid contents`` fields to define arrangements in Hex, Cartesian, or R-Z-Theta. The optional ``lattice pitch`` entry allows you to specify spacing between objects that is different from tight packing. This input is required @@ -622,7 +1034,7 @@ Example grid definitions are shown below:: .. _custom-isotopics: Custom Isotopics -================ +---------------- In some cases (such as benchmarking a previous reactor), the default mass fractions from the material library are not what you want to model. In these cases, you may override the isotopic composition provided by the material library in this section. There are three ways to specify @@ -649,10 +1061,10 @@ nuclear data library). The (mass) ``density`` input is invalid when specifying ``number densities``; the code will present an error message. Advanced topics -=============== +--------------- Overlapping shapes ------------------- +^^^^^^^^^^^^^^^^^^ Solids of different compositions in contact with each other present complications during thermal expansion. The ARMI Framework does not perform calculations to see exactly how such scenarios will behave mechanically; it instead focuses on conserving mass. To do this, users should @@ -675,7 +1087,7 @@ component and get the siblings ``mult``. If you are concerned about performance with a YAML anchor and alias. Component area modifications ----------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ In some scenarios, it is desired to have one component's area be subtracted or added to another. For example, the area of the skids in a skid duct design needs to be subtracted from the interstitial coolant. The mechanism to handle this involves adding a parameter to the component to be @@ -704,7 +1116,7 @@ without explicitly defining new components. modArea: holes.sub # "holes" is the name of the other component Putting it all together to make a Block ---------------------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Here is a complete fuel block definition:: @@ -769,7 +1181,7 @@ Here is a complete fuel block definition:: Making blocks with unshaped components --------------------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Sometimes you will want to make a homogenous block, which is a mixture of multiple materials, and will not want to define an exact shape for each of the components in @@ -848,15 +1260,13 @@ are now four components, but only three that have actual area and composition:: This can similarly be done for hex geometry and and a hexagon with Outer Pitch (``op``). ---------- - .. warning:: The rest of the input described below are scheduled to be moved into the settings input file, since their nature is that of a setting. .. _nuclide-flags: Nuclide Flags -============= +------------- The ``nuclide flags`` setting allows the user to choose which nuclides they would like to consider in the problem, and whether or not each nuclide should transmute and decay. For example, sometimes you may not want to deplete trace @@ -914,3 +1324,229 @@ The code will crash if materials used in :ref:`blocks-and-components` contain nu .. |Tinput| replace:: T\ :sub:`input` .. |Thot| replace:: T\ :sub:`hot` + + +Fuel Management Input +===================== + +Fuel management in ARMI is specified through custom Python scripts that often reside +in the working directory of a run (but can be anywhere if you use full paths). During a normal run, +ARMI checks for two fuel management settings: + +``shuffleLogic`` + The path to the Python source file that contains the user's custom fuel + management logic + +``fuelHandlerName`` + The name of a FuelHandler class that ARMI will look for in the Fuel Management Input file + pointed to by the ``shuffleLogic`` path. Since it's input, it's the user's responsibility + to design and place that object in that file. + +.. note:: We consider the limited syntax needed to express fuel management in Python + code itself to be sufficiently expressive and simple for non-programmers to + actually use. Indeed, this has been our experience. + +The ARMI Operator will call its fuel handler's ``outage`` method before each cycle (and, if requested, during branch +search calculations). The :py:meth:`~armi.physics.fuelCycle.fuelHandlers.FuelHandler.outage` method +will perform bookkeeping operations, and eventually +call the user-defined ``chooseSwaps`` method (located in Fuel Management Input). ``chooseSwaps`` will +generally contain calls to :py:meth:`~armi.physics.fuelCycle.fuelHandlers.FuelHandler.findAssembly`, +:py:meth:`~armi.physics.fuelCycle.fuelHandlers.FuelHandler.swapAssemblies` , +:py:meth:`~armi.physics.fuelCycle.fuelHandlers.FuelHandler.swapCascade`, and +:py:meth:`~armi.physics.fuelCycle.fuelHandlers.FuelHandler.dischargeSwap`, which are the primary +fuel management operations and can be found in the fuel management module. + +Also found in the user-defined Fuel Management Input module is a ``getFactors`` method, which is used to control which +shuffling routines get called and at which time. + +.. note:: + + See the :py:mod:`fuelHandlers module ` for more details. + +Fuel Management Operations +-------------------------- +In the ARMI, the assemblies can be moved as units around the reactor with swapAssemblies, +dischargeSwap, and swapCascade of a ``FuelHandler`` interface. + +swapAssemblies +^^^^^^^^^^^^^^ +swapAssemblies is the simplest fuel management operation. Given two assembly objects, this method will switch +their locations. :: + + self.swapAssemblies(a1,a2) + +dischargeSwap +^^^^^^^^^^^^^ +A discharge swap is a simple operation that puts a new assembly into the reactor while discharging an +outgoing one. :: + + self.dischargeSwap(newIncoming,oldOutgoing) + +This operation keeps track of the outgoing assembly in a AssemblyList object that the Reactor object has access to so you can see how much of what you discharged. + +swapCascade +^^^^^^^^^^^ +SwapCascade is a more powerful swapping function that can swap a list of assemblies in a "daisy-chain" type +of operation. These are useful for doing the main overtone shuffling operations such as convergent shuffling +and/or convergent-divergent shuffling. If we load up the list of assemblies, the first one will be put in the +last one's position, and all others will shift accordingly. + +As an example, consider assemblies 1 through 5 in core positions A through E.:: + + self.swapCascade([a1,a2,a3,a4,a5]) + +This table shows the positions of the assemblies before and after the swap cascade. + + +======== ============================ =========================== +Assembly Position Before Swap Cascade Position After Swap Cascade +======== ============================ =========================== +1 A E +2 B A +3 C B +4 D C +5 E D +======== ============================ =========================== + +Arbitrarily complex cascades can thusly be assembled by choosing the order of the assemblies passed into swapCascade. + +Choosing Assemblies to Move +--------------------------- +The methods described in the previous section require known assemblies to shuffle. Choosing these assemblies is +the essence of fuel shuffling design. The single method used for these purposes is the FuelHandler's ``findAssembly`` +method. This method is very general purpose, and ranks in the top 3 most important +methods of the ARMI altogether. + +To use it, just say:: + + a = self.findAssembly(param='maxPercentBu',compareTo=20) + +This will return the assembly in the reactor that has a maximum burnup closest to 20%. Other +inputs to findAssembly are summarized in the API docs of +:py:meth:`~armi.physics.fuelCycle.fuelHandlers.FuelHandler.findAssembly`. + + +Fuel Management Examples +------------------------ + +Convergent-Divergent +^^^^^^^^^^^^^^^^^^^^ +Convergent-divergent shuffling is when fresh assemblies march in from the outside until they approach the jump ring, +at which point they jump to the center and diverge until they reach the jump ring again, where they now jump to the +outer periphery of the core, or become discharged. + +If the jump ring is 6, the order of target rings is:: + + [6, 5, 4, 3, 2, 1, 6, 7, 8, 9, 10, 11, 12, 13] + +In this case, assemblies converge from ring 13 to 12, to 11, to 10, ..., to 6, and then jump to 1 and diverge +until they get back to 6. In a discharging equilibrium case, the highest burned assembly in the jumpRing should +get discharged and the lowest should jump by calling a dischargeSwap on cascade[0] and a fresh feed after this +cascade is run. + +The convergent rings in this case are 7 through 13 and the divergent ones are 1 through 5 are the divergent ones. + + +Fuel Management Tips +-------------------- +Some mistakes are common. Follow these tips. + + * Always make sure your assembly-level types in the settings file are up to date with the grids in your bluepints file. Otherwise you'll be moving feeds when you want to move igniters, or something. + * Use the exclusions list! If you move a cascade and then the next cascade tries to run, it will choose your newly-moved assemblies if they fit your criteria in ``findAssemblies``. This leads to very confusing results. Therefore, once you move assemblies, you should default to adding them to the exclusions list. + * Print cascades during debugging. After you've built a cascade to swap, print it out and check the locations and types of each assembly in it. Is it what you want? + * Watch ``typeNum`` in the database. You can get good intuition about what is getting moved by viewing this parameter. + +Running a branch search +----------------------- +ARMI can perform a branch search where a number of fuel management operations +are performed in parallel and the preferred one is chosen and proceeded with. +The key to any branch search is writing a fuel handler that can interpret +**fuel management factors**, defined as keyed values between 0 and 1. + +As an example, a fuel handler may be written to interpret two factors, ``numDischarges`` +and ``chargeEnrich``. One method in the fuel handler would then take +the value of ``factors['numDischarges']`` and multiply it by the maximum +number of discharges (often set by another user setting) and then discharge +this many assemblies. Similarly, another method would take the ``factors['chargeEnrich']`` +value (between 0 and 1) and multiply it by the maximum allowable enrichment +(again, usually controlled by a user setting) to determine which enrichment +should be used to fabricate new assemblies. + +Given a fuel handler that can thusly interpret factors between 0 and 1, the +concept of branch searches is simple. They simply build uniformly distributed +lists between 0 and 1 across however many CPUs are available and cases on all +of them, passing one of each of the factors to each CPU in parallel. When the cases finish, +the branch search determines the optimal result and selects the corresponding +value of the factor to proceed. + +Branch searches are controlled by custom `getFactorList` methods specified in the +`shuffleLogic` input files. This method should return two things: + + * A ``defaultFactors``; a dictionary with user-defined keys and values between + 0 and 1 for each key. These factors will be passed to the ``chooseSwaps`` + method, which is typically overridden by the user in custom fuel handling code. + The fuel handling code should interpret the values and move the fuel + according to what is sent. + + * A ``factorSearchFlags`` list, which lists the keys to be branch searched. + The search will optimize the first key first, and then do a second pass + on the second key, holding the optimal first value constant, and so on. + +Such a method may look like this:: + + def getFactorList(cycle,cs=None): + + # init default shuffling factors + defaultFactors = {'chargeEnrich':0,'numDischarges':1} + factorSearchFlags=[] # init factors to run branch searches on + + # determine when to activate various factors / searches + if cycle not in [0,5,6]: + # shuffling happens before neutronics so skip the first cycle. + defaultFactors['chargeEnrich']=1 + else: + defaultFactors['numDischarges']=0 + factorSearchFlags = ['chargeEnrich'] + + return defaultFactors,factorSearchFlags + +Once a proper ``getFactorList`` method exists and a fuel handler object +exists that can interpret the factors, activate a branch search +during a regular run by selecting the **Branch Search** option on the GUI. + +The **best** result from the branch search is determined by comparing the *keff* values +with the ``targetK`` setting, which is available for setting in the GUI. The branch +with *keff* closest to the setting, while still being above 1.0 is chosen. + +.. _settings-report: + +Settings Report +=============== +This document lists all the `settings <#the-settings-input-file>`_ in ARMI. + +They are all accessible to developers +through the :py:class:`armi.settings.caseSettings.Settings` object, which is typically stored in a variable named +``cs``. Interfaces have access to a simulation's settings through ``self.cs``. + + +.. exec:: + from armi import settings + import textwrap + + subclassTables = {} + cs = settings.Settings() + # User textwrap to split up long words that mess up the table. + wrapper = textwrap.TextWrapper(width=25, subsequent_indent='') + wrapper2 = textwrap.TextWrapper(width=10, subsequent_indent='') + content = '\n.. list-table:: ARMI Settings\n :header-rows: 1\n :widths: 30 30 10 10\n \n' + content += ' * - Name\n - Description\n - Default\n - Options\n' + + for setting in sorted(cs.values(), key=lambda s: s.name): + content += ' * - {}\n'.format(' '.join(wrapper.wrap(setting.name))) + content += ' - {}\n'.format(' '.join(wrapper.wrap(str(setting.description) or ''))) + content += ' - {}\n'.format(' '.join(['``{}``'.format(wrapped) for wrapped in wrapper2.wrap(str(getattr(setting, 'default', None)).split("/")[-1])])) + content += ' - {}\n'.format(' '.join(['``{}``'.format(wrapped) for wrapped in wrapper.wrap(str(getattr(setting,'options','') or ''))])) + + content += '\n' + + return content diff --git a/doc/user/inputs/fuel_management.rst b/doc/user/inputs/fuel_management.rst deleted file mode 100644 index 4fe3fb912..000000000 --- a/doc/user/inputs/fuel_management.rst +++ /dev/null @@ -1,194 +0,0 @@ -********************* -Fuel Management Input -********************* - -Fuel management in ARMI is specified through custom Python scripts that often reside -in the working directory of a run (but can be anywhere if you use full paths). During a normal run, -ARMI checks for two fuel management settings: - -``shuffleLogic`` - The path to the Python source file that contains the user's custom fuel - management logic - -``fuelHandlerName`` - The name of a FuelHandler class that ARMI will look for in the Fuel Management Input file - pointed to by the ``shuffleLogic`` path. Since it's input, it's the user's responsibility - to design and place that object in that file. - -.. note:: We consider the limited syntax needed to express fuel management in Python - code itself to be sufficiently expressive and simple for non-programmers to - actually use. Indeed, this has been our experience. - -The ARMI Operator will call its fuel handler's ``outage`` method before each cycle (and, if requested, during branch -search calculations). The :py:meth:`~armi.physics.fuelCycle.fuelHandlers.FuelHandler.outage` method -will perform bookkeeping operations, and eventually -call the user-defined ``chooseSwaps`` method (located in Fuel Management Input). ``chooseSwaps`` will -generally contain calls to :py:meth:`~armi.physics.fuelCycle.fuelHandlers.FuelHandler.findAssembly`, -:py:meth:`~armi.physics.fuelCycle.fuelHandlers.FuelHandler.swapAssemblies` , -:py:meth:`~armi.physics.fuelCycle.fuelHandlers.FuelHandler.swapCascade`, and -:py:meth:`~armi.physics.fuelCycle.fuelHandlers.FuelHandler.dischargeSwap`, which are the primary -fuel management operations and can be found in the fuel management module. - -Also found in the user-defined Fuel Management Input module is a ``getFactors`` method, which is used to control which -shuffling routines get called and at which time. - -.. note:: - - See the :py:mod:`fuelHandlers module ` for more details. - -Fuel Management Operations -========================== -In the ARMI, the assemblies can be moved as units around the reactor with swapAssemblies, -dischargeSwap, and swapCascade of a ``FuelHandler`` interface. - -swapAssemblies --------------- -swapAssemblies is the simplest fuel management operation. Given two assembly objects, this method will switch -their locations. :: - - self.swapAssemblies(a1,a2) - -dischargeSwap -------------- -A discharge swap is a simple operation that puts a new assembly into the reactor while discharging an -outgoing one. :: - - self.dischargeSwap(newIncoming,oldOutgoing) - -This operation keeps track of the outgoing assembly in a AssemblyList object that the Reactor object has access to so you can see how much of what you discharged. - -swapCascade ------------ -SwapCascade is a more powerful swapping function that can swap a list of assemblies in a "daisy-chain" type -of operation. These are useful for doing the main overtone shuffling operations such as convergent shuffling -and/or convergent-divergent shuffling. If we load up the list of assemblies, the first one will be put in the -last one's position, and all others will shift accordingly. - -As an example, consider assemblies 1 through 5 in core positions A through E.:: - - self.swapCascade([a1,a2,a3,a4,a5]) - -This table shows the positions of the assemblies before and after the swap cascade. - - -======== ============================ =========================== -Assembly Position Before Swap Cascade Position After Swap Cascade -======== ============================ =========================== -1 A E -2 B A -3 C B -4 D C -5 E D -======== ============================ =========================== - -Arbitrarily complex cascades can thusly be assembled by choosing the order of the assemblies passed into swapCascade. - -Choosing Assemblies to Move -=========================== - -The methods described in the previous section require known assemblies to shuffle. Choosing these assemblies is -the essence of fuel shuffling design. The single method used for these purposes is the FuelHandler's ``findAssembly`` -method. This method is very general purpose, and ranks in the top 3 most important -methods of the ARMI altogether. - -To use it, just say:: - - a = self.findAssembly(param='maxPercentBu',compareTo=20) - -This will return the assembly in the reactor that has a maximum burnup closest to 20%. Other -inputs to findAssembly are summarized in the API docs of -:py:meth:`~armi.physics.fuelCycle.fuelHandlers.FuelHandler.findAssembly`. - - -Fuel Management Examples -======================== - -Convergent-Divergent --------------------- - -Convergent-divergent shuffling is when fresh assemblies march in from the outside until they approach the jump ring, -at which point they jump to the center and diverge until they reach the jump ring again, where they now jump to the -outer periphery of the core, or become discharged. - -If the jump ring is 6, the order of target rings is:: - - [6, 5, 4, 3, 2, 1, 6, 7, 8, 9, 10, 11, 12, 13] - -In this case, assemblies converge from ring 13 to 12, to 11, to 10, ..., to 6, and then jump to 1 and diverge -until they get back to 6. In a discharging equilibrium case, the highest burned assembly in the jumpRing should -get discharged and the lowest should jump by calling a dischargeSwap on cascade[0] and a fresh feed after this -cascade is run. - -The convergent rings in this case are 7 through 13 and the divergent ones are 1 through 5 are the divergent ones. - - -Fuel Management Tips -==================== -Some mistakes are common. Follow these tips. - - * Always make sure your assembly-level types in the settings file are up to date with the grids in your bluepints file. Otherwise you'll be moving feeds when you want to move igniters, or something. - * Use the exclusions list! If you move a cascade and then the next cascade tries to run, it will choose your newly-moved assemblies if they fit your criteria in ``findAssemblies``. This leads to very confusing results. Therefore, once you move assemblies, you should default to adding them to the exclusions list. - * Print cascades during debugging. After you've built a cascade to swap, print it out and check the locations and types of each assembly in it. Is it what you want? - * Watch ``typeNum`` in the database. You can get good intuition about what is getting moved by viewing this parameter. - -Running a branch search -======================= -ARMI can perform a branch search where a number of fuel management operations -are performed in parallel and the preferred one is chosen and proceeded with. -The key to any branch search is writing a fuel handler that can interpret -**fuel management factors**, defined as keyed values between 0 and 1. - -As an example, a fuel handler may be written to interpret two factors, ``numDischarges`` -and ``chargeEnrich``. One method in the fuel handler would then take -the value of ``factors['numDischarges']`` and multiply it by the maximum -number of discharges (often set by another user setting) and then discharge -this many assemblies. Similarly, another method would take the ``factors['chargeEnrich']`` -value (between 0 and 1) and multiply it by the maximum allowable enrichment -(again, usually controlled by a user setting) to determine which enrichment -should be used to fabricate new assemblies. - -Given a fuel handler that can thusly interpret factors between 0 and 1, the -concept of branch searches is simple. They simply build uniformly distributed -lists between 0 and 1 across however many CPUs are available and cases on all -of them, passing one of each of the factors to each CPU in parallel. When the cases finish, -the branch search determines the optimal result and selects the corresponding -value of the factor to proceed. - -Branch searches are controlled by custom `getFactorList` methods specified in the -`shuffleLogic` input files. This method should return two things: - - * A ``defaultFactors``; a dictionary with user-defined keys and values between - 0 and 1 for each key. These factors will be passed to the ``chooseSwaps`` - method, which is typically overridden by the user in custom fuel handling code. - The fuel handling code should interpret the values and move the fuel - according to what is sent. - - * A ``factorSearchFlags`` list, which lists the keys to be branch searched. - The search will optimize the first key first, and then do a second pass - on the second key, holding the optimal first value constant, and so on. - -Such a method may look like this:: - - def getFactorList(cycle,cs=None): - - # init default shuffling factors - defaultFactors = {'chargeEnrich':0,'numDischarges':1} - factorSearchFlags=[] # init factors to run branch searches on - - # determine when to activate various factors / searches - if cycle not in [0,5,6]: - # shuffling happens before neutronics so skip the first cycle. - defaultFactors['chargeEnrich']=1 - else: - defaultFactors['numDischarges']=0 - factorSearchFlags = ['chargeEnrich'] - - return defaultFactors,factorSearchFlags - -Once a proper ``getFactorList`` method exists and a fuel handler object -exists that can interpret the factors, activate a branch search -during a regular run by selecting the **Branch Search** option on the GUI. - -The **best** result from the branch search is determined by comparing the *keff* values -with the ``targetK`` setting, which is available for setting in the GUI. The branch -with *keff* closest to the setting, while still being above 1.0 is chosen. diff --git a/doc/user/inputs/index.rst b/doc/user/inputs/index.rst deleted file mode 100644 index fa3f7a977..000000000 --- a/doc/user/inputs/index.rst +++ /dev/null @@ -1,39 +0,0 @@ -****** -Inputs -****** - -ARMI input files define the initial state of the reactor model and tell ARMI what kind of analysis should be -performed on it. - -.. note:: We have an :doc:`input walkthrough ` tutorial for a quick - overview of the inputs. - -There are several input files: - -Settings file - Contains simulation parameters (like full power, cycle length, and which physics modules to - activate) and all kind of modeling approximation settings (e.g. convergence criteria) - -Blueprints file - Contains dimensions and composition of the components/blocks/assemblies in your reactor systems, from fuel - pins to heat exchangers - -Fuel management file - Describes how fuel moves around during a simulation - - -Depending on the type of analysis, there may be additional inputs required. These include things like -control logic, ex-core models for transients and shielding, etc. - -The core map input files can be graphically manipulated with the -:py:mod:`Grid editor `. - - ------------ - -.. toctree:: - - settings - blueprints - fuel_management - settings_report diff --git a/doc/user/inputs/settings.rst b/doc/user/inputs/settings.rst deleted file mode 100644 index d0f9cc92c..000000000 --- a/doc/user/inputs/settings.rst +++ /dev/null @@ -1,387 +0,0 @@ -*********************** -The Settings Input File -*********************** - -The **settings** input file defines a series of key/value pairs the define various information about the system you are -modeling as well as which modules to run and various modeling/approximation settings. For example, it includes: - -* The case title -* The reactor power -* The number of cycles to run -* Which physics solvers to activate -* Whether or not to perform a critical control search -* Whether or not to do tight coupling iterations -* What neutronics approximations specific to the chosen physics solver to apply -* Environment settings (paths to external codes) -* How many CPUs to use on a computer cluster - -This file is a YAML file that you can edit manually with a text editor or with the ARMI GUI. - -Here is an excerpt from a settings file: - -.. literalinclude:: ../../../armi/tests/armiRun.yaml - :language: yaml - :lines: 3-15 - -A full listing of settings available in the framework may be found in the :doc:`Table of all global settings `. - -Many settings are provided by the ARMI Framework, and others are defined by various plugins. - -.. _armi-gui: - -The ARMI GUI -============ -The ARMI GUI may be used to manipulate many common settings (though the GUI can't change all of the settings). The GUI -also enables the graphical manipulation of a reactor core map, and convenient automation of commands required to submit to a -cluster. The GUI is a front-end to -these files. You can choose to use the GUI or not, ARMI doesn't know or care --- it just reads these files and runs them. - -Note that one settings input file is required for each ARMI case, though many ARMI cases can refer to the same -Blueprints, Core Map, and Fuel Management inputs. - -.. tip:: The ARMI GUI is not yet included in the open-source ARMI framework - -The assembly clicker --------------------- -The assembly clicker (in the ``grids`` editor) allows users to define the 2-D layout of the assemblies defined in the -:doc:`/user/inputs/blueprints`. This can be done in hexagon or cartesian. The results of this arrangement get written to -grids in blueprints. Click on the assembly palette on the right and click on the locations where you want to put the -assembly. By default, the input assumes a 1/3 core model, but you can create a full core model through the menu. - -If you want one assembly type to fill all positions in a ring, right click it once it is placed and choose ``Make ring -like this hex``. Once you submit the job or save the settings file (File -> Save), you will be prompted for a new name -of the geometry file before the settings file is saved. The geometry setting in the main tab will also be updated. - -The ARMI Environment Tab ------------------------- -The environment tab contains important settings about which version of ARMI you will run -and with which version of Python, etc. Most important is the ``ARMI location`` setting. This -points to the codebase that will run. If you want to run the released version of ARMI, -ensure that it is set in this setting. If you want to run a developer version, then be sure -to update this setting. - -Other settings on this tab may need to be updated depending on your computational environment. -Talk to your system admins to determine which settings are best. - -Some special settings -===================== -A few settings warrant additional discussion. - -.. _detail-assems: - -Detail assemblies ------------------ -Many plugins perform more detailed analysis on certain regions of the reactor. Since the analyses -often take longer, ARMI has a feature, called *detail assemblies* to help. Different plugins -may treat detail assemblies differently, so it's important to read the plugin documentation -as well. For example, a depletion plugin may perform pin-level depletion and rotation analysis -only on the detail assemblies. Or perhaps CFD thermal/hydraulics will be run on detail assemblies, -while subchannel T/H is run on the others. - -Detail assemblies are specified by the user in a variety of ways, -through the GUI or the settings system. - -.. warning:: The Detail Assemblies mechanism has begun to be too broad of a brush - for serious multiphysics calculations with each plugin treating them differently. - It is likely that this feature will be extended to be more flexible and less - surprising in the future. - -Detail Assembly Locations BOL - The ``detailAssemLocationsBOL`` setting is a list of assembly location strings - (e.g. ``004-003`` for ring 4, position 3). Assemblies that are in these locations at the - beginning-of-life will be activated as detail assemblies. - -Detail assembly numbers - The ``detailAssemNums`` setting is a list of ``assemNum``\ s that can be inferred from a previous - case and specified, regardless of when the assemblies enter the core. This is useful for - activating detailed treatment of assemblies that enter the core at a later cycle. - -Detail all assemblies - The ``detailAllAssems`` setting makes all assemblies in the problem detail assemblies - -.. _kinetics-settings: - -Kinetics settings ------------------ -In reactor physics analyses it is standard practice to represent reactivity -in either absolute units (i.e., dk/kk' or pcm) or in dollars or cents. To -support this functionality, the framework supplies the ``beta`` and -``decayConstants`` settings to apply the delayed neutron fraction and -precursor decay constants to the Core parameters during initialization. - -These settings come with a few caveats: - - 1. The ``beta`` setting supports two different meanings depending on - the type that is provided. If a single value is given, then this setting - is interpreted as the effective delayed neutron fraction for the - system. If a list of values is provided, then this setting is interpreted - as the group-wise (precursor family) delayed neutron fractions (useful for - reactor kinetics simulations). - - 2. The ``decayConstants`` setting is used to define the precursor - decay constants for each group. When set, it must be - provided with a corresponding ``beta`` setting that has the - same number of groups. For example, if six-group delayed neutron - fractions are provided, the decay constants must also be provided - in the same six-group structure. - - 3. If ``beta`` is interpreted as the effective delayed neutron fraction for - the system, then the ``decayConstants`` setting will not be utilized. - - 4. If both the group-wise ``beta`` and ``decayConstants`` are provided - and their number of groups are consistent, then the effective delayed - neutron fraction for the system is calculated as the summation of the - group-wise delayed neutron fractions. - -.. _cycle-history: - -Cycle history -------------- -For all cases, ``nCycles`` and ``power`` must be specified by the user. -In the case that only a single state is to be examined (i.e. no burnup), the user need only additionally specify ``nCycles = 1``. - -In the case of burnup, the reactor cycle history may be specified using either the simple or detailed -option. -The simple cycle history consists of the following case settings: - - * ``power`` - * ``nCycles`` (default = 1) - * ``burnSteps`` (default = 4) - * ``availabilityFactor(s)`` (default = 1.0) - * ``cycleLength(s)`` (default = 365.2425) - -In addition, one may optionally use the ``powerFractions`` setting to change the reactor -power between each cycle. -With these settings, a user can define a history in which each cycle may vary -in power, length, and uptime. -The history is restricted, however, to each cycle having a constant power, to -each cycle having the same number of burnup nodes, and to those burnup nodes being -evenly spaced within each cycle. -An example simple cycle history might look like - -.. code-block:: yaml - - power: 1000000 - nCycles: 3 - burnSteps: 2 - cycleLengths: [100, R2] - powerFractions: [1.0, 0.5, 1.0] - availabilityFactors: [0.9, 0.3, 0.93] - -Note the use of the special shorthand list notation, where repeated values in a list can be specified using an "R" followed by the number of times the value is to be repeated. - -The above scheme would represent 3 cycles of operation: - - 1. 100% power for 90 days, split into two segments of 45 days each, followed by 10 days shutdown (i.e. 90% capacity) - - 2. 50% power for 30 days, split into two segments of 15 days each, followed by 70 days shutdown (i.e. 15% capacity) - - 3. 100% power for 93 days, split into two segments of 46.5 days each, followed by 7 days shutdown (i.e. 93% capacity) - -In each cycle, criticality calculations will be performed at 3 nodes evenly-spaced through the uptime portion of the cycle (i.e. ``availabilityFactor``*``powerFraction``), without option for changing node spacing or frequency. -This input format can be useful for quick scoping and certain types of real analyses, but clearly has its limitations. - -To overcome these limitations, the detailed cycle history, consisting of the ``cycles`` setting may be specified instead. -For each cycle, an entry to the ``cycles`` list is made with the following optional fields: - - * ``name`` - * ``power fractions`` - * ``cumulative days``, ``step days``, or ``burn steps`` + ``cycle length`` - * ``availability factor`` - -An example detailed cycle history employing all of these fields could look like - -.. code-block:: yaml - - power: 1000000 - nCycles: 4 - cycles: - - name: A - step days: [1, 1, 98] - power fractions: [0.1, 0.2, 1] - availability factor: 0.1 - - name: B - cumulative days: [2, 72, 78, 86] - power fractions: [0.2, 1.0, 0.95, 0.93] - - name: C - step days: [5, R5] - power fractions: [1, R5] - - cycle length: 100 - burn steps: 2 - availability factor: 0.9 - -Note that repeated values in a list may be again be entered using the shorthand notation for ``step days``, ``power fractions``, and ``availability factors`` (though not ``cumulative days`` because entries must be monotonically increasing). - -Such a scheme would define the following cycles: - - 1. A 2 day power ramp followed by full power operations for 98 days, with three nodes clustered during the ramp and another at the end of the cycle, followed by 900 days of shutdown - - 2. A 2 day power ramp followed by a prolonged period at full power and then a slight power reduction for the last 14 days in the cycle - - 3. Constant full-power operation for 30 days split into six even increments - - 4. Constant full-power operation for 90 days, split into two equal-length 45 day segments, followed by 10 days of downtime - -As can be seen, the detailed cycle history option provides much greated flexibility for simulating realistic operations, particularly power ramps or scenarios that call for unevenly spaced burnup nodes, such as xenon buildup in the early period of thermal reactor operations. - -.. note:: Although the detailed cycle history option allows for powers to change within each cycle, it should be noted that the power over each step is still considered to be constant. - -.. note:: The ``name`` field of the detailed cycle history is not yet used for anything, but this information will still be accessible on the operator during runtime. - -.. note:: Cycles without names will be given the name ``None`` - -.. warning:: When a detailed cycle history is combined with tight coupling, a subclass of :py:meth:`LatticePhysicsInterface.interactCoupled ` should be used. - -.. _restart-cases: - -Restart cases -------------- - -Oftentimes the user is interested in re-examining just a specific set of time nodes from an existing run. -In these cases, it is sometimes not necessary to rerun an entire reactor history, and one may instead use one of the following options: - - 1. Snapshot, where the reactor state is loaded from a database and just a single time node is run. - - 2. Restart, where the cycle history is loaded from a database and the calculation continues through the remaining specified time history. - -For either of these options, it is possible to alter the specific settings applied to the run by simply adjusting the case settings for the run. -For instance, a run that originally had only neutronics may incorporate thermal hydraulics during a snapshot run by adding in the relevant TH settings. - -.. note:: For either of these options, it is advisable to first create a new case settings file with a name different than the one from which you will be restarting off of, so as to not overwrite those results. - -To run a snapshot, the following settings must be added to your case settings: - - * Set ``runType`` to ``Snapshots`` - * Add a list of cycle/node pairs corresponding to the desired snapshots to ``dumpSnapshot`` formatted as ``'CCCNNN'`` - * Set ``reloadDBName`` to the existing database file that you would like to load the reactor state from - -An example of a snapshot run input: - -.. code-block:: yaml - - runType: Snapshots - reloadDBName: my-old-results.h5 - dumpSnapshot: ['000000', '001002'] # would produce 2 snapshots, at BOL and at node 2 of cycle 1 - -To run a restart, the following settings must be added to your case settings: - - * Set ``runType`` to ``Standard`` - * Set ``loadStyle`` to ``fromDB`` - * Set ``startCycle`` and ``startNode`` to the cycle/node that you would like to continue the calculation from (inclusive). - ``startNode`` may use negative indexing. - * Set ``reloadDBName`` to the existing database file from which you would like to load the reactor history up to the restart point - * If you would like to change the specified reactor history (see :ref:`restart-cases`), keep the history up to the restarting cycle/node - unchanged, and just alter the history after that point. This means that the cycle history specified in your restart run should include - all cycles/nodes up to the end of the simulation. For complicated restarts, it - may be necessary to use the detailed ``cycles`` setting, even if the original case only used the simple history option. - -A few examples of restart cases: - - - Restarting a calculation at a specific cycle/node and continuing for the remainder of the originally-specified cycle history: - .. code-block:: yaml - - # old settings - nCycles: 2 - burnSteps: 2 - cycleLengths: [100, 100] - runType: Standard - loadStyle: fromInput - loadingFile: my-blueprints.yaml - - .. code-block:: yaml - - # restart settings - nCycles: 2 - burnSteps: 2 - cycleLengths: [100, 100] - runType: Standard - loadStyle: fromDB - startCycle: 1 - startNode: 0 - reloadDBName: my-original-results.h5 - - - Add an additional cycle to the end of a case: - .. code-block:: yaml - - # old settings - nCycles: 1 - burnSteps: 2 - cycleLengths: [100] - runType: Standard - loadStyle: fromInput - loadingFile: my-blueprints.yaml - - .. code-block:: yaml - - # restart settings - nCycles: 2 - burnSteps: 2 - cycleLengths: [100, 100] - runType: Standard - loadStyle: fromDB - startCycle: 0 - startNode: -1 - reloadDBName: my-original-results.h5 - - - Restart but cut the reactor history short: - .. code-block:: yaml - - # old settings - nCycles: 3 - burnSteps: 2 - cycleLengths: [100, 100, 100] - runType: Standard - loadStyle: fromInput - loadingFile: my-blueprints.yaml - - .. code-block:: yaml - - # restart settings - nCycles: 2 - burnSteps: 2 - cycleLengths: [100, 100] - runType: Standard - loadStyle: fromDB - startCycle: 1 - startNode: 0 - reloadDBName: my-original-results.h5 - - - Restart with a different number of steps in the third cycle using the detailed ``cycles`` setting: - .. code-block:: yaml - - # old settings - nCycles: 3 - burnSteps: 2 - cycleLengths: [100, 100, 100] - runType: Standard - loadStyle: fromInput - loadingFile: my-blueprints.yaml - - .. code-block:: yaml - - # restart settings - nCycles: 3 - cycles: - - cycle length: 100 - burn steps: 2 - - cycle length: 100 - burn steps: 2 - - cycle length: 100 - burn steps: 4 - runType: Standard - loadStyle: fromDB - startCycle: 2 - startNode: 0 - reloadDBName: my-original-results.h5 - -.. note:: The ``skipCycles`` setting is related to skipping the lattice physics calculation specifically, it is not required to do a restart run. - -.. note:: The *-SHUFFLES.txt file is required to do explicit repeated fuel management. - -.. note:: The restart.dat file is required to repeat the exact fuel management methods during a branch search. These can potentially modify the reactor state in ways that cannot be captures with the SHUFFLES.txt file. - -.. note:: The ISO* binary cross section libraries are required to run cases that skip the lattice physics calculation (e.g. MC**2) - -.. note:: The multigroup flux is not yet stored on the output databases. If you need to do a restart with these values (e.g. for depletion), then you need to reload from neutronics outputs. - -.. note:: Restarting a calculation with an different version of ARMI than what was used to produce the restarting database may result in undefined behavior. \ No newline at end of file diff --git a/doc/user/inputs/settings_report.rst b/doc/user/inputs/settings_report.rst deleted file mode 100644 index 5008e31d0..000000000 --- a/doc/user/inputs/settings_report.rst +++ /dev/null @@ -1,30 +0,0 @@ -Settings Report -=============== -This document lists all the :doc:`settings ` in ARMI. - -They are all accessible to developers -through the :py:class:`armi.settings.caseSettings.Settings` object, which is typically stored in a variable named -``cs``. Interfaces have access to a simulation's settings through ``self.cs``. - - -.. exec:: - from armi import settings - import textwrap - - subclassTables = {} - cs = settings.Settings() - # User textwrap to split up long words that mess up the table. - wrapper = textwrap.TextWrapper(width=25, subsequent_indent='') - wrapper2 = textwrap.TextWrapper(width=10, subsequent_indent='') - content = '\n.. list-table:: ARMI Settings\n :header-rows: 1\n :widths: 30 30 10 10\n \n' - content += ' * - Name\n - Description\n - Default\n - Options\n' - - for setting in sorted(cs.values(), key=lambda s: s.name): - content += ' * - {}\n'.format(' '.join(wrapper.wrap(setting.name))) - content += ' - {}\n'.format(' '.join(wrapper.wrap(str(setting.description) or ''))) - content += ' - {}\n'.format(' '.join(['``{}``'.format(wrapped) for wrapped in wrapper2.wrap(str(getattr(setting, 'default', None)).split("/")[-1])])) - content += ' - {}\n'.format(' '.join(['``{}``'.format(wrapped) for wrapped in wrapper.wrap(str(getattr(setting,'options','') or ''))])) - - content += '\n' - - return content diff --git a/doc/user/inputs/looseCouplingIllustration.dot b/doc/user/looseCouplingIllustration.dot similarity index 100% rename from doc/user/inputs/looseCouplingIllustration.dot rename to doc/user/looseCouplingIllustration.dot diff --git a/doc/user/manual_data_access.rst b/doc/user/manual_data_access.rst index fe138ac33..3a322f308 100644 --- a/doc/user/manual_data_access.rst +++ b/doc/user/manual_data_access.rst @@ -8,20 +8,20 @@ in programmatically building and manipulating inputs and gathering detailed info out of ARMI results. Let's now go into a bit more detail for the power user. Settings and State Variables ----------------------------- +============================ The following links contain large tables describing the various global settings and state parameters in use across ARMI. -* :doc:`Table of all global settings ` -* :doc:`Reactor Parameters ` -* :doc:`Core Parameters ` -* :doc:`Component Parameters ` -* :doc:`Assembly Parameters ` -* :doc:`Block Parameters ` +* :ref:`settings-report` +* :ref:`reactor-parameters-report` +* :ref:`core-parameters-report` +* :ref:`component-parameters-report` +* :ref:`assembly-parameters-report` +* :ref:`block-parameters-report` Accessing Some Interesting Info -------------------------------- +=============================== Often times, you may be interested in the geometric dimensions of various blocks. These are stored on the :py:mod:`components `, and may be accessed as follows:: diff --git a/doc/user/outputs/database.rst b/doc/user/outputs.rst similarity index 71% rename from doc/user/outputs/database.rst rename to doc/user/outputs.rst index dc159d827..d72de91e0 100644 --- a/doc/user/outputs/database.rst +++ b/doc/user/outputs.rst @@ -1,7 +1,71 @@ -***************** -The Database File -***************** +******* +Outputs +******* + +ARMI output files are described in this section. Many outputs may be generated during an ARMI run. They fall into +various categories: + +Framework outputs + Files like the **stdout** and the **database** are produced in nearly all runs. + +Interface outputs + Certain plugins/interfaces produce intermediate output files. + +Physics kernel outputs + If ARMI executes an external physics kernel during a run, its associated output files are often available in the + working directory. These files are typically read by ARMI during the run, and relevant data is transferred onto the + reactor model (and ends up in the ARMI **database**). If the user desires to retain all of the inputs and outputs + associated with the physics kernel runs for a given time step, this can be specified with the ``savePhysicsIO`` setting. + For any time step specified in the list under ``savePhysicsIO``, a ``cXnY/`` folder will be created, and ARMI will store all + inputs and outputs associated with each physics kernel executed at this time step in a folder inside of ``cXnY/``. + The format for specifying a state point is 00X00Y for cycle X, step Y. + +Together the output fully define the analyzed ARMI case. + + +The Standard Output +=================== +The Standard Output (or **stdout**) is a running log of things an ARMI run prints out as it executes a case. It shows +what happened during a run, which inputs were used, which warnings were issued, and in some cases, what the summary +results are. Here is an excerpt:: + + =========== Completed BOL Event =========== + + =========== Triggering BOC - cycle 0 Event =========== + =========== 01 - main BOC - cycle 0 =========== + [impt] Beginning of Cycle 0 + =========== 02 - fissionProducts BOC - cycle 0 =========== + =========== 03 - xsGroups BOC - cycle 0 =========== + [xtra] Generating representative blocks for XS + [xtra] Cross section group manager summary + +In a standard run, the various interfaces will loop through and print out messages according to the `verbosity` +setting. In multi-processing runs, the **stdout** shows messages from the primary node first and then shows information +from all other nodes below (with verbosity set by the `branchVerbosity` setting). Sometimes a user will want to set the +verbosity of just one module (.py file) in the code higher than the rest of ARMI, to do so they can set up a custom +logger by placing this line at the top of the file:: + runLog = logging.getLogger(__name__) + +These single-module (file) loggers can be controlled using a the `moduleVerbosity` setting. All of these logger +verbosities can be controlled from the settings file, for example:: + + branchVerbosity: debug + moduleVerbosity: + armi.reactor.reactors: info + verbosity: extra + +If there is an error, a useful message may be printed in the **stdout**, and a full traceback will be provided in the +associated **stderr** file. + +Some Linux users tend to use the **tail** command to monitor the progress of an ARMI run:: + + tail -f myRun.stdout + +This provides live information on the progress. + +The Database File +================= The **database** file is a self-contained complete (or nearly complete) binary representation of the ARMI composite model state during a case. The database contains the text of the input files that were used to create the case, and for each time node, @@ -9,7 +73,7 @@ the values of all composite parameters as well as layout information to help ful reconstruct the structure of the reactor model. Loading Reactor State -===================== +--------------------- Among other things, the database file can be used to recover an ARMI reactor model from any of the time nodes that it contains. This can be useful for performing restart runs, or for doing custom post-processing tasks. To load a reactor state, you will need to @@ -28,7 +92,7 @@ load the reactor state at cycle 5, time node 2 with the following:: r = db.load(5, 2) Extracting Reactor History -========================== +-------------------------- Not only can the database reproduce reactor state for a given time node, it can also extract a history of specific parameters for specific objects through the :py:meth:`armi.bookkeeping.db.Database3.getHistory()` and @@ -48,15 +112,14 @@ following:: Extracting Settings and Blueprints -================================== +---------------------------------- As well as the reactor states for each time node, the database file also stores the input files (blueprints and settings files) used to run the case that generated it. These can be recovered using the `extract-inputs` ARMI entry point. Use `python -m armi extract-inputs --help` for more information. File format -=========== - +----------- The database file format is built on top of the HDF5 format. There are many tools available for viewing, editing, and scripting HDF5 files. The ARMI database uses the `h5py` package for interacting with the underlying data and metadata. @@ -80,7 +143,7 @@ There are many other features of HDF5, but from a usability standpoint that is e information to get started. Database Structure -================== +------------------ The database structure is outlined below. This shows the broad strokes of how the database is put together, but many more details may be gleaned from the in-line documentation of the database modules. @@ -168,7 +231,6 @@ documentation of the database modules. the ``/c{CC}n{NN}/layout/``. See the next table to see a description of the attributes. - Python supports a rich and dynamic type system, which is sometimes difficult to represent with the HDF5 format. Namely, HDF5 only supports dense, homogeneous N-dimensional collections of data in any given dataset. Some parameter values do not fit diff --git a/doc/user/outputs/index.rst b/doc/user/outputs/index.rst deleted file mode 100644 index 9851b956e..000000000 --- a/doc/user/outputs/index.rst +++ /dev/null @@ -1,31 +0,0 @@ -******* -Outputs -******* - -ARMI output files are described in this section. Many outputs may be generated during an ARMI run. They fall into -various categories: - -Framework outputs - Files like the **stdout** and the **database** are produced in nearly all runs. - -Interface outputs - Certain plugins/interfaces produce intermediate output files. - -Physics kernel outputs - If ARMI executes an external physics kernel during a run, its associated output files are often available in the - working directory. These files are typically read by ARMI during the run, and relevant data is transferred onto the - reactor model (and ends up in the ARMI **database**). If the user desires to retain all of the inputs and outputs - associated with the physics kernel runs for a given time step, this can be specified with the ``savePhysicsIO`` setting. - For any time step specified in the list under ``savePhysicsIO``, a ``cXnY/`` folder will be created, and ARMI will store all - inputs and outputs associated with each physics kernel executed at this time step in a folder inside of ``cXnY/``. - The format for specifying a state point is 00X00Y for cycle X, step Y. - -Together the output fully define the analyzed -ARMI case. - ------------ - -.. toctree:: - - stdout - database diff --git a/doc/user/outputs/stdout.rst b/doc/user/outputs/stdout.rst deleted file mode 100644 index 206e0a613..000000000 --- a/doc/user/outputs/stdout.rst +++ /dev/null @@ -1,43 +0,0 @@ -******************* -The Standard Output -******************* - -The Standard Output (or **stdout**) is a running log of things an ARMI run prints out as it executes a case. It shows -what happened during a run, which inputs were used, which warnings were issued, and in some cases, what the summary -results are. Here is an excerpt:: - - =========== Completed BOL Event =========== - - =========== Triggering BOC - cycle 0 Event =========== - =========== 01 - main BOC - cycle 0 =========== - [impt] Beginning of Cycle 0 - =========== 02 - fissionProducts BOC - cycle 0 =========== - =========== 03 - xsGroups BOC - cycle 0 =========== - [xtra] Generating representative blocks for XS - [xtra] Cross section group manager summary - -In a standard run, the various interfaces will loop through and print out messages according to the `verbosity` -setting. In multi-processing runs, the **stdout** shows messages from the primary node first and then shows information -from all other nodes below (with verbosity set by the `branchVerbosity` setting). Sometimes a user will want to set the -verbosity of just one module (.py file) in the code higher than the rest of ARMI, to do so they can set up a custom -logger by placing this line at the top of the file:: - - runLog = logging.getLogger(__name__) - -These single-module (file) loggers can be controlled using a the `moduleVerbosity` setting. All of these logger -verbosities can be controlled from the settings file, for example:: - - branchVerbosity: debug - moduleVerbosity: - armi.reactor.reactors: info - verbosity: extra - -If there is an error, a useful message may be printed in the **stdout**, and a full traceback will be provided in the -associated **stderr** file. - -Some Linux users tend to use the **tail** command to monitor the progress of an ARMI run:: - - tail -f myRun.stdout - -This provides live information on the progress. - diff --git a/doc/user/physics_coupling.rst b/doc/user/physics_coupling.rst index b537fcdc1..2390cba62 100644 --- a/doc/user/physics_coupling.rst +++ b/doc/user/physics_coupling.rst @@ -3,18 +3,18 @@ Physics Coupling **************** Loose Coupling ----------------- +============== ARMI supports loose and tight coupling. Loose coupling is interpreted as one-way coupling between physics for a single time node. For example, a power distribution in cycle 0 node 0 is used to calculate a temperature distribution in cycle 0 node 0. This temperature is then used in cycle 0 node 1 to compute new cross sections and a new power distribution. This process repeats itself for the lifetime of the simulation. -.. graphviz:: inputs/looseCouplingIllustration.dot +.. graphviz:: looseCouplingIllustration.dot Loose coupling is enabled by default in ARMI simulations. Tight Coupling ------------------ +============== Tight coupling is interpreted as two-way communication between physics within a given time node. Revisiting our previous example, enabling tight coupling results in the temperature distribution being used to generate updated cross sections (new temperatures induce changes such as Doppler broadening feedback) and ultimately an updated power distribution. This process is repeated iteratively until a numerical convergence criteria is met. -.. graphviz:: inputs/tightCouplingIllustration.dot +.. graphviz:: tightCouplingIllustration.dot The following settings are involved with enabling tight coupling in ARMI: @@ -41,14 +41,14 @@ The ``tightCouplingSettings`` settings interact with the interfaces available in In the global flux interface, the following norms are used to compute the convergence of :math:`k_{\text{eff}}` and block-wise power. Eigenvalue -^^^^^^^^^^ +---------- The convergence of the eigenvalue is measured through an L2-norm. .. math:: \epsilon = \| k_\text{eff} \|_2 = \left( \left( k_\text{eff,old} - k_\text{eff,new} \right)^2 \right) ^ \frac{1}{2} Block-wise Power -^^^^^^^^^^^^^^^^ +---------------- The block-wise power can be used as a convergence mechanism to avoid the integral effects of :math:`k_{\text{eff}}` (i.e., over and under predictions cancelling each other out) and in turn, can have a different convergence rate. To measure the convergence of the power distribution with the prescribed tolerances (e.g., 1e-4), the power is scaled in the following manner (otherwise the calculation struggles to converge). For an assembly, :math:`a`, we compute the total power of the assembly, diff --git a/doc/user/radial_and_axial_expansion.rst b/doc/user/radial_and_axial_expansion.rst index 8d9cb707f..ca7f28cd7 100644 --- a/doc/user/radial_and_axial_expansion.rst +++ b/doc/user/radial_and_axial_expansion.rst @@ -7,7 +7,7 @@ ARMI natively supports linear expansion in both the radial and axial dimensions. .. _thermalExpansion: Thermal Expansion ------------------ +================= ARMI treats thermal expansion as a linear phenomena using the standard linear expansion relationship, .. math:: @@ -68,9 +68,9 @@ Equation :eq:`linearExpansionFactor` is the expression used by ARMI in :py:meth: .. _radialExpansion: Radial Expansion ----------------- +================ .. _axialExpansion: Axial Expansion ---------------- +=============== diff --git a/doc/user/reactor_parameters_report.rst b/doc/user/reactor_parameters_report.rst index 1dd8ed062..e1e628b7f 100644 --- a/doc/user/reactor_parameters_report.rst +++ b/doc/user/reactor_parameters_report.rst @@ -1,3 +1,5 @@ +.. _reactor-parameters-report: + ****************** Reactor Parameters ****************** diff --git a/doc/user/inputs/tightCouplingIllustration.dot b/doc/user/tightCouplingIllustration.dot similarity index 100% rename from doc/user/inputs/tightCouplingIllustration.dot rename to doc/user/tightCouplingIllustration.dot diff --git a/doc/user/user_install.rst b/doc/user/user_install.rst index 269d9df92..882a76835 100644 --- a/doc/user/user_install.rst +++ b/doc/user/user_install.rst @@ -5,7 +5,7 @@ Installation This section will guide you through installing the ARMI Framework on your machine. Prerequisites -------------- +============= These instructions target users with some software development knowledge. In particular, we assume familiarity with `Python `__, `virtual environments `_, and `Git `_. @@ -28,7 +28,7 @@ You also likely need the following for interacting with the source code reposito * `Git `_ Preparing a Virtual Environment -------------------------------- +=============================== While not *technically* required, we highly recommend installing ARMI into a `virtual environment `_ to assist in dependency management. In short, virtual environments are a mechanism by which a Python user can @@ -60,11 +60,11 @@ library. On Linux, doing so will require some MPI development libraries (e.g. ``sudo apt install libopenmpi-dev``). Getting the code ----------------- +================ Choose one of the following two installation methods depending on your needs. Option 1: Install as a library -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +------------------------------ If you plan on running ARMI without viewing or modifying source code, you may install it with ``pip``, which will automatically discover and install the dependencies. This is useful for quick evaluations or to use it as a dependency @@ -73,7 +73,7 @@ in another project:: (armi-venv) $ pip install https://github.com/terrapower/armi/archive/main.zip Option 2: Install as a repository (for developers) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +-------------------------------------------------- If you'd like to view or change the ARMI source code (common!), you need to clone the ARMI source and then install its dependencies. Clone the ARMI source code from the git repository with:: @@ -97,7 +97,7 @@ Now install ARMI with all its dependencies:: Verifying installation -^^^^^^^^^^^^^^^^^^^^^^ +---------------------- Check the installation status by running:: (armi-venv) $ armi @@ -122,11 +122,11 @@ If it works, congrats! So far so good. Optional Setup --------------- +============== This subsection provides setup for optional items. GUI input -^^^^^^^^^ +--------- To use the :py:mod:`graphical core-map editor ` you will need to also install `wxPython `_. This is not installed by default during armi installation because it can cause installation complexities on some platforms. @@ -135,7 +135,7 @@ In any case, all GUI dependencies can be installed by:: (armi-venv) $ pip install armi[grids] GUI output -^^^^^^^^^^ +---------- ARMI can write VTK and XDMF output files which can be viewed in tools such as `ParaView `_ and `VisIT `_. Download and install those From 280234e897a695126289252ac11fb9ffc4b12158 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 4 Jan 2024 14:54:05 -0800 Subject: [PATCH 116/176] ARMI does not officially support Python 3.6 (#1572) --- doc/user/user_install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user/user_install.rst b/doc/user/user_install.rst index 882a76835..8ab44d8d4 100644 --- a/doc/user/user_install.rst +++ b/doc/user/user_install.rst @@ -12,7 +12,7 @@ particular, we assume familiarity with `Python `__, You must have the following installed before proceeding: -* `Python `__ version 3.6 or later (preferably 64-bit) +* `Python `__ version 3.7 or newer (preferably 64-bit). .. admonition:: The right Python command From 6fbc1b92a0525c2bb27379672251de35ea2357a9 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Mon, 8 Jan 2024 08:15:46 -0800 Subject: [PATCH 117/176] Cleaning up RST formatting in docs (#1573) --- doc/developer/guide.rst | 14 +++++++------- doc/developer/tooling.rst | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/doc/developer/guide.rst b/doc/developer/guide.rst index 0d256a2b0..64bff5fe5 100644 --- a/doc/developer/guide.rst +++ b/doc/developer/guide.rst @@ -15,14 +15,14 @@ package's code during runtime. An approximation of `Composite Design Pattern `_ is used to represent the **Reactor** -in ARMI. In this hierarchy the **Reactor** object has a child **Core** object, and -potentially many generic **Composite** child objects representing ex-core structures. -The **Core** is made of **Assembly** objects, which are in turn made up as a collection -of **Block** objects. :term:`State ` variables may be stored at any level -of this hierarchy using the :py:mod:`armi.reactor.parameters` system to contain results +in ARMI. In this hierarchy the **Reactor** object has a child **Core** object, and +potentially many generic **Composite** child objects representing ex-core structures. +The **Core** is made of **Assembly** objects, which are in turn made up as a collection +of **Block** objects. :term:`State ` variables may be stored at any level +of this hierarchy using the :py:mod:`armi.reactor.parameters` system to contain results (e.g., ``keff``, ``flow rates``, ``power``, ``flux``, etc.). Within each block are - **Components** that define the pin-level geometry. Associated with each Component are -**Material** objects that contain material properties (``density``, ``conductivity``, +**Components** that define the pin-level geometry. Associated with each Component are +**Material** objects that contain material properties (``density``, ``conductivity``, ``heat capacity``, etc.) and isotopic mass fractions. .. note:: Non-core structures (spent fuel pools, core restraint, heat exchangers, etc.) diff --git a/doc/developer/tooling.rst b/doc/developer/tooling.rst index b46d83fa4..b9d8c1bd6 100644 --- a/doc/developer/tooling.rst +++ b/doc/developer/tooling.rst @@ -164,6 +164,7 @@ Every release should follow this process: - Or from another commit: ``git tag 1.0.0 -m "Release v1.0.0"`` - Pushing to the repo: ``git push origin 1.0.0`` - **NOTE** - The ONLY tags in the ARMI repo are for official version releases. + 5. Also add the release notes on `the GitHub UI `__. 6. Follow the instructions `here `_ to archive the new documentation. From f55b0bc0f5969000f714ca81a819cdbd098460d8 Mon Sep 17 00:00:00 2001 From: Zachary Prince Date: Tue, 9 Jan 2024 10:18:49 -0800 Subject: [PATCH 118/176] Fixing docs for PDF build (#1574) --- doc/developer/guide.rst | 4 ++-- doc/developer/profiling.rst | 2 +- doc/tutorials/walkthrough_lwr_inputs.rst | 2 +- doc/user/inputs.rst | 2 +- doc/user/physics_coupling.rst | 2 +- doc/user/radial_and_axial_expansion.rst | 12 ++++++------ 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/doc/developer/guide.rst b/doc/developer/guide.rst index 64bff5fe5..6fddb3248 100644 --- a/doc/developer/guide.rst +++ b/doc/developer/guide.rst @@ -33,7 +33,7 @@ of this hierarchy using the :py:mod:`armi.reactor.parameters` system to contain .. figure:: /.static/armi_reactor_objects.png :align: center - **Figure 1.** The primary data containers in ARMI + The primary data containers in ARMI Each level of the composite pattern hierarchy contains most of its state data in a collection of parameters detailing considerations of how the reactor has progressed @@ -206,7 +206,7 @@ interface stack is traversed in order. .. figure:: /.static/armi_general_flowchart.png :align: center - **Figure 1.** The computational flow of the interface hooks + The computational flow of the interface hooks For example, input checking routines would run at beginning-of-life (BOL), calculation modules might run at every time node, etc. To accommodate these various needs, interface diff --git a/doc/developer/profiling.rst b/doc/developer/profiling.rst index 02a00717c..f7074c627 100644 --- a/doc/developer/profiling.rst +++ b/doc/developer/profiling.rst @@ -19,4 +19,4 @@ This produces images like this: .. figure:: /.static/buildMacros.png :align: center - **Figure 1.** An example of the profiler output rendered to a png. + An example of the profiler output rendered to a png. diff --git a/doc/tutorials/walkthrough_lwr_inputs.rst b/doc/tutorials/walkthrough_lwr_inputs.rst index 84e405338..e4fa3def6 100644 --- a/doc/tutorials/walkthrough_lwr_inputs.rst +++ b/doc/tutorials/walkthrough_lwr_inputs.rst @@ -222,7 +222,7 @@ This should show a simple representation of the block. .. figure:: https://terrapower.github.io/armi/_static/c5g7-mox.png :figclass: align-center - **Figure 1.** A representation of a C5G7 fuel assembly. + A representation of a C5G7 fuel assembly. Here are the full files used in this example: diff --git a/doc/user/inputs.rst b/doc/user/inputs.rst index 02fa47375..e2ee8ceba 100644 --- a/doc/user/inputs.rst +++ b/doc/user/inputs.rst @@ -456,7 +456,7 @@ The ARMI data model is represented schematically below, and the blueprints are d .. figure:: /.static/armi_reactor_objects.png :align: center - **Figure 1.** The primary data containers in ARMI + The primary data containers in ARMI :ref:`blocks `: Defines :py:class:`~armi.reactor.components.component.Component` inputs for a diff --git a/doc/user/physics_coupling.rst b/doc/user/physics_coupling.rst index 2390cba62..f3b76ceaa 100644 --- a/doc/user/physics_coupling.rst +++ b/doc/user/physics_coupling.rst @@ -75,4 +75,4 @@ These assembly-wise convergence parameters are then stored in an array of conver The total convergence of the power distribution is finally measured through the infinity norm (i.e, the max) of :math:`\xi`, .. math:: - \epsilon = \| \xi \|_\inf = \max \xi. + \epsilon = \| \xi \|_{\inf} = \max \xi. diff --git a/doc/user/radial_and_axial_expansion.rst b/doc/user/radial_and_axial_expansion.rst index ca7f28cd7..ea43e858a 100644 --- a/doc/user/radial_and_axial_expansion.rst +++ b/doc/user/radial_and_axial_expansion.rst @@ -23,28 +23,28 @@ where, :math:`\Delta L` and :math:`\Delta T` are the change in length and temper Given Equation :eq:`linearExp`, we can create expressions for the change in length between our "hot" temperature (Equation :eq:`hotExp`) .. math:: - \begin{align} + \begin{aligned} \frac{L_h - L_0}{L_0} &= \alpha(T_h)\left(T_h - T_0\right),\\ \frac{L_h}{L_0} &= 1 + \alpha(T_h)\left(T_h - T_0\right). - \end{align} + \end{aligned} :label: hotExp and "non-reference" temperature, :math:`T_c` (Equation :eq:`nonRefExp`), .. math:: - \begin{align} + \begin{aligned} \frac{L_c - L_0}{L_0} &= \alpha(T_c)\left(T_c - T_0\right),\\ \frac{L_c}{L_0} &= 1 + \alpha(T_c)\left(T_c - T_0\right). - \end{align} + \end{aligned} :label: nonRefExp These are used within ARMI to enable thermal expansion and contraction with a temperature not equal to the reference temperature, :math:`T_0`. By taking the difference between Equation :eq:`hotExp` and :eq:`nonRefExp`, we can obtain an expression relating the change in length, :math:`L_h - L_c`, to the reference length, :math:`L_0`, .. math:: - \begin{align} + \begin{aligned} \frac{L_h - L_0}{L_0} - \frac{L_c - L_0}{L_0} &= \frac{L_h}{L_0} - 1 - \frac{L_c}{L_0} + 1, \\ &= \frac{L_h - L_c}{L_0}. - \end{align} + \end{aligned} :label: diffHotNonRef Using Equations :eq:`diffHotNonRef` and :eq:`nonRefExp`, we can obtain an expression for the change in length, :math:`L_h - L_c`, relative to the non-reference temperature, From f282eead728d20e2f749a19d7ef3702ba7782c9d Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Wed, 10 Jan 2024 15:16:05 -0800 Subject: [PATCH 119/176] Adding longtable class to a table in docs (#1575) --- doc/user/outputs.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/user/outputs.rst b/doc/user/outputs.rst index d72de91e0..8267fa93c 100644 --- a/doc/user/outputs.rst +++ b/doc/user/outputs.rst @@ -150,6 +150,7 @@ documentation of the database modules. .. list-table:: Database structure :header-rows: 1 + :class: longtable * - Name - Type From 4d3aafde25625ab1424f77ba61af39e3ad11b6c5 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 11 Jan 2024 11:51:07 -0800 Subject: [PATCH 120/176] Adding list-table powers to the docs (#1576) --- armi/utils/dochelpers.py | 50 +++++++++++++++++++++++++++++ armi/utils/tests/test_dochelpers.py | 28 ++++++++++++++++ doc/user/inputs.rst | 10 +++--- 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/armi/utils/dochelpers.py b/armi/utils/dochelpers.py index f0cc57d6a..9c23c5aed 100644 --- a/armi/utils/dochelpers.py +++ b/armi/utils/dochelpers.py @@ -68,6 +68,56 @@ def create_table(rst_table, caption=None, align=None, widths=None, width=None): return "\n".join(rst) +def createListTable( + rows, caption=None, align=None, widths=None, width=None, klass=None +): + """Take a list of data, and produce an RST-type string for a list-table. + + Parameters + ---------- + rows: list + List of input data (first row is the header). + align: str + "left", "center", or "right" + widths: str + "auto", "grid", or a list of integers + width: str + `length`_ or `percentage`_ of the current line width + klass: str + Should be "class", but that is a reserved keyword. + "longtable", "special", or something custom + + Returns + ------- + str: RST list-table string + """ + # we need valid input data + assert len(rows) > 1, "Not enough input data." + len0 = len(rows[0]) + for row in rows[1:]: + assert len(row) == len0, "Rows aren't all the same length." + + # build the list-table header block + rst = [".. list-table:: {}".format(caption or "")] + rst += [" :header-rows: 1"] + if klass: + rst += [" :class: {}".format(klass)] + if align: + rst += [" :align: {}".format(align)] + if width: + rst += [" :width: {}".format(width)] + if widths: + rst += [" :widths: " + " ".join([str(w) for w in widths])] + rst += [""] + + # build the list-table data + for row in rows: + rst += [f" * - {row[0]}"] + rst += [f" - {word}" for word in row[1:]] + + return "\n".join(rst) + + class ExecDirective(Directive): """ Execute the specified python code and insert the output into the document. diff --git a/armi/utils/tests/test_dochelpers.py b/armi/utils/tests/test_dochelpers.py index 160513a55..5042fabde 100644 --- a/armi/utils/tests/test_dochelpers.py +++ b/armi/utils/tests/test_dochelpers.py @@ -19,6 +19,7 @@ from armi.utils.dochelpers import ( create_figure, create_table, + createListTable, generateParamTable, generatePluginSettingsTable, ) @@ -73,3 +74,30 @@ def test_createTable(self): self.assertIn("width: 250", table) self.assertIn("widths: [200, 300]", table) self.assertIn("thing", table) + + def test_createListTable(self): + rows = [["h1", "h2"], ["a1", "a2"], ["b1", "b2"]] + table = createListTable( + rows, + caption="awesomeTable", + align="left", + widths=[10, 30], + width=100, + klass="longtable", + ) + + self.assertEqual(len(table), 189) + self.assertIn("awesomeTable", table) + self.assertIn("list-table", table) + self.assertIn("longtable", table) + self.assertIn("width: 100", table) + self.assertIn("widths: 10 30", table) + + with self.assertRaises(AssertionError): + createListTable([]) + + with self.assertRaises(AssertionError): + createListTable([["h1", "h2"]]) + + with self.assertRaises(AssertionError): + createListTable([["h1", "h2"], ["a1", "b2", "c3"]]) diff --git a/doc/user/inputs.rst b/doc/user/inputs.rst index e2ee8ceba..ee62f2006 100644 --- a/doc/user/inputs.rst +++ b/doc/user/inputs.rst @@ -586,12 +586,14 @@ in cm. The following is a list of included component shapes and their dimension additional/custom components with arbitrary dimensions may be provided by the user via plugins. .. exec:: - from tabulate import tabulate from armi.reactor.components import ComponentType + from armi.utils.dochelpers import createListTable - return create_table(tabulate(headers=('Component Name', 'Dimensions'), - tabular_data=[(c.__name__, ', '.join(c.DIMENSION_NAMES)) for c in ComponentType.TYPES.values()], - tablefmt='rst'), caption="Component list") + rows = [['Component Name', 'Dimensions']] + for c in ComponentType.TYPES.values(): + rows.append([c.__name__, ', '.join(c.DIMENSION_NAMES)]) + + return createListTable(rows, widths=[25, 65], klass="longtable") When a ``DerivedShape`` is specified as the final component in a block, its area is inferred from the difference between the area of the block and the sum of the areas From dae2ba07137506eb7dab3663d093106d885391b8 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 11 Jan 2024 15:53:06 -0800 Subject: [PATCH 121/176] Cleaning up documentation (#1578) --- armi/utils/dochelpers.py | 2 +- doc/{user => .static}/looseCouplingIllustration.dot | 0 doc/{user => .static}/tightCouplingIllustration.dot | 0 doc/developer/making_armi_based_apps.rst | 4 ++-- doc/user/physics_coupling.rst | 4 ++-- 5 files changed, 5 insertions(+), 5 deletions(-) rename doc/{user => .static}/looseCouplingIllustration.dot (100%) rename doc/{user => .static}/tightCouplingIllustration.dot (100%) diff --git a/armi/utils/dochelpers.py b/armi/utils/dochelpers.py index 9c23c5aed..6bacbd004 100644 --- a/armi/utils/dochelpers.py +++ b/armi/utils/dochelpers.py @@ -82,7 +82,7 @@ def createListTable( widths: str "auto", "grid", or a list of integers width: str - `length`_ or `percentage`_ of the current line width + length or percentage of the line, surrounded by backticks klass: str Should be "class", but that is a reserved keyword. "longtable", "special", or something custom diff --git a/doc/user/looseCouplingIllustration.dot b/doc/.static/looseCouplingIllustration.dot similarity index 100% rename from doc/user/looseCouplingIllustration.dot rename to doc/.static/looseCouplingIllustration.dot diff --git a/doc/user/tightCouplingIllustration.dot b/doc/.static/tightCouplingIllustration.dot similarity index 100% rename from doc/user/tightCouplingIllustration.dot rename to doc/.static/tightCouplingIllustration.dot diff --git a/doc/developer/making_armi_based_apps.rst b/doc/developer/making_armi_based_apps.rst index 8af292b1f..433abe5a3 100644 --- a/doc/developer/making_armi_based_apps.rst +++ b/doc/developer/making_armi_based_apps.rst @@ -46,8 +46,8 @@ getting started to get an idea of what is available. Some implementation details --------------------------- -One can just monkey-see-monkey-do their own plugins without fully understanding the -following. However, having a deeper understanding of what is going on may be useful. +Plugins are designed to make it easy to build a plugin by copy/pasting from an existing +plugin. However, having a deeper understanding of what is going on may be useful. Feel free to skip this section. The plugin system is built on top of a Python library called `pluggy diff --git a/doc/user/physics_coupling.rst b/doc/user/physics_coupling.rst index f3b76ceaa..15872a385 100644 --- a/doc/user/physics_coupling.rst +++ b/doc/user/physics_coupling.rst @@ -6,7 +6,7 @@ Loose Coupling ============== ARMI supports loose and tight coupling. Loose coupling is interpreted as one-way coupling between physics for a single time node. For example, a power distribution in cycle 0 node 0 is used to calculate a temperature distribution in cycle 0 node 0. This temperature is then used in cycle 0 node 1 to compute new cross sections and a new power distribution. This process repeats itself for the lifetime of the simulation. -.. graphviz:: looseCouplingIllustration.dot +.. graphviz:: /.static/looseCouplingIllustration.dot Loose coupling is enabled by default in ARMI simulations. @@ -14,7 +14,7 @@ Tight Coupling ============== Tight coupling is interpreted as two-way communication between physics within a given time node. Revisiting our previous example, enabling tight coupling results in the temperature distribution being used to generate updated cross sections (new temperatures induce changes such as Doppler broadening feedback) and ultimately an updated power distribution. This process is repeated iteratively until a numerical convergence criteria is met. -.. graphviz:: tightCouplingIllustration.dot +.. graphviz:: /.static/tightCouplingIllustration.dot The following settings are involved with enabling tight coupling in ARMI: From 2e7304bae853541d2e0d5c4f1e37a738f28758eb Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Fri, 12 Jan 2024 13:58:18 -0800 Subject: [PATCH 122/176] Cleaning up the docs... again (#1580) --- armi/materials/tests/test_materials.py | 8 ++++---- doc/developer/parallel_coding.rst | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/armi/materials/tests/test_materials.py b/armi/materials/tests/test_materials.py index abc79f455..aa670d947 100644 --- a/armi/materials/tests/test_materials.py +++ b/armi/materials/tests/test_materials.py @@ -981,13 +981,13 @@ def test_volumetricExpansion(self): def test_linearExpansion(self): """Unit tests for lead materials linear expansion. - .. test:: There is a base class for fluid materials. + .. test:: Fluid materials do not linearly expand, at any temperature. :id: T_ARMI_MAT_FLUID2 :tests: R_ARMI_MAT_FLUID """ - cur = self.mat.linearExpansion(400) - ref = 0.0 - self.assertEqual(cur, ref) + for t in range(300, 901, 25): + cur = self.mat.linearExpansion(t) + self.assertEqual(cur, 0) def test_setDefaultMassFracs(self): """ diff --git a/doc/developer/parallel_coding.rst b/doc/developer/parallel_coding.rst index 154ad9f2c..565f570ef 100644 --- a/doc/developer/parallel_coding.rst +++ b/doc/developer/parallel_coding.rst @@ -223,7 +223,7 @@ shows how different operations can be performed in parallel:: A simplified approach --------------------- -Transferring state to and from a Reactor can be complicated and add a lot of code. An alternative approachis to ensure +Transferring state to and from a Reactor can be complicated and add a lot of code. An alternative approach is to ensure that the reactor state is synchronized across all nodes, and then use the reactor instead of raw data:: class SomeInterface(interfaces.Interface): From a9feedf93e69bca351c080fa8ec44d25465252d6 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Mon, 15 Jan 2024 11:29:43 -0800 Subject: [PATCH 123/176] Adding test crumb finder to CI (#1582) --- .github/workflows/find_test_crumbs.py | 53 +++++++++++++++++++++++++++ .github/workflows/wintests.yaml | 2 + 2 files changed, 55 insertions(+) create mode 100644 .github/workflows/find_test_crumbs.py diff --git a/.github/workflows/find_test_crumbs.py b/.github/workflows/find_test_crumbs.py new file mode 100644 index 000000000..a8bc9d296 --- /dev/null +++ b/.github/workflows/find_test_crumbs.py @@ -0,0 +1,53 @@ +# Copyright 2024 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. + +"""This script exists so we can determine if new tests in CI are leaving crumbs.""" + +import subprocess + +# A list of objects we expect during a run, and don't mind (like pycache dirs). +IGNORED_OBJECTS = [ + ".pytest_cache", + ".tox", + "__pycache__", + "armi.egg-info", + "armi/logs/ARMI.armiRun.", + "armi/logs/armiRun.mpi.log", + "armi/tests/tutorials/case-suite/", + "armi/tests/tutorials/logs/", +] + + +def main(): + # use "git clean" to find all non-tracked files + proc = subprocess.Popen(["git", "clean", "-xnd"], stdout=subprocess.PIPE) + lines = proc.communicate()[0].decode("utf-8").split("\n") + + # clean up the whitespace + lines = [ln.strip() for ln in lines if len(ln.strip())] + + # ignore certain untracked object, like __pycache__ dirs + for ignore in IGNORED_OBJECTS: + lines = [ln for ln in lines if ignore not in ln] + + # fail hard if there are still untracked files + if len(lines): + for line in lines: + print(line) + + raise ValueError("The workspace is dirty; the tests are leaving crumbs!") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/wintests.yaml b/.github/workflows/wintests.yaml index 354ae0813..9faf86900 100644 --- a/.github/workflows/wintests.yaml +++ b/.github/workflows/wintests.yaml @@ -25,3 +25,5 @@ jobs: run: python -m pip install tox tox-gh-actions - name: Run Tox run: tox -e test + - name: Find Test Crumbs + run: python .github/workflows/find_test_crumbs.py From bceafc917c1558827f02a140b3003410db742ed9 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Mon, 15 Jan 2024 13:18:07 -0800 Subject: [PATCH 124/176] Adding interactCoupled to dev docs (#1581) --- README.rst | 2 +- armi/__init__.py | 2 +- armi/bookkeeping/db/tests/__init__.py | 13 ------------- doc/developer/guide.rst | 7 +++++-- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/README.rst b/README.rst index a5a9e43f7..d689dd0f3 100644 --- a/README.rst +++ b/README.rst @@ -405,7 +405,7 @@ The ARMI system is licensed as follows: .. code-block:: none - Copyright 2009-2023 TerraPower, LLC + Copyright 2009-2024 TerraPower, LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/armi/__init__.py b/armi/__init__.py index 4c36c7694..3cc68fb66 100644 --- a/armi/__init__.py +++ b/armi/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2009-2019 TerraPower, LLC +# Copyright 2019 TerraPower, LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/armi/bookkeeping/db/tests/__init__.py b/armi/bookkeeping/db/tests/__init__.py index 03f780d3b..7f2271639 100644 --- a/armi/bookkeeping/db/tests/__init__.py +++ b/armi/bookkeeping/db/tests/__init__.py @@ -13,16 +13,3 @@ # limitations under the License. """Database tests.""" -# Copyright 2009-2019 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. diff --git a/doc/developer/guide.rst b/doc/developer/guide.rst index 6fddb3248..600b9c65b 100644 --- a/doc/developer/guide.rst +++ b/doc/developer/guide.rst @@ -221,8 +221,8 @@ hooks include: * :py:meth:`interactBOC ` -- Beginning of cycle. Happens once per cycle. -* :py:meth:`interactEveryNode ` -- happens - after every node step/flux calculation +* :py:meth:`interactEveryNode ` -- Happens + after every node step/flux calculation. * :py:meth:`interactEOC ` -- End of cycle. @@ -231,6 +231,9 @@ hooks include: * :py:meth:`interactError ` -- When an error occurs, this can run to clean up or print debugging info. +* :py:meth:`interactCoupled ` -- Happens + after every node step/flux calculation, if tight physics coupling is active. + These interaction points are optional in every interface, and you may override one or more of them to suit your needs. You should not change the arguments to the hooks, which are integers. From f2d99cd36ec659147dcefa0c65dc7fdb082b6db6 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Wed, 17 Jan 2024 11:07:23 -0800 Subject: [PATCH 125/176] Removing reference to CaseSettings class (#1586) --- armi/bookkeeping/db/database3.py | 2 +- armi/cases/case.py | 27 ++++++++++---------- armi/cases/inputModifiers/inputModifiers.py | 6 ++--- armi/cases/suite.py | 2 +- armi/cases/suiteBuilder.py | 8 +++--- armi/cli/modify.py | 2 +- armi/interfaces.py | 6 ++--- armi/operators/operator.py | 4 +-- armi/physics/executers.py | 2 +- armi/plugins.py | 2 +- armi/reactor/blueprints/__init__.py | 4 +-- armi/reactor/blueprints/assemblyBlueprint.py | 4 +-- armi/reactor/blueprints/blockBlueprint.py | 4 +-- armi/reactor/converters/uniformMesh.py | 2 +- armi/reactor/reactors.py | 27 +++++++++++--------- armi/reactor/systemLayoutInput.py | 2 +- armi/settings/settingsIO.py | 2 +- armi/settings/tests/test_settings.py | 2 +- armi/tests/tutorials/data_model.ipynb | 2 +- doc/developer/guide.rst | 2 +- doc/release/0.2.rst | 2 +- 21 files changed, 59 insertions(+), 55 deletions(-) diff --git a/armi/bookkeeping/db/database3.py b/armi/bookkeeping/db/database3.py index 15f6224c5..f732f9080 100644 --- a/armi/bookkeeping/db/database3.py +++ b/armi/bookkeeping/db/database3.py @@ -448,7 +448,7 @@ def loadGeometry(self): def writeInputsToDB(self, cs, csString=None, geomString=None, bpString=None): """ - Write inputs into the database based the CaseSettings. + Write inputs into the database based the Settings. This is not DRY on purpose. The goal is that any particular Database implementation should be very stable, so we dont want it to be easy to change diff --git a/armi/cases/case.py b/armi/cases/case.py index ce6cd0af7..38fae6b4a 100644 --- a/armi/cases/case.py +++ b/armi/cases/case.py @@ -13,11 +13,12 @@ # limitations under the License. """ -The ``Case`` object is responsible for running, and executing a set of user inputs. Many -entry points redirect into ``Case`` methods, such as ``clone``, ``compare``, and ``run``. +The ``Case`` object is responsible for running, and executing a set of +user inputs. Many entry points redirect into ``Case`` methods, such as +``clone``, ``compare``, and ``run``. -The ``Case`` object provides an abstraction around ARMI inputs to allow for manipulation and -collection of cases. +The ``Case`` object provides an abstraction around ARMI inputs to allow +for manipulation and collection of cases. See Also -------- @@ -81,8 +82,8 @@ def __init__(self, cs, caseSuite=None, bp=None, geom=None): Parameters ---------- - cs : CaseSettings - CaseSettings for this Case + cs : Settings + Settings for this Case caseSuite : CaseSuite, optional CaseSuite this particular case belongs. Passing this in allows dependency @@ -96,8 +97,8 @@ def __init__(self, cs, caseSuite=None, bp=None, geom=None): ``cs`` as needed. geom : SystemLayoutInput, optional - SystemLayoutInput for this case. If not supplied, it will be loaded from the ``cs`` as - needed. + SystemLayoutInput for this case. If not supplied, it will be loaded from the + ``cs`` as needed. """ self._startTime = time.time() self._caseSuite = caseSuite @@ -374,7 +375,7 @@ def run(self): def _startCoverage(self): """Helper to the Case.run(): spin up the code coverage tooling, - if the CaseSettings file says to. + if the Settings file says to. Returns ------- @@ -402,7 +403,7 @@ def _startCoverage(self): @staticmethod def _endCoverage(userCovFile, cov=None): """Helper to the Case.run(): stop and report code coverage, - if the CaseSettings file says to. + if the Settings file says to. Parameters ---------- @@ -468,7 +469,7 @@ def _getCoverageRcFile(userCovFile, makeCopy=False): def _startProfiling(self): """Helper to the Case.run(): start the Python profiling, - if the CaseSettings file says to. + if the Settings file says to. Returns ------- @@ -485,7 +486,7 @@ def _startProfiling(self): @staticmethod def _endProfiling(profiler=None): """Helper to the Case.run(): stop and report python profiling, - if the CaseSettings file says to. + if the Settings file says to. Parameters ---------- @@ -912,7 +913,7 @@ def copyInterfaceInputs( Parameters ---------- - cs : CaseSettings + cs : Settings The source case settings to find input files destination : str The target directory to copy input files to diff --git a/armi/cases/inputModifiers/inputModifiers.py b/armi/cases/inputModifiers/inputModifiers.py index 0a220a03e..b16a8539f 100644 --- a/armi/cases/inputModifiers/inputModifiers.py +++ b/armi/cases/inputModifiers/inputModifiers.py @@ -20,7 +20,7 @@ class InputModifier: (This class is abstract.) - Subclasses must implement a ``__call__`` method accepting a ``CaseSettings``, + Subclasses must implement a ``__call__`` method accepting a ``Settings``, ``Blueprints``, and ``SystemLayoutInput``. The class attribute ``FAIL_IF_AFTER`` should be a tuple defining what, if any, @@ -64,7 +64,7 @@ class SamplingInputModifier(InputModifier): (This class is abstract.) - Subclasses must implement a ``__call__`` method accepting a ``CaseSettings``, + Subclasses must implement a ``__call__`` method accepting a ``Settings``, ``Blueprints``, and ``SystemLayoutInput``. This is a modified version of the InputModifier abstract class that imposes @@ -110,7 +110,7 @@ class FullCoreModifier(InputModifier): Notes ----- - Besides the core, other grids may also be of interest for expansion, like + Besides the Core, other grids may also be of interest for expansion, like a grid that defines fuel management. However, the expansion of a fuel management schedule to full core is less trivial than just expanding the core itself. Thus, this modifier currently does not attempt diff --git a/armi/cases/suite.py b/armi/cases/suite.py index fb390aa2b..5a3008847 100644 --- a/armi/cases/suite.py +++ b/armi/cases/suite.py @@ -92,7 +92,7 @@ def discover( """ Finds case objects by searching for a pattern of inputs, and adds them to the suite. - This searches for CaseSettings input files and loads them to create Case objects. + This searches for Settings input files and loads them to create Case objects. Parameters ---------- diff --git a/armi/cases/suiteBuilder.py b/armi/cases/suiteBuilder.py index 12baddbf6..c0b604e18 100644 --- a/armi/cases/suiteBuilder.py +++ b/armi/cases/suiteBuilder.py @@ -90,9 +90,9 @@ def addDegreeOfFreedom(self, inputModifiers): Parameters ---------- - inputModifiers : list(callable(CaseSettings, Blueprints, SystemLayoutInput)) + inputModifiers : list(callable(Settings, Blueprints, SystemLayoutInput)) A list of callable objects with the signature - ``(CaseSettings, Blueprints, SystemLayoutInput)``. When these objects are called + ``(Settings, Blueprints, SystemLayoutInput)``. When these objects are called they should perturb the settings, blueprints, and/or geometry by some amount determined by their construction. """ @@ -121,7 +121,7 @@ def buildSuite(self, namingFunc=None): and a tuple of InputModifiers used to edit the case. This should be enough information for someone to derive a meaningful name. - The function should return a string specifying the path of the ``CaseSettings``, this + The function should return a string specifying the path of the ``Settings``, this allows the user to specify the directories where each case will be run. If not supplied the path will be ``./case-suite/<0000>/-<0000>``, where @@ -396,7 +396,7 @@ def buildSuite(self, namingFunc=None): and a tuple of InputModifiers used to edit the case. This should be enough information for someone to derive a meaningful name. - The function should return a string specifying the path of the ``CaseSettings``, this + The function should return a string specifying the path of the ``Settings``, this allows the user to specify the directories where each case will be run. If not supplied the path will be ``./case-suite/<0000>/<title>-<0000>``, where diff --git a/armi/cli/modify.py b/armi/cli/modify.py index 480476436..75c557a58 100644 --- a/armi/cli/modify.py +++ b/armi/cli/modify.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -r""" +""" Search through a directory tree and modify ARMI settings in existing input file(s). All valid settings may be used as keyword arguments. """ diff --git a/armi/interfaces.py b/armi/interfaces.py index b0d2721dc..4b19f4260 100644 --- a/armi/interfaces.py +++ b/armi/interfaces.py @@ -601,7 +601,7 @@ def specifyInputs(cs) -> Dict[Union[str, settings.Setting], List[str]]: relative to the input directory. Absolute paths will not be copied anywhere. - The returned dictionary will enable the source CaseSettings object to + The returned dictionary will enable the source Settings object to be updated to the new file location. While the dictionary keys are recommended to be Setting objects, the name of the setting as a string, e.g., "shuffleLogic", is still interpreted. If the string name does not @@ -618,7 +618,7 @@ def specifyInputs(cs) -> Dict[Union[str, settings.Setting], List[str]]: Parameters ---------- - cs : CaseSettings + cs : Settings The case settings for a particular Case """ return {} @@ -740,7 +740,7 @@ def getActiveInterfaceInfo(cs): Parameters ---------- - cs : CaseSettings + cs : Settings The case settings that activate relevant Interfaces """ interfaceInfo = [] diff --git a/armi/operators/operator.py b/armi/operators/operator.py index c17875d56..526909428 100644 --- a/armi/operators/operator.py +++ b/armi/operators/operator.py @@ -86,7 +86,7 @@ class Operator: Attributes ---------- - cs : CaseSettings object + cs : Settings Global settings that define the run. cycleNames : list of str @@ -127,7 +127,7 @@ def __init__(self, cs): Parameters ---------- - cs : CaseSettings object + cs : Settings Global settings that define the run. Raises diff --git a/armi/physics/executers.py b/armi/physics/executers.py index 73151dff3..15169c72e 100644 --- a/armi/physics/executers.py +++ b/armi/physics/executers.py @@ -89,7 +89,7 @@ def __repr__(self): return f"<{self.__class__.__name__}: {self.label}>" def fromUserSettings(self, cs): - """Set options from a particular CaseSettings object.""" + """Set options from a particular Settings object.""" raise NotImplementedError() def fromReactor(self, reactor): diff --git a/armi/plugins.py b/armi/plugins.py index 5ba26687a..bcda63102 100644 --- a/armi/plugins.py +++ b/armi/plugins.py @@ -381,7 +381,7 @@ def defineSettings() -> List: Define configuration settings for this plugin. This hook allows plugins to provide their own configuration settings, which can - participate in the :py:class:`armi.settings.caseSettings.CaseSettings`. Plugins + participate in the :py:class:`armi.settings.caseSettings.Settings`. Plugins may provide entirely new settings to what are already provided by ARMI, as well as new options or default values for existing settings. For instance, the framework provides a ``neutronicsKernel`` setting for selecting which global diff --git a/armi/reactor/blueprints/__init__.py b/armi/reactor/blueprints/__init__.py index c432c4543..29020a9a2 100644 --- a/armi/reactor/blueprints/__init__.py +++ b/armi/reactor/blueprints/__init__.py @@ -110,7 +110,7 @@ def loadFromCs(cs, roundTrip=False): - """Function to load Blueprints based on supplied ``CaseSettings``.""" + """Function to load Blueprints based on supplied ``Settings``.""" from armi.utils import directoryChangers with directoryChangers.DirectoryChanger(cs.inputDirectory, dumpOnException=False): @@ -231,7 +231,7 @@ def constructAssem(self, cs, name=None, specifier=None): Parameters ---------- - cs : CaseSettings object + cs : Settings Used to apply various modeling options when constructing an assembly. name : str (optional, and should be exclusive with specifier) diff --git a/armi/reactor/blueprints/assemblyBlueprint.py b/armi/reactor/blueprints/assemblyBlueprint.py index 0ef15b43a..54744353e 100644 --- a/armi/reactor/blueprints/assemblyBlueprint.py +++ b/armi/reactor/blueprints/assemblyBlueprint.py @@ -148,8 +148,8 @@ def construct(self, cs, blueprint): Parameters ---------- - cs : CaseSettings - CaseSettings object which containing relevant modeling options. + cs : Settings + Settings object which containing relevant modeling options. blueprint : Blueprint Root blueprint object containing relevant modeling options. """ diff --git a/armi/reactor/blueprints/blockBlueprint.py b/armi/reactor/blueprints/blockBlueprint.py index fd737a6ed..a3326ab06 100644 --- a/armi/reactor/blueprints/blockBlueprint.py +++ b/armi/reactor/blueprints/blockBlueprint.py @@ -86,8 +86,8 @@ def construct( Parameters ---------- - cs : CaseSettings - CaseSettings object for the appropriate simulation. + cs : Settings + Settings object for the appropriate simulation. blueprint : Blueprints Blueprints object containing various detailed information, such as nuclides to model diff --git a/armi/reactor/converters/uniformMesh.py b/armi/reactor/converters/uniformMesh.py index e8b945bea..c28953511 100644 --- a/armi/reactor/converters/uniformMesh.py +++ b/armi/reactor/converters/uniformMesh.py @@ -508,7 +508,7 @@ def initNewReactor(sourceReactor, cs): ---------- sourceReactor : :py:class:`Reactor <armi.reactor.reactors.Reactor>` object. original reactor to be copied - cs: CaseSetting object + cs: Setting Complete settings object """ # developer note: deepcopy on the blueprint object ensures that all relevant blueprints diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index 31efd23f8..8d2744885 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -13,12 +13,15 @@ # limitations under the License. """ -Reactor objects represent the highest level in the hierarchy of structures that compose the system -to be modeled. Core objects represent collections of assemblies. - -Core is a high-level object in the data model in ARMI. They contain assemblies which in turn contain -more refinement in representing the physical reactor. The reactor is the owner of many of the -plant-wide state variables such as keff, cycle, and node. +Reactor objects represent the highest level in the hierarchy of +structures that compose the system to be modeled. Core objects +represent collections of assemblies. + +Core is a high-level object in the data model in ARMI. They +contain assemblies which in turn contain more refinement in +representing the physical reactor. The reactor is the owner of +many of the plant-wide state variables such as keff, cycle, +and node. """ from typing import Optional import collections @@ -170,7 +173,7 @@ def loadFromCs(cs) -> Reactor: Parameters ---------- - cs: CaseSettings + cs: Settings A relevant settings object Returns @@ -559,7 +562,7 @@ def removeAssembliesInRing(self, ringNum, cs, overrideCircularRingMode=False): ---------- ringNum : int The ring to remove - cs: CaseSettings + cs: Settings A relevant settings object overrideCircularRingMode : bool, optional False ~ default: use circular/square/hex rings, just as the reactor defines them @@ -1810,7 +1813,7 @@ def createFreshFeed(self, cs=None): Parameters ---------- - cs : CaseSettings object + cs : Settings Global settings for the case See Also @@ -1829,7 +1832,7 @@ def createAssemblyOfType(self, assemType=None, enrichList=None, cs=None): The assembly type to create enrichList : list weight percent enrichments of each block - cs : CaseSettings object + cs : Settings Global settings for the case Returns @@ -2449,12 +2452,12 @@ def processLoading(self, cs, dbLoad: bool = False): def buildManualZones(self, cs): """ - Build the Zones that are defined manually in the given CaseSettings file, + Build the Zones that are defined manually in the given Settings file, in the `zoneDefinitions` setting. Parameters ---------- - cs : CaseSettings + cs : Settings The standard ARMI settings object Examples diff --git a/armi/reactor/systemLayoutInput.py b/armi/reactor/systemLayoutInput.py index cac411c7f..38573b029 100644 --- a/armi/reactor/systemLayoutInput.py +++ b/armi/reactor/systemLayoutInput.py @@ -551,7 +551,7 @@ def fromReactor(cls, reactor): @classmethod def loadFromCs(cls, cs): - """Function to load Geoemtry based on supplied ``CaseSettings``.""" + """Function to load Geoemtry based on supplied ``Settings``.""" if not cs["geomFile"]: return None diff --git a/armi/settings/settingsIO.py b/armi/settings/settingsIO.py index 9fb02eab1..7f871ce2d 100644 --- a/armi/settings/settingsIO.py +++ b/armi/settings/settingsIO.py @@ -147,7 +147,7 @@ class SettingsReader: Parameters ---------- - cs : CaseSettings + cs : Settings The settings object to read into """ diff --git a/armi/settings/tests/test_settings.py b/armi/settings/tests/test_settings.py index 1d62ae894..79c29f812 100644 --- a/armi/settings/tests/test_settings.py +++ b/armi/settings/tests/test_settings.py @@ -86,7 +86,7 @@ def defineSettings(): ] -class TestCaseSettings(unittest.TestCase): +class TestSettings(unittest.TestCase): def setUp(self): self.cs = caseSettings.Settings() diff --git a/armi/tests/tutorials/data_model.ipynb b/armi/tests/tutorials/data_model.ipynb index 2e88a1b52..b71c61cc9 100644 --- a/armi/tests/tutorials/data_model.ipynb +++ b/armi/tests/tutorials/data_model.ipynb @@ -516,7 +516,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "With this `CaseSettings` object, we could create a brand new `Case` and `Operator` and do all sorts of magic. This way of interacting with ARMI is rather advanced, and beyond the scope of this tutorial." + "With this `Settings` object, we could create a brand new `Case` and `Operator` and do all sorts of magic. This way of interacting with ARMI is rather advanced, and beyond the scope of this tutorial." ] }, { diff --git a/doc/developer/guide.rst b/doc/developer/guide.rst index 600b9c65b..86e434bf6 100644 --- a/doc/developer/guide.rst +++ b/doc/developer/guide.rst @@ -281,7 +281,7 @@ cases, launch the GUI, and perform various testing and utility operations. When invoke ARMI with ``python -m armi run``, the ``__main__.py`` file is loaded and all valid Entry Points are dynamically loaded. The proper entry point (in this case, :py:class:`armi.cli.run.RunEntryPoint`) is invoked. As ARMI initializes itself, settings -are loaded into a :py:class:`CaseSettings <armi.settings.caseSettings.CaseSettings>` +are loaded into a :py:class:`Settings <armi.settings.caseSettings.Settings>` object. From those settings, an :py:class:`Operator <armi.operators.operator.Operator>` subclass is built by a factory and its ``operate`` method is called. This fires up the main ARMI analysis loop and its interface stack is looped over as indicated by user diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst index 356265788..9cfed61e6 100644 --- a/doc/release/0.2.rst +++ b/doc/release/0.2.rst @@ -9,7 +9,7 @@ Release Date: 2023-09-27 What's new in ARMI ------------------ #. Moved the ``Reactor`` assembly number from the global scope to a ``Parameter``. (`PR#1383 <https://github.com/terrapower/armi/pull/1383>`_) -#. Removed the global ``CaseSettings`` object, ``getMasterCs()``, and ``setMasterCs()``. (`PR#1399 <https://github.com/terrapower/armi/pull/1399>`_) +#. Removed the global ``Settings`` object, ``getMasterCs()``, and ``setMasterCs()``. (`PR#1399 <https://github.com/terrapower/armi/pull/1399>`_) #. Moved the Spent Fuel Pool (``sfp``) from the ``Core`` to the ``Reactor``. (`PR#1336 <https://github.com/terrapower/armi/pull/1336>`_) #. Made the ``sfp`` a child of the ``Reactor`` so it is stored in the database. (`PR#1349 <https://github.com/terrapower/armi/pull/1349>`_) #. Broad cleanup of ``Parameters``: filled in all empty units and descriptions, removed unused params. (`PR#1345 <https://github.com/terrapower/armi/pull/1345>`_) From 6fd8402bc54856b0fddde0bf072afc59aee3fc63 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Mon, 22 Jan 2024 10:40:30 -0800 Subject: [PATCH 126/176] Adding implementation description to runLog impl crumbs (#1584) --- armi/runLog.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/armi/runLog.py b/armi/runLog.py index 176d790f9..8a3204ccf 100644 --- a/armi/runLog.py +++ b/armi/runLog.py @@ -326,6 +326,16 @@ def concatenateLogs(logDir=None): .. impl:: Log files from different processes are combined. :id: I_ARMI_LOG_MPI :implements: R_ARMI_LOG_MPI + + The log files are plain text files. Since ARMI is frequently run in parallel, + the situation arises where each ARMI process generates its own plain text log + file. This function combines the separate log files, per process, into one log + file. + + The files are written in numerical order, with the lead process stdout first + then the lead process stderr. Then each other process is written to the + combined file, in order, stdout then stderr. Finally, the original stdout and + stderr files are deleted. """ if logDir is None: logDir = LOG_DIR @@ -503,9 +513,40 @@ class RunLogger(logging.Logger): :id: I_ARMI_LOG :implements: R_ARMI_LOG + Log statements are any text a user wants to record during a run. For instance, + basic notifications of what is happening in the run, simple warnings, or hard + errors. Every log message has an associated log level, controlled by the + "verbosity" of the logging statement in the code. In the ARMI codebase, you + can see many examples of logging: + + .. code-block:: python + + runLog.error("This sort of error might usually terminate the run.") + runLog.warning("Users probably want to know.") + runLog.info("This is the usual verbosity.") + runLog.debug("This is only logged during a debug run.") + + The full list of logging levels is defined in ``_RunLog.getLogLevels()``, and + the developer specifies the verbosity of a run via ``_RunLog.setVerbosity()``. + + At the end of the ARMI-based simulation, the analyst will have a full record of + potentially interesting information they can use to understand their run. + .. impl:: Logging is done to the screen and to file. :id: I_ARMI_LOG_IO :implements: R_ARMI_LOG_IO + + This logger makes it easy for users to add log statements to and ARMI + application, and ARMI will control the flow of those log statements. In + particular, ARMI overrides the normal Python logging tooling, to allow + developers to pipe their log statements to both screen and file. This works for + stdout and stderr. + + At any place in the ARMI application, developers can interject a plain text + logging message, and when that code is hit during an ARMI simulation, the text + will be piped to screen and a log file. By default, the ``logging`` module only + logs to screen, but ARMI adds a ``FileHandler` in the ``RunLog`` constructor + and in py:meth:`armi.runLog._RunLog.startLog`. """ FMT = "%(levelname)s%(message)s" From 4d45672a1110223874fb7c9bbaca13ed6f104496 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Mon, 22 Jan 2024 11:20:12 -0800 Subject: [PATCH 127/176] Adding implementation information to the materials package (#1587) --- armi/materials/__init__.py | 72 +++++++++++++++++++++++++------------- armi/materials/material.py | 25 ++++++++++++- armi/materials/void.py | 6 ++++ 3 files changed, 78 insertions(+), 25 deletions(-) diff --git a/armi/materials/__init__.py b/armi/materials/__init__.py index f231e44b6..84fd7028f 100644 --- a/armi/materials/__init__.py +++ b/armi/materials/__init__.py @@ -23,25 +23,24 @@ As the fundamental macroscopic building blocks of any physical object, these are highly important to reactor analysis. -This module handles the dynamic importing of all the materials defined here at the framework -level as well as in all the attached plugins. It is expected that most teams will -have special material definitions that they will want to define. +This module handles the dynamic importing of all the materials defined here at the +framework level as well as in all the attached plugins. It is expected that most teams +will have special material definitions that they will want to define. It may also make sense in the future to support user-input materials that are not hard-coded into the app. The base class for all materials is in :py:mod:`armi.materials.material`. """ -import pkgutil -import importlib from typing import List +import importlib import inspect +import pkgutil from armi.materials.material import Material -# this will frequently be updated by the CONF_MATERIAL_NAMESPACE_ORDER setting -# during reactor construction (see armi.reactor.reactors.factory) -# This may also be replaced by a more global material registry at some point. +# This will frequently be updated by the CONF_MATERIAL_NAMESPACE_ORDER setting +# during reactor construction (see armi.reactor.reactors.factory). _MATERIAL_NAMESPACE_ORDER = ["armi.materials"] @@ -49,9 +48,23 @@ def setMaterialNamespaceOrder(order): """ Set the material namespace order at the Python interpreter, global level. - .. impl:: Materials can be searched across packages in a defined namespace. - :id: I_ARMI_MAT_NAMESPACE - :implements: R_ARMI_MAT_NAMESPACE + .. impl:: Material collections are defined with an order of precedence in the case + of duplicates. + :id: I_ARMI_MAT_ORDER + :implements: R_ARMI_MAT_ORDER + + An ARMI application will need materials. Materials can be imported from + any code the application has access to, like plugin packages. This leads to + the situation where one ARMI application will want to import multiple + collections of materials. To handle this, ARMI keeps a list of material + namespaces. This is an ordered list of importable packages that ARMI + can search for a particular material by name. + + This automatic exploration of an importable package saves the user the + tedium have having to import or include hundreds of materials manually somehow. + But it comes with a caveat; the list is ordered. If two different namespaces in + the list include a material with the same name, the first one found in the list + is chosen, i.e. earlier namespaces in the list have precedence. """ global _MATERIAL_NAMESPACE_ORDER _MATERIAL_NAMESPACE_ORDER = order @@ -120,30 +133,41 @@ def resolveMaterialClassByName(name: str, namespaceOrder: List[str] = None): Find the first material class that matches a name in an ordered namespace. Names can either be fully resolved class paths (e.g. ``armi.materials.uZr:UZr``) - or simple class names (e.g. ``UZr``). In the latter case, the ``CONF_MATERIAL_NAMESPACE_ORDER`` - setting to allows users to choose which particular material of a common name (like UO2 or HT9) - gets used. + or simple class names (e.g. ``UZr``). In the latter case, the + ``CONF_MATERIAL_NAMESPACE_ORDER`` setting to allows users to choose which + particular material of a common name (like UO2 or HT9) gets used. Input files usually specify a material like UO2. Which particular implementation gets used (Framework's UO2 vs. a user plugins UO2 vs. the Kentucky Transportation Cabinet's UO2) is up to the user at runtime. - .. impl:: Material collections are defined with an order of precedence in the case of duplicates. - :id: I_ARMI_MAT_ORDER - :implements: R_ARMI_MAT_ORDER + .. impl:: Materials can be searched across packages in a defined namespace. + :id: I_ARMI_MAT_NAMESPACE + :implements: R_ARMI_MAT_NAMESPACE + + During the runtime of an ARMI application, but particularly during the + construction of the reactor in memory, materials will be requested by name. At + that point, this code is called to search for that material name. The search + goes through the ordered list of Python namespaces provided. The first time an + instance of that material is found, it is returned. In this way, the first + items in the material namespace list take precedence. + + When a material name is passed to this function, it may be either a simple + name like the string ``"UO2"`` or it may be much more specific, like + ``armi.materials.uraniumOxide:UO2``. Parameters ---------- name : str The material class name to find, e.g. ``"UO2"``. Optionally, a module path - and class name can be provided with a colon separator as ``module:className``, e.g. - ``armi.materials.uraniumOxide:UO2`` for direct specification. + and class name can be provided with a colon separator as ``module:className``, + e.g. ``armi.materials.uraniumOxide:UO2`` for direct specification. namespaceOrder : list of str, optional - A list of namespaces in order of preference in which to search for the material. - If not passed, the value in the global ``MATERIAL_NAMESPACE_ORDER`` will be used, - which is often set by the ``CONF_MATERIAL_NAMESPACE_ORDER`` setting (e.g. - during reactor construction). Any value passed into this argument will be ignored - if the ``name`` is provided with a ``modulePath``. + A list of namespaces in order of preference in which to search for the + material. If not passed, the value in the global ``MATERIAL_NAMESPACE_ORDER`` + will be used, which is often set by the ``CONF_MATERIAL_NAMESPACE_ORDER`` + setting (e.g. during reactor construction). Any value passed into this argument + will be ignored if the ``name`` is provided with a ``modulePath``. Returns ------- diff --git a/armi/materials/material.py b/armi/materials/material.py index cdf4af64f..9ba80ec59 100644 --- a/armi/materials/material.py +++ b/armi/materials/material.py @@ -34,16 +34,29 @@ class Material: """ - A material is made up of elements or isotopes. It has bulk properties like mass density. + A material is made up of elements or isotopes. It has bulk properties like density. .. impl:: The abstract material class. :id: I_ARMI_MAT_PROPERTIES :implements: R_ARMI_MAT_PROPERTIES + The ARMI Materials library is based on the Object-Oriented Programming design + approach, and uses this generic ``Material`` base class. In this class we + define a large number of material properties like density, heat capacity, or + linear expansion coefficient. Specific materials then subclass this base class to + assign particular values to those properties. + .. impl:: Materials generate nuclide mass fractions at instantiation. :id: I_ARMI_MAT_FRACS :implements: R_ARMI_MAT_FRACS + An ARMI material is meant to be able to represent real world materials that + might be used in the construction of a nuclear reactor. As such, they are + not just individual nuclides, but practical materials like a particular + concrete, steel, or water. One of the main things that will be needed to + describe such a material is the exact nuclide fractions. As such, the + constructor of every Material subclass attempts to set these mass fractions. + Attributes ---------- parent : Component @@ -108,6 +121,11 @@ def name(self): .. 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. """ return self._name @@ -725,6 +743,11 @@ def linearExpansion(self, Tk=None, Tc=None): .. impl:: Fluid materials are not thermally expandable. :id: I_ARMI_MAT_FLUID :implements: R_ARMI_MAT_FLUID + + ARMI does not model thermal expansion of fluids. The ``Fluid`` superclass + therefore sets the thermal expansion coefficient to zero. All fluids + subclassing the ``Fluid`` material will inherit this method which sets the + linear expansion coefficient to zero at all temperatures. """ return 0.0 diff --git a/armi/materials/void.py b/armi/materials/void.py index eff62c309..de0ef63b8 100644 --- a/armi/materials/void.py +++ b/armi/materials/void.py @@ -26,6 +26,12 @@ class Void(material.Fluid): .. 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. """ def pseudoDensity(self, Tk: float = None, Tc: float = None) -> float: From 8bfaea69eeedc180d0f907c2348769dcb55ec3da Mon Sep 17 00:00:00 2001 From: Zachary Prince <zachmprince@gmail.com> Date: Mon, 22 Jan 2024 13:55:13 -0700 Subject: [PATCH 128/176] Adding implementation details to tags in globalFlux (#1588) --- .../globalFlux/globalFluxInterface.py | 130 ++++++++++++------ 1 file changed, 88 insertions(+), 42 deletions(-) diff --git a/armi/physics/neutronics/globalFlux/globalFluxInterface.py b/armi/physics/neutronics/globalFlux/globalFluxInterface.py index d7b6cc48d..b34acbfc8 100644 --- a/armi/physics/neutronics/globalFlux/globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/globalFluxInterface.py @@ -122,9 +122,17 @@ def interactEOC(self, cycle=None): def checkEnergyBalance(self): """Check that there is energy balance between the power generated and the specified power. - .. impl:: Validate the energy generate matches user specifications. + .. impl:: Validate the energy generation matches user specifications. :id: I_ARMI_FLUX_CHECK_POWER :implements: R_ARMI_FLUX_CHECK_POWER + + This method checks that the global power computed from flux + evaluation matches the global power specified from the user within a + tolerance; if it does not, a ``ValueError`` is raised. The + global power from the flux solve is computed by summing the + block-wise power in the core. This value is then compared to the + user-specified power and raises an error if relative difference is + above :math:`10^{-5}`. """ powerGenerated = ( self.r.core.calcTotalParam( @@ -247,6 +255,17 @@ def getTightCouplingValue(self): .. impl:: Return k-eff or assembly-wise power distribution for coupled interactions. :id: I_ARMI_FLUX_COUPLING_VALUE :implements: R_ARMI_FLUX_COUPLING_VALUE + + This method either returns the k-eff or assembly-wise power + distribution. If the :py:class:`coupler + <armi.interfaces.TightCoupler>` ``parameter`` member is ``"keff"``, + then this method returns the computed k-eff from the global flux + evaluation. If the ``parameter`` value is ``"power"``, then it + returns a list of power distributions in each assembly. The assembly + power distributions are lists of values representing the block + powers that are normalized to unity based on the assembly total + power. If the value is neither ``"keff"`` or ``"power"``, then this + method returns ``None``. """ if self.coupler.parameter == "keff": return self.r.core.p.keff @@ -348,6 +367,21 @@ class GlobalFluxOptions(executers.ExecutionOptions): :id: I_ARMI_FLUX_OPTIONS :implements: R_ARMI_FLUX_OPTIONS + This class functions as a data structure for setting and retrieving + execution options for performing flux evaluations, these options + involve: + + * What sort of problem is to be solved, i.e. real/adjoint, + eigenvalue/fixed-source, neutron/gamma, boundary conditions + * Convergence criteria for iterative algorithms + * Geometry type and mesh conversion details + * Specific parameters to be calculated after flux has been evaluated + + These options can be retrieved by directly accessing class members. The + options are set by specifying a :py:class:`Settings + <armi.settings.caseSettings.Settings>` object and optionally specifying + a :py:class:`Reactor <armi.reactor.reactors.Reactor>` object. + Attributes ---------- adjoint : bool @@ -523,6 +557,15 @@ class GlobalFluxExecuter(executers.DefaultExecuter): .. impl:: Ensure the mesh in the reactor model is appropriate for neutronics solver execution. :id: I_ARMI_FLUX_GEOM_TRANSFORM :implements: R_ARMI_FLUX_GEOM_TRANSFORM + + The primary purpose of this class is perform geometric and mesh + transformations on the reactor model to ensure a flux evaluation can + properly perform. This includes: + + * Applying a uniform axial mesh for the 3D flux solve + * Expanding symmetrical geometries to full-core if necessary + * Adding/removing edge assemblies if necessary + * Undoing any transformations that might affect downstream calculations """ def __init__(self, options: GlobalFluxOptions, reactor): @@ -1183,10 +1226,6 @@ def computeDpaRate(mgFlux, dpaXs): r""" Compute the DPA rate incurred by exposure of a certain flux spectrum. - .. impl:: Compute DPA rates. - :id: I_ARMI_FLUX_DPA - :implements: R_ARMI_FLUX_DPA - Parameters ---------- mgFlux : list @@ -1200,31 +1239,35 @@ def computeDpaRate(mgFlux, dpaXs): dpaPerSecond : float The dpa/s in this material due to this flux - Notes - ----- - Displacements calculated by displacement XS - .. math:: + .. impl:: Compute DPA rates. + :id: I_ARMI_FLUX_DPA + :implements: R_ARMI_FLUX_DPA + + This method calculates DPA rates using the inputted multigroup flux and DPA cross sections. + Displacements calculated by displacement XS: - \text{Displacement rate} &= \phi N_{\text{HT9}} \sigma \\ - &= (\#/\text{cm}^2/s) \cdot (1/cm^3) \cdot (\text{barn})\\ - &= (\#/\text{cm}^5/s) \cdot \text{(barn)} * 10^{-24} \text{cm}^2/\text{barn} \\ - &= \#/\text{cm}^3/s + .. math:: + \text{Displacement rate} &= \phi N_{\text{HT9}} \sigma \\ + &= (\#/\text{cm}^2/s) \cdot (1/cm^3) \cdot (\text{barn})\\ + &= (\#/\text{cm}^5/s) \cdot \text{(barn)} * 10^{-24} \text{cm}^2/\text{barn} \\ + &= \#/\text{cm}^3/s - :: - DPA rate = displacement density rate / (number of atoms/cc) - = dr [#/cm^3/s] / (nHT9) [1/cm^3] - = flux * barn * 1e-24 + :: + DPA rate = displacement density rate / (number of atoms/cc) + = dr [#/cm^3/s] / (nHT9) [1/cm^3] + = flux * barn * 1e-24 - .. math:: - \frac{\text{dpa}}{s} = \frac{\phi N \sigma}{N} = \phi * \sigma + .. math:: - the Number density of the structural material cancels out. It's in the macroscopic - XS and in the original number of atoms. + \frac{\text{dpa}}{s} = \frac{\phi N \sigma}{N} = \phi * \sigma + + the Number density of the structural material cancels out. It's in the macroscopic + XS and in the original number of atoms. Raises ------ @@ -1266,10 +1309,6 @@ def calcReactionRates(obj, keff, lib): r""" Compute 1-group reaction rates for this object (usually a block). - .. impl:: Return the reaction rates for a given ArmiObject - :id: I_ARMI_FLUX_RX_RATES - :implements: R_ARMI_FLUX_RX_RATES - Parameters ---------- obj : Block @@ -1284,33 +1323,40 @@ def calcReactionRates(obj, keff, lib): lib : XSLibrary Microscopic cross sections to use in computing the reaction rates. - Notes - ----- - Values include: - * Fission - * nufission - * n2n - * absorption + .. impl:: Return the reaction rates for a given ArmiObject + :id: I_ARMI_FLUX_RX_RATES + :implements: R_ARMI_FLUX_RX_RATES + + This method computes 1-group reaction rates for the inputted + :py:class:`ArmiObject <armi.reactor.composites.ArmiObject>` These + reaction rates include: + + * fission + * nufission + * n2n + * absorption - Scatter could be added as well. This function is quite slow so it is - skipped for now as it is uncommonly needed. + Scatter could be added as well. This function is quite slow so it is + skipped for now as it is uncommonly needed. - Reaction rates are: + Reaction rates are: - .. math:: + .. math:: - \Sigma \phi = \sum_{\text{nuclides}} \sum_{\text{energy}} \Sigma \phi + \Sigma \phi = \sum_{\text{nuclides}} \sum_{\text{energy}} \Sigma + \phi - The units of :math:`N \sigma \phi` are:: + The units of :math:`N \sigma \phi` are:: - [#/bn-cm] * [bn] * [#/cm^2/s] = [#/cm^3/s] + [#/bn-cm] * [bn] * [#/cm^2/s] = [#/cm^3/s] - The group-averaged microscopic cross section is: + The group-averaged microscopic cross section is: - .. math:: + .. math:: - \sigma_g = \frac{\int_{E g}^{E_{g+1}} \phi(E) \sigma(E) dE}{\int_{E_g}^{E_{g+1}} \phi(E) dE} + \sigma_g = \frac{\int_{E g}^{E_{g+1}} \phi(E) \sigma(E) + dE}{\int_{E_g}^{E_{g+1}} \phi(E) dE} """ rate = {} for simple in RX_PARAM_NAMES: From 639a04790faa1073615fbf0dce151d6d5a038203 Mon Sep 17 00:00:00 2001 From: Michael Jarrett <mjarrett@terrapower.com> Date: Tue, 23 Jan 2024 07:40:14 -0800 Subject: [PATCH 129/176] Add implementation descriptions for energyGroups and macroXSGenerationInterface (#1593) * Add implementation descriptions * energyGroups.py * macroXSGenerationInterface.py * Black formatting. * Update armi/physics/neutronics/energyGroups.py * Update armi/physics/neutronics/macroXSGenerationInterface.py Co-authored-by: Zachary Prince <zachmprince@gmail.com> * Update armi/physics/neutronics/macroXSGenerationInterface.py Co-authored-by: Zachary Prince <zachmprince@gmail.com> --------- Co-authored-by: Zachary Prince <zachmprince@gmail.com> --- armi/physics/neutronics/energyGroups.py | 26 ++++++++-- .../neutronics/macroXSGenerationInterface.py | 48 +++++++++++++------ 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/armi/physics/neutronics/energyGroups.py b/armi/physics/neutronics/energyGroups.py index 3f430326a..f528cf093 100644 --- a/armi/physics/neutronics/energyGroups.py +++ b/armi/physics/neutronics/energyGroups.py @@ -30,11 +30,21 @@ def getFastFluxGroupCutoff(eGrpStruc): - """Given a constant "fast" energy threshold, return which ARMI energy group index contains this threshold. + """ + Given a constant "fast" energy threshold, return which ARMI energy group + index contains this threshold. .. impl:: Return the energy group index which contains a given energy threshold. :id: I_ARMI_EG_FE :implements: R_ARMI_EG_FE + + This function returns the energy group within a given group structure + that contains the fast flux threshold energy. The threshold energy is + imported from the :py:mod:`constants <armi.physics.neutronics.const>` in + the neutronics module, where it is defined as 100 keV. This is a + standard definition for fast flux. This function also calculates and + returns the fraction of the threshold energy group that is above the 100 + keV threshold. """ gThres = -1 for g, eV in enumerate(eGrpStruc): @@ -70,16 +80,24 @@ def _create_anl_energies_with_group_lethargies(*group_lethargies): def getGroupStructure(name): """ - Return descending neutron energy group upper bounds in eV for a given structure name. + Return descending neutron energy group upper bounds in eV for a given + structure name. .. impl:: Provide the neutron energy group bounds for a given group structure. :id: I_ARMI_EG_NE :implements: R_ARMI_EG_NE + There are several built-in group structures that are defined in this + module, which are stored in a dictionary. This function takes a group + structure name as an input parameter, which it uses as a key for the + group structure dictionary. If the group structure name is valid, it + returns a copy of the energy group structure resulting from the + dictionary lookup. Otherwise, it throws an error. + Notes ----- - Copy of the group structure is return so that modifications of the energy bounds does - not propagate back to the `GROUP_STRUCTURE` dictionary. + Copy of the group structure is return so that modifications of the energy + bounds does not propagate back to the `GROUP_STRUCTURE` dictionary. """ try: return copy.copy(GROUP_STRUCTURE[name]) diff --git a/armi/physics/neutronics/macroXSGenerationInterface.py b/armi/physics/neutronics/macroXSGenerationInterface.py index 79c5c9cb1..18ecb33aa 100644 --- a/armi/physics/neutronics/macroXSGenerationInterface.py +++ b/armi/physics/neutronics/macroXSGenerationInterface.py @@ -136,38 +136,56 @@ def buildMacros( libType="micros", ): """ - Builds block-level macroscopic cross sections for making diffusion equation matrices. + Builds block-level macroscopic cross sections for making diffusion + equation matrices. This will use MPI if armi.context.MPI_SIZE > 1 - Builds G-vectors of the basic XS ('nGamma','fission','nalph','np','n2n','nd','nt') - Builds GxG matrices for scatter matrices + Builds G-vectors of the basic XS + ('nGamma','fission','nalph','np','n2n','nd','nt') Builds GxG matrices + for scatter matrices .. impl:: Build macroscopic cross sections for blocks. :id: I_ARMI_MACRO_XS :implements: R_ARMI_MACRO_XS + This method builds macroscopic cross sections for a user-specified + set of blocks using a specified microscopic neutron or gamma cross + section library. If no blocks are specified, cross sections are + calculated for all blocks in the core. If no library is specified, + the existing r.core.lib is used. The basic arithmetic involved in + generating macroscopic cross sections consists of multiplying + isotopic number densities by isotopic microscopic cross sections and + summing over all isotopes in a composition. The calculation is + implemented in :py:func:`computeMacroscopicGroupConstants + <armi.nuclearDataIO.xsCollections.computeMacroscopicGroupConstants>`. + This method uses an :py:class:`mpiAction + <armi.mpiActions.MpiAction>` to distribute the work of calculating + macroscopic cross sections across the worker processes. + Parameters ---------- lib : library object , optional - If lib is specified, then buildMacros will build macro XS using micro XS data from lib. - If lib = None, then buildMacros will use the existing library self.r.core.lib. If that does - not exist, then buildMacros will use a new nuclearDataIO.ISOTXS object. + If lib is specified, then buildMacros will build macro XS using + micro XS data from lib. If lib = None, then buildMacros will use the + existing library self.r.core.lib. If that does not exist, then + buildMacros will use a new nuclearDataIO.ISOTXS object. buildScatterMatrix : Boolean, optional - If True, all macro XS will be built, including the time-consuming scatter matrix. - If False, only the macro XS that are needed for fluxRecon.computePinMGFluxAndPower - will be built. These include 'transport', 'fission', and a few others. No ng x ng - matrices (such as 'scatter' or 'chi') will be built. Essentially, this option - saves huge runtime for the fluxRecon module. + If True, all macro XS will be built, including the time-consuming + scatter matrix. If False, only the macro XS that are needed for + fluxRecon.computePinMGFluxAndPower will be built. These include + 'transport', 'fission', and a few others. No ng x ng matrices (such + as 'scatter' or 'chi') will be built. Essentially, this option saves + huge runtime for the fluxRecon module. buildOnlyCoolant : Boolean, optional - If True, homogenized macro XS will be built only for NA-23. - If False, the function runs normally. + If True, homogenized macro XS will be built only for NA-23. If + False, the function runs normally. libType : str, optional - The block attribute containing the desired microscopic XS for this block: - either "micros" for neutron XS or "gammaXS" for gamma XS. + The block attribute containing the desired microscopic XS for this + block: either "micros" for neutron XS or "gammaXS" for gamma XS. """ cycle = self.r.p.cycle self.macrosLastBuiltAt = ( From e7479a21fd4adcf4a51d97e75b73ad3f1385200d Mon Sep 17 00:00:00 2001 From: Chris Keckler <ckeckler@terrapower.com> Date: Tue, 23 Jan 2024 10:24:02 -0600 Subject: [PATCH 130/176] Adding longer descriptions for `isotopicDepletion` impls (#1592) --- .../isotopicDepletion/crossSectionTable.py | 50 ++++++++++++++++--- .../isotopicDepletionInterface.py | 9 ++++ 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/armi/physics/neutronics/isotopicDepletion/crossSectionTable.py b/armi/physics/neutronics/isotopicDepletion/crossSectionTable.py index 3af26af80..2f159db01 100644 --- a/armi/physics/neutronics/isotopicDepletion/crossSectionTable.py +++ b/armi/physics/neutronics/isotopicDepletion/crossSectionTable.py @@ -34,15 +34,11 @@ class CrossSectionTable(collections.OrderedDict): """ This is a set of one group cross sections for use with isotopicDepletion analysis. - Really it's a reaction rate table. + It can also double as a reaction rate table. XStable is indexed by nucNames (nG), (nF), (n2n), (nA), (nP) and (n3n) are expected the cross sections are returned in barns - - .. impl:: Generate cross section table. - :id: I_ARMI_DEPL_TABLES0 - :implements: R_ARMI_DEPL_TABLES """ rateTypes = ("nG", "nF", "n2n", "nA", "nP", "n3n") @@ -131,7 +127,19 @@ def getXsecTable( tableFormat="\n{{mcnpId}} {nG:.5e} {nF:.5e} {n2n:.5e} {n3n:.5e} {nA:.5e} {nP:.5e}", ): """ - make a cross section table for external depletion physics code input decks. + Make a cross section table for external depletion physics code input decks. + + .. impl:: Generate a formatted cross section table. + :id: I_ARMI_DEPL_TABLES1 + :implements: R_ARMI_DEPL_TABLES + + Loops over the reaction rates stored as ``self`` to produce a string with + the cross sections for each nuclide in the block. Cross sections may be + populated by :py:meth:`~armi.physics.neutronics.isotopicDepletion.crossSectionTable.makeReactionRateTable`. + + The string will have a header with the table's name formatted according + to ``headerFormat`` followed by rows for each unique nuclide/reaction + combination, where each line is formatted according to ``tableFormat``. Parameters ---------- @@ -167,10 +175,35 @@ def makeReactionRateTable(obj, nuclides: List = None): Often useful in support of depletion. - .. impl:: Generate cross section table. - :id: I_ARMI_DEPL_TABLES1 + .. impl:: Generate a reaction rate table with entries for (nG), (nF), (n2n), (nA), and (nP) reactions. + :id: I_ARMI_DEPL_TABLES0 :implements: R_ARMI_DEPL_TABLES + For a given composite object ``obj`` and a list of nuclides ``nuclides`` + in that object, call ``obj.getReactionRates()`` for each nuclide with a ``nDensity`` parameter of 1.0. + If ``nuclides`` is not specified, use a list of all nuclides in ``obj``. + This will reach upwards through the parents of ``obj`` to the associated + :py:class:`~armi.reactor.reactors.Core` object and pull the ISOTXS library + that is stored there. If ``obj`` does not belong to a ``Core``, a warning + is printed. + + For each child of ``obj``, use the ISOTXS library and the xsID for the associated block + to produce a reaction rate dictionary in units of inverse seconds for + the nuclide specified in the original call to ``obj.getReactionRates()``. + Because ``nDensity`` was originally specified as + 1.0, this dictionary actually represents the reaction rates per unit volume. + If the nuclide is not in the ISOTXS library, a warning is printed. + + Combine the reaction rates for all nuclides into a combined dictionary by + summing together reaction rates of the same type on the same isotope from + each of the children of ``obj``. + + If ``obj`` has a non-zero multi-group flux, sum the group-wise flux into + the total flux and normalize the reaction rates by the total flux, producing + a one-group macroscopic cross section for each reaction type on each + nuclide. Store these values in a + :py:class:`~armi.physics.neutronics.isotopicDepletion.crossSectionTable.CrossSectionTable`. + Parameters ---------- nuclides : list, optional @@ -185,6 +218,7 @@ def makeReactionRateTable(obj, nuclides: List = None): See Also -------- armi.physics.neutronics.isotopicDepletion.isotopicDepletionInterface.CrossSectionTable + armi.reactor.composites.Composite.getReactionRates """ if nuclides is None: nuclides = obj.getNuclides() diff --git a/armi/physics/neutronics/isotopicDepletion/isotopicDepletionInterface.py b/armi/physics/neutronics/isotopicDepletion/isotopicDepletionInterface.py index 185eca2e5..57a591d7e 100644 --- a/armi/physics/neutronics/isotopicDepletion/isotopicDepletionInterface.py +++ b/armi/physics/neutronics/isotopicDepletion/isotopicDepletionInterface.py @@ -45,6 +45,10 @@ def isDepletable(obj: composites.ArmiObject): :id: I_ARMI_DEPL_DEPLETABLE :implements: R_ARMI_DEPL_DEPLETABLE + Uses :py:meth:`~armi.reactor.composite.ArmiObject.hasFlags` or + :py:meth:`~armi.reactor.composite.ArmiObject.containsAtLeastOneChildWithFlags` + to determine if the "depletable" flag is in the ``obj``. If so, returns True. + .. warning:: The ``DEPLETABLE`` flag is automatically added to compositions that have active nuclides. If you explicitly define any flags at all, you must also manually include ``DEPLETABLE`` or else the objects will silently not deplete. @@ -82,6 +86,11 @@ class AbstractIsotopicDepleter: .. impl:: ARMI provides a base class to deplete isotopes. :id: I_ARMI_DEPL_ABC :implements: R_ARMI_DEPL_ABC + + This class provides some basic infrastructure typically needed in depletion + calculations within the ARMI framework. It stores a reactor, operator, + and case settings object, and also defines methods to store and retrieve + the objects which should be depleted based on their names. """ name = None From 6bda86daaa73d1b916160623c1e77679dc39224e Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 23 Jan 2024 10:35:35 -0800 Subject: [PATCH 131/176] Adding more implementation detail to our docstrings (#1585) --- armi/__init__.py | 20 ++++-- armi/apps.py | 37 ++++++---- armi/interfaces.py | 62 ++++++++++------ armi/operators/operator.py | 77 ++++++++++++++++---- armi/operators/operatorMPI.py | 31 ++++---- armi/operators/settingsValidation.py | 18 +++-- armi/plugins.py | 84 ++++++++++++++-------- armi/settings/caseSettings.py | 26 ++++--- armi/settings/fwSettings/globalSettings.py | 28 +++++++- armi/settings/setting.py | 33 ++++----- 10 files changed, 287 insertions(+), 129 deletions(-) diff --git a/armi/__init__.py b/armi/__init__.py index 3cc68fb66..c3603ecc4 100644 --- a/armi/__init__.py +++ b/armi/__init__.py @@ -122,17 +122,27 @@ def init(choice=None, fName=None, cs=None): :id: I_ARMI_SETTING1 :implements: R_ARMI_SETTING + This method initializes an ARMI run, and if successful returns an Operator. + That operator is designed to drive the reactor simulation through time steps to + simulate its operation. This method takes in a settings file or object to + initialize the operator. Whether a settings file or object is supplied, the + operator will be built based on the those settings. Because the total + collection of settings can be modified by developers of ARMI applications, + providing these settings allow ARMI end-users to define their simulation as + granularly as they need. + Parameters ---------- choice : int, optional - Automatically run with this item out of the menu - that would be produced of existing xml files. + Automatically run with this item out of the menu that would be produced by the + existing YAML files. fName : str, optional - An actual case name to load. e.g. ntTwr1.xml + The path to a settings file to load: my_case.yaml - cs : object, optional - If supplied, supercede the other case input methods and use the object directly + cs : Settings, optional + If supplied, this CS object will supercede the other case input methods and use + the object directly. Examples -------- diff --git a/armi/apps.py b/armi/apps.py index 9e9d0d5f7..3020cbef0 100644 --- a/armi/apps.py +++ b/armi/apps.py @@ -42,23 +42,23 @@ class App: """ - The main point of customization for the ARMI Framework. - - The App class is intended to be subclassed in order to customize the functionality - and look-and-feel of the ARMI Framework for a specific use case. An App contains a - plugin manager, which should be populated in ``__init__()`` with a collection of - plugins that are deemed suitable for a given application, as well as other methods - which provide further customization. - - The base App class is also a good place to expose some more convenient ways to get - data out of the Plugin API; calling the ``pluggy`` hooks directly can sometimes be a - pain, as the results returned by the individual plugins may need to be merged and/or - checked for errors. Adding that logic here reduces boilerplate throughout the rest - of the code. + The highest-level of abstraction for defining what happens during an ARMI run. .. impl:: An App has a plugin manager. :id: I_ARMI_APP_PLUGINS :implements: R_ARMI_APP_PLUGINS + + The App class is intended to be subclassed in order to customize the functionality + and look-and-feel of the ARMI Framework for a specific use case. An App contains a + plugin manager, which should be populated in ``__init__()`` with a collection of + plugins that are deemed suitable for a given application, as well as other methods + which provide further customization. + + The base App class is also a good place to expose some more convenient ways to get + data out of the Plugin API; calling the ``pluggy`` hooks directly can sometimes be a + pain, as the results returned by the individual plugins may need to be merged and/or + checked for errors. Adding that logic here reduces boilerplate throughout the rest + of the code. """ name = "armi" @@ -130,6 +130,17 @@ def getSettings(self) -> Dict[str, Setting]: .. 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. """ # Start with framework settings settingDefs = { diff --git a/armi/interfaces.py b/armi/interfaces.py index 4b19f4260..65a3bacf1 100644 --- a/armi/interfaces.py +++ b/armi/interfaces.py @@ -44,24 +44,23 @@ class STACK_ORDER: # noqa: invalid-class-name """ Constants that help determine the order of modules in the interface stack. - Each module specifies an ``ORDER`` constant that specifies where in this order it + Each module defines an ``ORDER`` constant that specifies where in this order it should be placed in the Interface Stack. .. impl:: Define an ordered list of interfaces. :id: I_ARMI_OPERATOR_INTERFACES0 :implements: R_ARMI_OPERATOR_INTERFACES - Notes - ----- - Originally, the ordering was accomplished with a very large if/else construct in ``createInterfaces``. - This made more modular by moving the add/activate logic into each module and replacing the if/else with - just a large hard-coded list of modules in order that could possibly be added. That hard-coded - list presented ``ImportError`` problems when building various subset distributions of ARMI so this ordering - mechanism was created to replace it, allowing the modules to define their required order internally. + At each time node during a simulation, an ordered colletion of Interfaces + are run (referred to as the interface stack). But ARMI does not force the order upon the analyst. + Instead, each Interface registers where in that ordered list it belongs by + giving itself an order number (which can be an integer or a decimal). + This class defines a set of constants which can be imported and used + by Interface developers to define that Interface's position in the stack. - Future improvements may include simply defining what information is required to perform a calculation - and figuring out the ordering from that. It's complex because in coupled simulations, everything - depends on everything. + The constants defined are given names, based on common stack orderings + in the ARMI ecosystem. But in the end, these are just constant values, + and the names they are given are merely suggestions. See Also -------- @@ -94,15 +93,28 @@ class TightCoupler: :id: I_ARMI_OPERATOR_PHYSICS0 :implements: R_ARMI_OPERATOR_PHYSICS + During a simulation, the developers of an ARMI application frequently want to + iterate on some physical calculation until that calculation has converged to + within some small tolerance. This is typically done to solve the nonlinear + dependence of different physical properties of the reactor, like fuel + performance. However, what parameter is being tightly coupled is configurable + by the developer. + + This class provides a way to calculate if a single parameter has converged + based on some convergence tolerance. The user provides the parameter, + tolerance, and a maximum number of iterations to define a basic convergence + calculation. If in the ``isConverged`` method the parameter has not converged, + the number of iterations is incremented, and this class will wait, presuming + another iteration is forthcoming. + Parameters ---------- param : str The name of a parameter defined in the ARMI Reactor model. tolerance : float - Defines the allowable error, epsilon, between the current previous - parameter value(s) to determine if the selected coupling parameter has - been converged. + Defines the allowable error between the current and previous parameter values + to determine if the selected coupling parameter has converged. maxIters : int Maximum number of tight coupling iterations allowed @@ -241,17 +253,23 @@ def getListDimension(listToCheck: list, dim: int = 1) -> int: class Interface: """ - The eponymous Interface between the ARMI Reactor model and modules that operate upon it. - - This defines the operator's contract for interacting with the ARMI reactor model. - It is expected that interact* methods are defined as appropriate for the physics modeling. - - Interface instances are gathered into an interface stack in - :py:meth:`armi.operators.operator.Operator.createInterfaces`. + The eponymous Interface between the ARMI reactor data model and the Plugins. .. impl:: The interface shall allow code execution at important operational points in time. :id: I_ARMI_INTERFACE :implements: R_ARMI_INTERFACE + + The Interface class defines a number methods with names like ``interact***``. + These methods are called in order at each time node. This allows for an + individual Plugin defining multiple interfaces to insert code at the start + or end of a particular time node or cycle during reactor simulation. In this + fashion, the Plugins and thus the Operator control when their code is run. + + The end goal of all this work is to allow the Plugins to carefully tune + when and how they interact with the reactor data model. + + Interface instances are gathered into an interface stack in + :py:meth:`armi.operators.operator.Operator.createInterfaces`. """ # list containing interfaceClass @@ -270,7 +288,7 @@ def getInputFiles(cls, cs): overridden by any concrete class that extends this one. """ - # TODO: This is a terrible variable name. + # TODO: This is a terrible name. function = None """ The function performed by an Interface. This is not required be be defined diff --git a/armi/operators/operator.py b/armi/operators/operator.py index 526909428..1d567e6bf 100644 --- a/armi/operators/operator.py +++ b/armi/operators/operator.py @@ -62,9 +62,10 @@ class Operator: """ - Orchestrates an ARMI run, building all the pieces, looping through the interfaces, and manipulating the reactor. + Orchestrate an ARMI run, building all the pieces, looping through the interfaces, + and manipulating the reactor. - This Standard Operator loops over a user-input number of cycles, each with a + This Operator loops over a user-input number of cycles, each with a user-input number of subcycles (called time nodes). It calls a series of interaction hooks on each of the :py:class:`~armi.interfaces.Interface` in the Interface Stack. @@ -80,10 +81,30 @@ class Operator: :id: I_ARMI_OPERATOR_COMM :implements: R_ARMI_OPERATOR_COMM + A major design feature of ARMI is that the Operator orchestrates the + simulation, and as part of that, the Operator has access to the + Reactor data model. In code, this just means the reactor object is + a mandatory attribute of an instance of the Operator. But conceptually, + this means that while the Operator drives the simulation of the + reactor, all code has access to the same copy of the reactor data + model. This is a crucial idea that allows disparate external nuclear + models to interact; they interact with the ARMI reactor data model. + .. impl:: An operator is built from user settings. :id: I_ARMI_OPERATOR_SETTINGS :implements: R_ARMI_OPERATOR_SETTINGS + A major design feature of ARMI is that a run is built from user settings. + In code, this means that a case ``Settings`` object is passed into this + class to intialize an Operator. Conceptually, this means that the + Operator that controls a reactor simulation is defined by user settings. + Because developers can create their own settings, the user can + control an ARMI simulation with arbitrary granularity in this way. In + practice, settings common control things like: how many cycles a + reactor is being modeled for, how many timesteps are to be modeled + per time node, the verbosity of the logging during the run, and + which modeling steps (such as economics) will be run. + Attributes ---------- cs : Settings @@ -182,6 +203,16 @@ def stepLengths(self): .. impl:: Calculate step lengths from cycles and burn steps. :id: I_ARMI_FW_HISTORY :implements: R_ARMI_FW_HISTORY + + In all computational modeling of physical systems, it is + necessary to break time into discrete chunks. In reactor + modeling, it is common to first break the time a reactor + is simulated for into the practical cycles the reactor + runs. And then those cycles are broken down into smaller + chunks called burn steps. The final step lengths this + method returns is a two-tiered list, where primary indices + correspond to the cycle and secondary indices correspond to + the length of each intra-cycle step (in days). """ if not self._stepLengths: self._stepLengths = getStepLengths(self.cs) @@ -622,27 +653,38 @@ def interactAllEOL(self): Notes ----- - If the interfaces are flagged to be reversed at EOL, they are separated from the main stack and appended - at the end in reverse order. This allows, for example, an interface that must run first to also run last. + If the interfaces are flagged to be reversed at EOL, they are + separated from the main stack and appended at the end in reverse + order. This allows, for example, an interface that must run + first to also run last. """ activeInterfaces = self.getActiveInterfaces("EOL") self._interactAll("EOL", activeInterfaces) def interactAllCoupled(self, coupledIteration): """ - Interact for tight physics coupling over all enabled interfaces. - - Tight coupling implies operator-split iterations between two or more physics solvers at the same solution - point in time. For example, a flux solution might be computed, then a temperature solution, and then - another flux solution based on updated temperatures (which updated densities, dimensions, and Doppler). - - This is distinct from loose coupling, which would simply uses the temperature values from the previous timestep - in the current flux solution. It's also distinct from full coupling where all fields are solved simultaneously. - ARMI supports tight and loose coupling. + Run all interfaces that are involved in tight physics coupling. .. impl:: Physics coupling is driven from Operator. :id: I_ARMI_OPERATOR_PHYSICS1 :implements: R_ARMI_OPERATOR_PHYSICS + + This method runs all the interfaces that are defined as part + of the tight physics coupling of the reactor. Then it returns + if the coupling has converged or not. + + Tight coupling implies the operator has split iterations + between two or more physics solvers at the same solution point + in simulated time. For example, a flux solution might be + computed, then a temperature solution, and then another flux + solution based on updated temperatures (which updates + densities, dimensions, and Doppler). + + This is distinct from loose coupling, which simply uses + the temperature values from the previous timestep in the + current flux solution. It's also distinct from full coupling + where all fields are solved simultaneously. ARMI supports + tight and loose coupling. """ activeInterfaces = self.getActiveInterfaces("Coupled") # Store the previous iteration values before calling interactAllCoupled @@ -924,6 +966,15 @@ def getInterfaces(self): :id: I_ARMI_OPERATOR_INTERFACES :implements: R_ARMI_OPERATOR_INTERFACES + This method returns an ordered list of instances of the Interface + class. This list is useful because at any time node in the + reactor simulation, these interfaces will be called in + sequence to perform various types of calculations. It is + important to note that this Operator instance has a list of + Plugins, and each of those Plugins potentially defines + multiple Interfaces. And these Interfaces define their own + order, separate from the ordering of the Plugins. + Notes ----- Returns a copy so you can manipulate the list in an interface, like dependencies. diff --git a/armi/operators/operatorMPI.py b/armi/operators/operatorMPI.py index eb3b62e37..191ae7d4d 100644 --- a/armi/operators/operatorMPI.py +++ b/armi/operators/operatorMPI.py @@ -15,20 +15,24 @@ """ The MPI-aware variant of the standard ARMI operator. -See :py:class:`~armi.operators.operator.Operator` for the parent class. +.. impl:: There is an MPI-aware variant of the ARMI Operator. + :id: I_ARMI_OPERATOR_MPI + :implements: R_ARMI_OPERATOR_MPI -This sets up the main Operator on the primary MPI node and initializes worker -processes on all other MPI nodes. At certain points in the run, particular interfaces -might call into action all the workers. For example, a depletion or -subchannel T/H module may ask the MPI pool to perform a few hundred -independent physics calculations in parallel. In many cases, this can -speed up the overall execution of an analysis manyfold, if a big enough -computer or computer cluster is available. + This sets up the main Operator on the primary MPI node and initializes + worker processes on all other MPI nodes. At certain points in the run, + particular interfaces might call into action all the workers. For + example, a depletion or subchannel T/H module may ask the MPI pool to + perform a few hundred independent physics calculations in parallel. In + many cases, this can speed up the overall execution of an analysis, + if a big enough computer or computing cluster is available. + + See :py:class:`~armi.operators.operator.Operator` for the parent class. Notes ----- This is not *yet* smart enough to use shared memory when the MPI -tasks are on the same machine. Everything goes through MPI. This can +tasks are on the same machine. Everything goes through MPI. This can be optimized as needed. """ import gc @@ -46,13 +50,7 @@ class OperatorMPI(Operator): - """ - MPI-aware Operator. - - .. impl:: There is an MPI-aware operator. - :id: I_ARMI_OPERATOR_MPI - :implements: R_ARMI_OPERATOR_MPI - """ + """MPI-aware Operator.""" def __init__(self, cs): try: @@ -191,6 +189,7 @@ def workerOperate(self): cmd ) ) + pm = getPluginManager() resetFlags = pm.hook.mpiActionRequiresReset(cmd=cmd) # only reset if all the plugins agree to reset diff --git a/armi/operators/settingsValidation.py b/armi/operators/settingsValidation.py index 015621db8..3defb5f24 100644 --- a/armi/operators/settingsValidation.py +++ b/armi/operators/settingsValidation.py @@ -13,9 +13,10 @@ # limitations under the License. """ -A system to check user settings for validity and provide users with meaningful suggestions to fix. +A system to check user settings for validity and provide users with meaningful +suggestions to fix. -This allows developers to specify a rich set of rules and suggestions for user settings. +This allows developers to define a rich set of rules and suggestions for user settings. These then pop up during initialization of a run, either on the command line or as dialogues in the GUI. They say things like: "Your ___ setting has the value ___, which is impossible. Would you like to switch to ___?" @@ -48,6 +49,15 @@ class Query: .. impl:: Rules to validate and customize a setting's behavior. :id: I_ARMI_SETTINGS_RULES :implements: R_ARMI_SETTINGS_RULES + + This class is meant to represent a generic validation test against a setting. + The goal is: developers create new settings and they want to make sure those + settings are used correctly. As an implementation, users pass in a + ``condition`` function to this class that returns ``True`` or ``False`` based + on the setting name and value. And then this class has a ``resolve`` method + which tests if the condition is met. Optionally, this class also contains a + ``correction`` function that allows users to automatically correct a bad + setting, if the developers can find a clear path forward. """ def __init__(self, condition, statement, question, correction): @@ -57,8 +67,8 @@ def __init__(self, condition, statement, question, correction): Parameters ---------- condition : callable - A callable that returns True or False. If True, - then the query activates its question and potential correction. + A callable that returns True or False. If True, then the query activates + its question and potential correction. statement : str A statement of the problem indicated by a True condition question : str diff --git a/armi/plugins.py b/armi/plugins.py index bcda63102..918ccabbb 100644 --- a/armi/plugins.py +++ b/armi/plugins.py @@ -138,12 +138,22 @@ class ArmiPlugin: """ - An ArmiPlugin provides a namespace to collect hook implementations provided by a - single "plugin". This API is incomplete, unstable, and expected to change. + An ArmiPlugin exposes a collection of hooks that allow users to add a + variety of things to their ARMI application: Interfaces, parameters, + settings, flags, and much more. - .. impl:: Plugins have interfaces to add code to the application. + .. impl:: Plugins add code to the application through interfaces. :id: I_ARMI_PLUGIN :implements: R_ARMI_PLUGIN + + Each plugin has the option of implementing the ``exposeInterfaces`` method, and + this will be used as a plugin hook to add one or more Interfaces to the ARMI + Application. Interfaces can wrap external executables with nuclear modeling + codes in them, or directly implement their logic in Python. But because + Interfaces are Python code, they have direct access to read and write from + ARMI's reactor data model. This Plugin to multiple Interfaces to reactor data + model connection is the primary way that developers add code to an ARMI + application and simulation. """ @staticmethod @@ -152,10 +162,15 @@ def exposeInterfaces(cs) -> List: """ Function for exposing interface(s) to other code. - .. impl:: Plugins have interfaces to the operator. + .. impl:: Plugins can add interfaces to the operator. :id: I_ARMI_PLUGIN_INTERFACES :implements: R_ARMI_PLUGIN_INTERFACES + This method takes in a Settings object and returns a list of Interfaces, + the position of each Interface in the Interface stack, and a list of + arguments to pass to the Interface when initializing it later. These + Interfaces can then be used to add code to a simulation. + Returns ------- list @@ -180,15 +195,30 @@ def defineParameters() -> Dict: :id: I_ARMI_PLUGIN_PARAMS :implements: R_ARMI_PLUGIN_PARAMS + Through this method, plugin developers can create new Parameters. A + parameter can represent any physical property an analyst might want to + track. And they can be added at any level of the reactor data model. + Through this, the developers can extend ARMI and what physical properties + of the reactor they want to calculate, track, and store to the database. + .. impl:: Define an arbitrary physical parameter. :id: I_ARMI_PARAM :implements: R_ARMI_PARAM + Through this method, plugin developers can create new Parameters. A + parameter can represent any physical property an analyst might want to + track. For example, through this method, a plugin developer can add a new + thermodynamic property that adds a thermodynamic parameter to every block + in the reactor. Or they could add a neutronics parameter to every fuel + assembly. A parameter is quite generic. But these parameters will be + tracked in the reactor data model, extend what developers can do with ARMI, + and will be saved to the output database. + Returns ------- dict Keys should be subclasses of ArmiObject, values being a - ParameterDefinitionCollection should be added to the key's perameter + ParameterDefinitionCollection should be added to the key's parameter definitions. Example @@ -243,23 +273,19 @@ def onProcessCoreLoading(core, cs, dbLoad) -> None: @HOOKSPEC def defineFlags() -> Dict[str, Union[int, flags.auto]]: """ - Function to provide new Flags definitions. - - This allows a plugin to provide novel values for the Flags system. - Implementations should return a dictionary mapping flag names to their desired - numerical values. In most cases, no specific value is needed, in which case - :py:class:`armi.utils.flags.auto` should be used. - - Flags should be added to the ARMI system with great care; flag values for each - object are stored in a bitfield, so each additional flag increases the width of - the data needed to store them. Also, due to the `what things are` interpretation - of flags (see :py:mod:`armi.reactor.flags`), new flags should probably refer to - novel design elements, rather than novel behaviors. + Add new flags to the reactor data model, and the simulation. .. impl:: Plugins can define new, unique flags to the system. :id: I_ARMI_FLAG_EXTEND1 :implements: R_ARMI_FLAG_EXTEND + This method allows a plugin developers to provide novel values for + the Flags system. This method returns a dictionary mapping flag names + to their desired numerical values. In most cases, no specific value + is needed, one can be automatically generated using + :py:class:`armi.utils.flags.auto`. (For more information, see + :py:mod:`armi.reactor.flags`.) + See Also -------- armi.reactor.flags @@ -380,21 +406,23 @@ def defineSettings() -> List: """ Define configuration settings for this plugin. - This hook allows plugins to provide their own configuration settings, which can - participate in the :py:class:`armi.settings.caseSettings.Settings`. Plugins - may provide entirely new settings to what are already provided by ARMI, as well - as new options or default values for existing settings. For instance, the - framework provides a ``neutronicsKernel`` setting for selecting which global - physics solver to use. Since we wish to enforce that the user specify a valid - kernel, the settings validator will check to make sure that the user's requested - kernel is among the available options. If a plugin were to provide a new - neutronics kernel (let's say MCNP), it should also define a new option to tell - the settings system that ``"MCNP"`` is a valid option. - .. impl:: Plugins can add settings to the run. :id: I_ARMI_PLUGIN_SETTINGS :implements: R_ARMI_PLUGIN_SETTINGS + This hook allows plugin developers to provide their own configuration + settings, which can participate in the + :py:class:`armi.settings.caseSettings.Settings`. Plugins may provide + entirely new settings to what are already provided by ARMI, as well as + new options or default values for existing settings. For instance, the + framework provides a ``neutronicsKernel`` setting for selecting which + global physics solver to use. Since we wish to enforce that the user + specify a valid kernel, the settings validator will check to make sure + that the user's requested kernel is among the available options. If a + plugin were to provide a new neutronics kernel (let's say MCNP), it + should also define a new option to tell the settings system that + ``"MCNP"`` is a valid option. + Returns ------- list diff --git a/armi/settings/caseSettings.py b/armi/settings/caseSettings.py index 8b516e854..8b76f72e0 100644 --- a/armi/settings/caseSettings.py +++ b/armi/settings/caseSettings.py @@ -48,16 +48,17 @@ class Settings: """ A container for run settings, such as case title, power level, and many more. - It is accessible to most ARMI objects through self.cs (for 'Case Settings'). - It acts largely as a dictionary, and setting values are accessed by keys. - - The Settings object has a 1-to-1 correspondence with the ARMI settings input file. - This file may be created by hand or by the GUI in submitter.py. - .. impl:: Settings are used to define an ARMI run. :id: I_ARMI_SETTING0 :implements: R_ARMI_SETTING + The Settings object is accessible to most ARMI objects through self.cs + (for 'case settings'). It acts largely as a dictionary, and setting values + are accessed by keys. + + The Settings object has a 1-to-1 correspondence with the ARMI settings + input file. This file may be created by hand or by a GUI. + Notes ----- The actual settings in any instance of this class are immutable. @@ -112,6 +113,15 @@ def caseTitle(self): .. impl:: Define a case title to go with the settings. :id: I_ARMI_SETTINGS_META0 :implements: R_ARMI_SETTINGS_META + + Every Settings object has a "case title"; a string for users to + help identify their run. This case title is used in log file + names, it is printed during a run, it is frequently used to + name the settings file. It is designed to be an easy-to-use + and easy-to-understand way to keep track of simulations. The + general idea here is that the average analyst that is using + ARMI will run many ARMI-based simulations, and there needs + to be an easy to identify them all. """ if not self.path: return self.defaultCaseTitle @@ -421,7 +431,7 @@ def writeToYamlStream(self, stream, style="short", settingsSetByUser=[]): Returns ------- - writer : SettingsWriter object + writer : SettingsWriter """ writer = settingsIO.SettingsWriter( self, style=style, settingsSetByUser=settingsSetByUser @@ -435,7 +445,7 @@ def updateEnvironmentSettingsFrom(self, otherCs): Parameters ---------- - otherCs : Settings object + otherCs : Settings A cs object that environment settings will be inherited from. This enables users to run tests with their environment rather than the reference environment diff --git a/armi/settings/fwSettings/globalSettings.py b/armi/settings/fwSettings/globalSettings.py index 5e521d3db..4f88367ba 100644 --- a/armi/settings/fwSettings/globalSettings.py +++ b/armi/settings/fwSettings/globalSettings.py @@ -15,8 +15,9 @@ """ Framework-wide settings definitions and constants. -This should contain Settings definitions for general-purpose "framework" settings. These -should only include settings that are not related to any particular physics or plugins. +This should contain Settings definitions for general-purpose "framework" +settings. These should only include settings that are not related to any +particular physics or plugins. """ import os from typing import List @@ -126,9 +127,32 @@ def defineSettings() -> List[setting.Setting]: :id: I_ARMI_SETTINGS_POWER :implements: R_ARMI_SETTINGS_POWER + ARMI defines a collection of settings by default to be associated + with all runs, and one such setting is ``power``. This is the + total thermal power of the reactor. This is designed to be the + standard power of the reactor core, to be easily set by the user. + There is frequently the need to adjust the power of the reactor + at different cycles. That is done by setting the ``powerFractions`` + setting to a list of fractions of this power. + .. impl:: Define a comment and a versions list to go with the settings. :id: I_ARMI_SETTINGS_META1 :implements: R_ARMI_SETTINGS_META + + Because nuclear analysts have a lot to keep track of when doing + various simulations of a reactor, ARMI provides a ``comment`` + setting that takes an arbitrary string and stores it. This string + will be preserved in the settings file and thus in the database, + and can provide helpful notes for analysts in the future. + + Likewise, it is helpful to know what versions of software were + used in an ARMI application. There is a dictionary-like setting + called ``versions`` that allows users to track the versions of: + ARMI, their ARMI application, and the versions of all the plugins + in their simulation. While it is always helpful to know what + versions of software you run, it is particularly needed in nuclear + engineering where demands will be made to track the exact + versions of code used in simulations. """ settings = [ setting.Setting( diff --git a/armi/settings/setting.py b/armi/settings/setting.py index 2eb5a9a57..4f3c45210 100644 --- a/armi/settings/setting.py +++ b/armi/settings/setting.py @@ -17,13 +17,11 @@ Notes ----- -Rather than having subclases for each setting type, we simply derive -the type based on the type of the default, and we enforce it with -schema validation. This also allows for more complex schema validation -for settings that are more complex dictionaries (e.g. XS, rx coeffs, etc.). - -One reason for complexity of the previous settings implementation was -good interoperability with the GUI widgets. +The type of each setting is derived from the type of the default +value. When users set values to their settings, ARMI enforces +these types with schema validation. This also allows for more +complex schema validation for settings that are more complex +dictionaries (e.g. XS, rx coeffs). """ from collections import namedtuple from typing import List, Optional, Tuple @@ -48,20 +46,19 @@ class Setting: """ A particular setting. - Setting objects hold all associated information of a setting in ARMI and should - typically be accessed through the Settings class methods rather than directly. The - exception being the SettingAdapter class designed for additional GUI related - functionality. - - Setting subclasses can implement custom ``load`` and ``dump`` methods - that can enable serialization (to/from dicts) of custom objects. When - you set a setting's value, the value will be unserialized into - the custom object and when you call ``dump``, it will be serialized. - Just accessing the value will return the actual object in this case. - .. impl:: The setting default is mandatory. :id: I_ARMI_SETTINGS_DEFAULTS :implements: R_ARMI_SETTINGS_DEFAULTS + + Setting objects hold all associated information of a setting in ARMI and should + typically be accessed through the Settings class methods rather than directly. + Settings require a mandatory default value. + + Setting subclasses can implement custom ``load`` and ``dump`` methods that can + enable serialization (to/from dicts) of custom objects. When you set a + setting's value, the value will be unserialized into the custom object and when + you call ``dump``, it will be serialized. Just accessing the value will return + the actual object in this case. """ def __init__( From 48dbb06f96a470d15acef7c18fb699e1a430e8ff Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 23 Jan 2024 14:01:37 -0800 Subject: [PATCH 132/176] Adding implementation text to neutronics settings (#1603) --- armi/physics/neutronics/settings.py | 59 ++++++++++++------- .../blueprints/tests/test_gridBlueprints.py | 1 + armi/reactor/converters/uniformMesh.py | 32 ++++++---- 3 files changed, 61 insertions(+), 31 deletions(-) diff --git a/armi/physics/neutronics/settings.py b/armi/physics/neutronics/settings.py index f00e72700..fcbb49c12 100644 --- a/armi/physics/neutronics/settings.py +++ b/armi/physics/neutronics/settings.py @@ -83,11 +83,16 @@ def defineSettings(): - """Standard function to define settings - for neutronics. + """Standard function to define settings; for neutronics. .. impl:: Users to select if gamma cross sections are generated. :id: I_ARMI_GAMMA_XS :implements: R_ARMI_GAMMA_XS + + A single boolean setting can be used to turn on/off the calculation of gamma + cross sections. This is implemented with the usual boolean ``Setting`` logic. + The goal here is performance; save the compute time if the analyst doesn't need + those cross sections. """ settings = [ setting.Setting( @@ -125,8 +130,8 @@ def defineSettings(): label="Multigroup Cross Sections Generation", description="Generate multigroup cross sections for the selected particle " "type(s) using the specified lattice physics kernel (see Lattice Physics " - "tab). When not set, the XS library will be auto-loaded from an existing ISOTXS " - "within then working directory and fail if the ISOTXS does not exist.", + "tab). When not set, the XS library will be auto-loaded from an existing " + "ISOTXS in the working directory, but fail if there is no ISOTXS.", options=["", "Neutron", "Neutron and Gamma"], ), setting.Setting( @@ -178,7 +183,7 @@ def defineSettings(): CONF_EIGEN_PROB, default=True, label="Eigenvalue Problem", - description="Whether this is a eigenvalue problem or a fixed source problem", + description="Is this a eigenvalue problem or a fixed source problem?", ), setting.Setting( CONF_EXISTING_FIXED_SOURCE, @@ -199,7 +204,8 @@ def defineSettings(): CONF_EPS_EIG, default=1e-07, label="Eigenvalue Epsilon", - description="Convergence criteria for calculating the eigenvalue in the global flux solver", + description="Convergence criteria for calculating the eigenvalue in the " + "global flux solver", ), setting.Setting( CONF_EPS_FSAVG, @@ -219,8 +225,8 @@ def defineSettings(): label="Load pad elevation (cm)", description=( "The elevation of the bottom of the above-core load pad (ACLP) in cm " - "from the bottom of the upper grid plate. Used for calculating the load " - "pad dose" + "from the bottom of the upper grid plate. Used for calculating the " + "load pad dose" ), ), setting.Setting( @@ -233,7 +239,8 @@ def defineSettings(): CONF_ACLP_DOSE_LIMIT, default=80.0, label="ALCP dose limit", - description="Dose limit in dpa used to position the above-core load pad (if one exists)", + description="Dose limit in dpa used to position the above-core load pad" + "(if one exists)", ), setting.Setting( CONF_RESTART_NEUTRONICS, @@ -251,7 +258,8 @@ def defineSettings(): CONF_INNERS_, default=0, label="Inner Iterations", - description="XY and Axial partial current sweep inner iterations. 0 lets the neutronics code pick a default.", + description="XY and Axial partial current sweep inner iterations. 0 lets " + "the neutronics code pick a default.", ), setting.Setting( CONF_GRID_PLATE_DPA_XS_SET, @@ -280,34 +288,40 @@ def defineSettings(): CONF_MINIMUM_FISSILE_FRACTION, default=0.045, label="Minimum Fissile Fraction", - description="Minimum fissile fraction (fissile number densities / heavy metal number densities).", + description="Minimum fissile fraction (fissile number densities / heavy " + "metal number densities).", oldNames=[("mc2.minimumFissileFraction", None)], ), setting.Setting( CONF_MINIMUM_NUCLIDE_DENSITY, default=1e-15, label="Minimum nuclide density", - description="Density to use for nuclides and fission products at infinite dilution. " - + "This is also used as the minimum density considered for computing macroscopic cross " - + "sections. It can also be passed to physics plugins.", + description="Density to use for nuclides and fission products at infinite " + "dilution. This is also used as the minimum density considered for " + "computing macroscopic cross sections. It can also be passed to physics " + "plugins.", ), setting.Setting( CONF_INFINITE_DILUTE_CUTOFF, default=1e-10, label="Infinite Dillute Cutoff", - description="Do not model nuclides with density less than this cutoff. Used with PARTISN and SERPENT.", + description="Do not model nuclides with density less than this cutoff. " + "Used with PARTISN and SERPENT.", ), setting.Setting( CONF_TOLERATE_BURNUP_CHANGE, default=0.0, label="Cross Section Burnup Group Tolerance", - description="Burnup window for computing cross sections. If the prior cross sections were computed within the window, new cross sections will not be generated and the prior calculated cross sections will be used.", + description="Burnup window for computing cross sections. If the prior " + "cross sections were computed within the window, new cross sections will " + "not be generated and the prior calculated cross sections will be used.", ), setting.Setting( CONF_XS_BLOCK_REPRESENTATION, default="Average", label="Cross Section Block Averaging Method", - description="The type of averaging to perform when creating cross sections for a group of blocks", + description="The type of averaging to perform when creating cross " + "sections for a group of blocks", options=[ "Median", "Average", @@ -319,7 +333,9 @@ def defineSettings(): CONF_DISABLE_BLOCK_TYPE_EXCLUSION_IN_XS_GENERATION, default=False, label="Include All Block Types in XS Generation", - description="Use all blocks in a cross section group when generating a representative block. When this is disabled only `fuel` blocks will be considered", + description="Use all blocks in a cross section group when generating a " + "representative block. When this is disabled only `fuel` blocks will be " + "considered", ), setting.Setting( CONF_XS_KERNEL, @@ -332,7 +348,8 @@ def defineSettings(): CONF_LATTICE_PHYSICS_FREQUENCY, default="BOC", label="Frequency of lattice physics updates", - description="Define the frequency at which cross sections are updated with new lattice physics interactions.", + description="Define the frequency at which cross sections are updated with " + "new lattice physics interactions.", options=[opt.name for opt in list(LatticePhysicsFrequency)], enforcedOptions=True, ), @@ -346,7 +363,8 @@ def defineSettings(): CONF_XS_BUCKLING_CONVERGENCE, default=1e-05, label="Buckling Convergence Criteria", - description="Convergence criteria for the buckling iteration if it is available in the lattice physics solver", + description="Convergence criteria for the buckling iteration if it is " + "available in the lattice physics solver", oldNames=[ ("mc2BucklingConvergence", None), ("bucklingConvergence", None), @@ -356,7 +374,8 @@ def defineSettings(): CONF_XS_EIGENVALUE_CONVERGENCE, default=1e-05, label="Eigenvalue Convergence Criteria", - description="Convergence criteria for the eigenvalue in the lattice physics kernel", + description="Convergence criteria for the eigenvalue in the lattice " + "physics kernel", ), ] diff --git a/armi/reactor/blueprints/tests/test_gridBlueprints.py b/armi/reactor/blueprints/tests/test_gridBlueprints.py index 9fdf1e3a1..61f1bf0d5 100644 --- a/armi/reactor/blueprints/tests/test_gridBlueprints.py +++ b/armi/reactor/blueprints/tests/test_gridBlueprints.py @@ -192,6 +192,7 @@ [8,6]: assembly9_7 fuel """ +# ruff: noqa: E501 RTH_GEOM = """ <reactor geom="ThetaRZ" symmetry="eighth core periodic"> <assembly azimuthalMesh="4" name="assembly1_1 fuel" rad1="0.0" rad2="14.2857142857" radialMesh="4" theta1="0.0" theta2="0.11556368446681414" /> diff --git a/armi/reactor/converters/uniformMesh.py b/armi/reactor/converters/uniformMesh.py index c28953511..dac128566 100644 --- a/armi/reactor/converters/uniformMesh.py +++ b/armi/reactor/converters/uniformMesh.py @@ -347,21 +347,31 @@ def lastBlockTop(a, flags): class UniformMeshGeometryConverter(GeometryConverter): """ - This geometry converter can be used to change the axial mesh structure of the reactor core. + This geometry converter can be used to change the axial mesh structure of the + reactor core. Notes ----- There are several staticmethods available on this class that allow for: - - Creation of a new reactor without applying a new uniform axial mesh. See: `<UniformMeshGeometryConverter.initNewReactor>` - - Creation of a new assembly with a new axial mesh applied. See: `<UniformMeshGeometryConverter.makeAssemWithUniformMesh>` - - Resetting the parameter state of an assembly back to the defaults for the provided block parameters. See: `<UniformMeshGeometryConverter.clearStateOnAssemblies>` - - Mapping number densities and block parameters between one assembly to another. See: `<UniformMeshGeometryConverter.setAssemblyStateFromOverlaps>` - - This class is meant to be extended for specific physics calculations that require a uniform mesh. - The child types of this class should define custom `reactorParamsToMap` and `blockParamsToMap` attributes, and the `_setParamsToUpdate` method - to specify the precise parameters that need to be mapped in each direction between the non-uniform and uniform mesh assemblies. The definitions should avoid mapping - block parameters in both directions because the mapping process will cause numerical diffusion. The behavior of `setAssemblyStateFromOverlaps` is dependent on the - direction in which the mapping is being applied to prevent the numerical diffusion problem. + - Creation of a new reactor without applying a new uniform axial mesh. See: + `<UniformMeshGeometryConverter.initNewReactor>` + - Creation of a new assembly with a new axial mesh applied. See: + `<UniformMeshGeometryConverter.makeAssemWithUniformMesh>` + - Resetting the parameter state of an assembly back to the defaults for the + provided block parameters. See: + `<UniformMeshGeometryConverter.clearStateOnAssemblies>` + - Mapping number densities and block parameters between one assembly to + another. See: `<UniformMeshGeometryConverter.setAssemblyStateFromOverlaps>` + + This class is meant to be extended for specific physics calculations that require a + uniform mesh. The child types of this class should define custom + `reactorParamsToMap` and `blockParamsToMap` attributes, and the + `_setParamsToUpdate` method to specify the precise parameters that need to be + mapped in each direction between the non-uniform and uniform mesh assemblies. The + definitions should avoid mapping block parameters in both directions because the + mapping process will cause numerical diffusion. The behavior of + `setAssemblyStateFromOverlaps` is dependent on the direction in which the mapping + is being applied to prevent the numerical diffusion problem. - "in" is used when mapping parameters into the uniform assembly from the non-uniform assembly. From 540b4b6632d2168e6bbd00e1c89c3fae7a7af7f6 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 23 Jan 2024 14:16:42 -0800 Subject: [PATCH 133/176] Adding implementation text for Grids (#1602) --- armi/reactor/grids/grid.py | 31 +++++++++++- armi/reactor/grids/hexagonal.py | 68 +++++++++++++++++++++----- armi/reactor/grids/locations.py | 41 +++++++--------- armi/reactor/grids/structuredgrid.py | 27 +++++++--- armi/reactor/grids/tests/test_grids.py | 5 +- 5 files changed, 125 insertions(+), 47 deletions(-) diff --git a/armi/reactor/grids/grid.py b/armi/reactor/grids/grid.py index 8f57fad12..6961816b5 100644 --- a/armi/reactor/grids/grid.py +++ b/armi/reactor/grids/grid.py @@ -39,6 +39,19 @@ class Grid(ABC): :id: I_ARMI_GRID_NEST :implements: R_ARMI_GRID_NEST + The reactor will usually have (i,j,k) coordinates to define a + simple mesh for locating objects in the reactor. But inside that mesh can + be a smaller mesh to define the layout of pins in a reactor, or fuel pellets in + a pin, or the layout of some intricate ex-core structure. + + Every time the :py:class:`armi.reactor.grids.locations.IndexLocation` of an + object in the reactor is returned, ARMI will look to see if the grid this object + is in has a :py:meth:`parent <armi.reactor.grids.locations.IndexLocation.parentLocation>`, + and if so, ARMI will try to sum the + :py:meth:`indices <armi.reactor.grids.locations.IndexLocation.indices>` of the two + nested grids to give a resultant, more finely-grained grid position. ARMI can only + handle grids nested 3 deep. + Parameters ---------- geomType : str or armi.reactor.geometry.GeomType @@ -48,7 +61,6 @@ class Grid(ABC): armiObject : optional, armi.reactor.composites.ArmiObject If given, what is this grid attached to or what does it describe? Something like a :class:`armi.reactor.Core` - """ _geomType: str @@ -88,8 +100,23 @@ def symmetry(self) -> str: """Symmetry applied to the grid. .. impl:: Grids shall be able to repesent 1/3 and full core symmetries. - :id: I_ARMI_GRID_SYMMETRY + :id: I_ARMI_GRID_SYMMETRY0 :implements: R_ARMI_GRID_SYMMETRY + + Every grid contains a :py:class:`armi.reactor.geometry.SymmetryType` or + string that defines a grid as full core or a partial core: 1/3, 1/4, 1/8, or 1/16 + core. The idea is that the user can define 1/3 or 1/4 of the reactor, so + the analysis can be run faster on a smaller reactor. And if a non-full + core reactor grid is defined, the boundaries of the grid can be reflective + or periodic, to determine what should happen at the boundaries of the + reactor core. + + It is important to note, that not all of these geometries will apply to + every reactor or core. If your core is made of hexagonal assemblies, then a + 1/3 core grid would make sense, but not if your reactor core was made up of + square assemblies. Likewise, a hexagonal core would not make be able to + support a 1/4 grid. You want to leave assemblies (and other objects) whole + when dividing a grid up fractionally. """ return geometry.SymmetryType.fromStr(self._symmetry) diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py index 2edd56e94..2365b1001 100644 --- a/armi/reactor/grids/hexagonal.py +++ b/armi/reactor/grids/hexagonal.py @@ -47,13 +47,20 @@ class HexGrid(StructuredGrid): """ Has 6 neighbors in plane. - It is recommended to use :meth:`fromPitch` rather than - calling the ``__init__`` constructor directly. + It is recommended to use :meth:`fromPitch` rather than calling the ``__init__`` + constructor directly. .. impl:: Construct a hexagonal lattice. :id: I_ARMI_GRID_HEX :implements: R_ARMI_GRID_HEX + This class represents a hexagonal ``StructuredGrid``, that is one where the + mesh maps to real, physical coordinates. This hexagonal grid is 2D, and divides + the plane up into regular hexagons. That is, each hexagon is symmetric and + is precisely flush with six neighboring hexagons. This class only allows for + two rotational options: flats up (where two sides of the hexagons are parallel + with the X-axis), and points up (where two sides are parallel with the Y-axis). + Notes ----- In an axial plane (i, j) are as follows (second one is pointedEndUp):: @@ -82,10 +89,22 @@ def fromPitch(pitch, numRings=25, armiObject=None, pointedEndUp=False, symmetry= :id: I_ARMI_GRID_HEX_TYPE :implements: R_ARMI_GRID_HEX_TYPE - .. impl:: The user can specify the symmetry of a hexagonal grid when creating one. + When this method creates a ``HexGrid`` object, it can create a hexagonal + grid with one of two rotations: flats up (where two sides of the hexagons + are parallel with the X-axis), and points up (where two sides are parallel + with the Y-axis). While it is possible to imagine the hexagons being + rotated at other arbitrary angles, those are not supported here. + + .. impl:: When creating a hexagonal grid, the user can specify the symmetry. :id: I_ARMI_GRID_SYMMETRY1 :implements: R_ARMI_GRID_SYMMETRY + When this method creates a ``HexGrid`` object, it takes as an input the + symmetry of the resultant grid. This symmetry can be a string (e.g. "full") + or a ``SymmetryType`` object (e.g. ``FULL_CORE``). If the grid is not full- + core, the method ``getSymmetricEquivalents()`` will be usable to map any + possible grid cell to the ones that are being modeled in the sub-grid. + Parameters ---------- pitch : float @@ -322,11 +341,18 @@ def overlapsWhichSymmetryLine(self, indices: IJType) -> Optional[int]: return symmetryLine def getSymmetricEquivalents(self, indices: IJKType) -> List[IJType]: - """Retrieve e quivalent contents based on 3rd symmetry. + """Retrieve the equivalent indices; return them as-is if this is full core, but + return the symmetric equivalent if this is a 1/3-core grid. - .. impl:: Equivalent contents in 3rd geometry are retrievable. + .. impl:: Equivalent contents in 1/3-core geometries are retrievable. :id: I_ARMI_GRID_EQUIVALENTS :implements: R_ARMI_GRID_EQUIVALENTS + + This method takes in (I,J,K) indices, and if this ``HexGrid`` is full core, + it returns them as-is. If this ``HexGrid`` is 1/3-core, this method will + return the 1/3-core symmetric equivalent. If this grid is any other kind, + this method will just return an error; a hexagonal grid with any other + symmetry is probably an error. """ if ( self.symmetry.domain == geometry.DomainType.THIRD_CORE @@ -344,7 +370,9 @@ def getSymmetricEquivalents(self, indices: IJKType) -> List[IJType]: @staticmethod def _getSymmetricIdenticalsThird(indices) -> List[IJType]: - """This works by rotating the indices by 120 degrees twice, counterclockwise.""" + """This works by rotating the indices by 120 degrees twice, + counterclockwise. + """ i, j = indices[:2] if i == 0 and j == 0: return [] @@ -353,7 +381,8 @@ def _getSymmetricIdenticalsThird(indices) -> List[IJType]: def triangleCoords(self, indices: IJKType) -> numpy.ndarray: """ - Return 6 coordinate pairs representing the centers of the 6 triangles in a hexagon centered here. + Return 6 coordinate pairs representing the centers of the 6 triangles in a + hexagon centered here. Ignores z-coordinate and only operates in 2D for now. """ @@ -377,27 +406,37 @@ def locatorInDomain(self, locator, symmetryOverlap: Optional[bool] = False) -> b return True def isInFirstThird(self, locator, includeTopEdge=False) -> bool: - """True if locator is in first third of hex grid. + """Test if the given locator is in the first 1/3 of the HexGrid. .. impl:: Determine if grid is in first third. :id: I_ARMI_GRID_SYMMETRY_LOC :implements: R_ARMI_GRID_SYMMETRY_LOC + + This is a simple helper method to determine if a given locator (from an + ArmiObject) is in the first 1/3 of the ``HexGrid``. This method does not + attempt to check if this grid is full or 1/3-core. It just does the basic + math of dividing up a hex-assembly reactor core into thirds and testing if + the given location is in the first 1/3 or not. """ ring, pos = self.getRingPos(locator.indices) if ring == 1: return True + maxPosTotal = self.getPositionsInRing(ring) maxPos1 = ring + ring // 2 - 1 maxPos2 = maxPosTotal - ring // 2 + 1 if ring % 2: - # odd ring. Upper edge assem typically not included. + # Odd ring; upper edge assem typically not included. if includeTopEdge: maxPos1 += 1 else: - maxPos2 += 1 # make a table to understand this. + # Even ring; upper edge assem included. + maxPos2 += 1 + if pos <= maxPos1 or pos >= maxPos2: return True + return False def generateSortedHexLocationList(self, nLocs: int): @@ -410,7 +449,7 @@ def generateSortedHexLocationList(self, nLocs: int): by ring number then position number. """ # first, roughly calculate how many rings need to be created to cover nLocs worth of assemblies - nLocs = int(nLocs) # need to make this an integer + nLocs = int(nLocs) # next, generate a list of locations and corresponding distances locList = [] @@ -419,14 +458,16 @@ def generateSortedHexLocationList(self, nLocs: int): for position in range(1, positions + 1): i, j = self.getIndicesFromRingAndPos(ring, position) locList.append(self[(i, j, 0)]) + # round to avoid differences due to floating point math locList.sort( key=lambda loc: ( round(numpy.linalg.norm(loc.getGlobalCoordinates()), 6), - loc.i, # loc.i=ring + loc.i, loc.j, ) - ) # loc.j= pos + ) + return locList[:nLocs] # TODO: this is only used by testing and another method that just needs the count of assemblies @@ -456,4 +497,5 @@ def allPositionsInThird(self, ring, includeEdgeAssems=False): loc = IndexLocation(i, j, 0, None) if self.isInFirstThird(loc, includeEdgeAssems): positions.append(pos) + return positions diff --git a/armi/reactor/grids/locations.py b/armi/reactor/grids/locations.py index 126e2e42e..9bfff1f76 100644 --- a/armi/reactor/grids/locations.py +++ b/armi/reactor/grids/locations.py @@ -351,18 +351,19 @@ class MultiIndexLocation(IndexLocation): """ A collection of index locations that can be used as a spatialLocator. - This allows components with multiplicity>1 to have location information - within a parent grid. The implication is that there are multiple - discrete components, each one residing in one of the actual locators - underlying this collection. - - This class contains an implementation that allows a multi-index - location to be used in the ARMI data model similar to a - individual IndexLocation. + This allows components with multiplicity>1 to have location information within a + parent grid. The implication is that there are multiple discrete components, each + one residing in one of the actual locators underlying this collection. .. impl:: Store components with multiplicity greater than 1 :id: I_ARMI_GRID_MULT :implements: R_ARMI_GRID_MULT + + As not all grids are "full core symmetry", ARMI will sometimes need to track + multiple positions for a single object: one for each symmetric portion of the + reactor. This class doesn't calculate those positions in the reactor, it just + tracks the multiple positions given to it. In practice, this class is mostly + just a list of ``IndexLocation`` objects. """ # MIL's cannot be hashed, so we need to scrape off the implementation from @@ -382,8 +383,8 @@ def __getstate__(self) -> List[IndexLocation]: def __setstate__(self, state: List[IndexLocation]): """ - Unpickle a locator, the grid will attach itself if it was also pickled, otherwise this will - be detached. + Unpickle a locator, the grid will attach itself if it was also pickled, + otherwise this will be detached. """ self.__init__(None) self._locations = state @@ -432,17 +433,14 @@ def indices(self) -> List[numpy.ndarray]: """ Return indices for all locations. - .. impl:: Return the location of all instances of grid components with multiplicity greater than 1. + .. impl:: Return the location of all instances of grid components with + multiplicity greater than 1. :id: I_ARMI_GRID_ELEM_LOC :implements: R_ARMI_GRID_ELEM_LOC - Notes - ----- - Notice that this returns a list of all of the indices, unlike the ``indices()`` - implementation for :py:class:`IndexLocation`. This is intended to make the - behavior of getting the indices from the Locator symmetric with passing a list - of indices to the Grid's ``__getitem__()`` function, which constructs and - returns a ``MultiIndexLocation`` containing those indices. + This method returns the indices of all the ``IndexLocation`` objects. To be + clear, this does not return the ``IndexLocation`` objects themselves. This + is designed to be consistent with the Grid's ``__getitem__()`` method. """ return [loc.indices for loc in self._locations] @@ -476,9 +474,8 @@ def addingIsValid(myGrid: "Grid", parentGrid: "Grid"): """ True if adding a indices from one grid to another is considered valid. - In ARMI we allow the addition of a 1-D axial grid with a 2-D grid. - We do not allow any other kind of adding. This enables the 2D/1D - grid layout in Assemblies/Blocks but does not allow 2D indexing - in pins to become inconsistent. + In ARMI we allow the addition of a 1-D axial grid with a 2-D grid. We do not allow + any other kind of adding. This enables the 2D/1D grid layout in Assemblies/Blocks + but does not allow 2D indexing in pins to become inconsistent. """ return myGrid.isAxialOnly and not parentGrid.isAxialOnly diff --git a/armi/reactor/grids/structuredgrid.py b/armi/reactor/grids/structuredgrid.py index f37c95433..91b569696 100644 --- a/armi/reactor/grids/structuredgrid.py +++ b/armi/reactor/grids/structuredgrid.py @@ -290,11 +290,23 @@ def restoreBackup(self): self._unitSteps, self._bounds, self._offset = self._backup def getCoordinates(self, indices, nativeCoords=False) -> numpy.ndarray: - """Return the coordinates of the center of the mesh cell at the given indices in cm. + """Return the coordinates of the center of the mesh cell at the given indices + in cm. .. impl:: Get the coordinates from a location in a grid. :id: I_ARMI_GRID_GLOBAL_POS :implements: R_ARMI_GRID_GLOBAL_POS + + Probably the most common request of a structure grid will be to give the + grid indices and return the physical coordinates of the center of the mesh + cell. This is super handy in any situation where the coordinates have + physical meaning. + + The math for finding the centroid turns out to be very easy, as the mesh is + defined on the coordinates. So finding the mid-point along one axis is just + taking the upper and lower bounds and dividing by two. And this is done for + all axes. There are no more complicated situations where we need to find + the centroid of a octagon on a rectangular mesh, or the like. """ indices = numpy.array(indices) return self._evaluateMesh( @@ -319,16 +331,14 @@ def _evaluateMesh(self, indices, stepOperator, boundsOperator) -> numpy.ndarray: """ Evaluate some function of indices on this grid. - Recall from above that steps are mesh centered and bounds are mesh edged. + Recall from above that steps are mesh-centered and bounds are mesh-edged. Notes ----- - This method may be able to be simplified. Complications from arbitrary - mixtures of bounds-based and step-based meshing caused it to get bad. - These were separate subclasses first, but in practice almost all cases have some mix - of step-based (hexagons, squares), and bounds based (radial, zeta). - - Improvements welcome! + This method may be simplifiable. Complications arose from mixtures of bounds- + based and step-based meshing. These were separate subclasses, but in practice + many cases have some mix of step-based (hexagons, squares), and bounds based + (radial, zeta). """ boundCoords = [] for ii, bounds in enumerate(self._bounds): @@ -342,6 +352,7 @@ def _evaluateMesh(self, indices, stepOperator, boundsOperator) -> numpy.ndarray: result = numpy.zeros(len(indices)) result[self._stepDims] = stepCoords result[self._boundDims] = boundCoords + return result + self._offset def _centroidBySteps(self, indices): diff --git a/armi/reactor/grids/tests/test_grids.py b/armi/reactor/grids/tests/test_grids.py index bbfb5ca2e..4c1f2634b 100644 --- a/armi/reactor/grids/tests/test_grids.py +++ b/armi/reactor/grids/tests/test_grids.py @@ -13,7 +13,6 @@ # limitations under the License. """Tests for grids.""" -# pylint: disable=missing-function-docstring,missing-class-docstring,abstract-method,protected-access,no-self-use,attribute-defined-outside-init from io import BytesIO import math import unittest @@ -126,7 +125,9 @@ def test_recursion(self): assert_allclose(pinIndexLoc.getCompleteIndices(), (1, 5, 0)) def test_recursionPin(self): - """Ensure pin the center assem has axial coordinates consistent with a pin in an off-center assembly.""" + """Ensure pin the center assem has axial coordinates consistent with a pin in + an off-center assembly. + """ core = MockArmiObject() assem = MockArmiObject(core) block = MockArmiObject(assem) From 14c951a05214154a1e6bfb43c728aa714084cea7 Mon Sep 17 00:00:00 2001 From: Zachary Prince <zachmprince@gmail.com> Date: Tue, 23 Jan 2024 16:10:39 -0700 Subject: [PATCH 134/176] Adding description to impl tags in `cases` (#1596) * Adding description to impl tags in `cases` * Apply suggestions from code review Co-authored-by: Michael Huang <lmichaelhuang3@gmail.com> --------- Co-authored-by: Michael Huang <lmichaelhuang3@gmail.com> --- armi/cases/case.py | 25 ++++++++++++++---- armi/cases/inputModifiers/inputModifiers.py | 26 +++++++++--------- armi/cases/suite.py | 14 +++++----- armi/cases/suiteBuilder.py | 29 +++++++++++++++++++-- 4 files changed, 68 insertions(+), 26 deletions(-) diff --git a/armi/cases/case.py b/armi/cases/case.py index 38fae6b4a..dec8e69b4 100644 --- a/armi/cases/case.py +++ b/armi/cases/case.py @@ -331,16 +331,20 @@ def run(self): """ Run an ARMI case. - This initializes an ``Operator``, a ``Reactor`` and invokes - :py:meth:`Operator.operate`! - - It also activates supervisory things like code coverage checking, profiling, - or tracing, if requested by users during debugging. .. impl:: The case class allows for a generic ARMI simulation. :id: I_ARMI_CASE :implements: R_ARMI_CASE + This method is responsible for "running" the ARMI simulation + instigated by the inputted settings. This initializes an + :py:class:`~armi.operators.operator.Operator`, a + :py:class:`~armi.reactor.reactors.Reactor` and invokes + :py:meth:`Operator.operate + <armi.operators.operator.Operator.operate>`. It also activates + supervisory things like code coverage checking, profiling, or + tracing, if requested by users during debugging. + Notes ----- Room for improvement: The coverage, profiling, etc. stuff can probably be moved @@ -565,6 +569,17 @@ def checkInputs(self): :id: I_ARMI_CASE_CHECK :implements: R_ARMI_CASE_CHECK + This method checks the validity of the current settings. It relies + on an :py:class:`~armi.operators.settingsValidation.Inspector` + object from the :py:class:`~armi.operators.operator.Operator` to + generate a list of + :py:class:`~armi.operators.settingsValidation.Query` objects that + represent potential issues in the settings. After gathering the + queries, this method prints a table of query "statements" and + "questions" to the console. If running in an interactive mode, the + user then has the opportunity to address the questions posed by the + queries by either addressing the potential issue or ignoring it. + Returns ------- bool diff --git a/armi/cases/inputModifiers/inputModifiers.py b/armi/cases/inputModifiers/inputModifiers.py index b16a8539f..ebc07a166 100644 --- a/armi/cases/inputModifiers/inputModifiers.py +++ b/armi/cases/inputModifiers/inputModifiers.py @@ -18,22 +18,22 @@ class InputModifier: """ Object that modifies input definitions in some well-defined way. - (This class is abstract.) - - Subclasses must implement a ``__call__`` method accepting a ``Settings``, - ``Blueprints``, and ``SystemLayoutInput``. - - The class attribute ``FAIL_IF_AFTER`` should be a tuple defining what, if any, - modifications this should fail if performed after. For example, one should not - adjust the smear density (a function of Cladding ID) before adjusting the Cladding - ID. - - Some subclasses are provided, but you are expected to make your own design-specific - modifiers in most cases. - .. impl:: A generic tool to modify user inputs on multiple cases. :id: I_ARMI_CASE_MOD1 :implements: R_ARMI_CASE_MOD + + This class serves as an abstract base class for modifying the inputs of + a case, typically case settings. Child classes must implement a + ``__call__`` method accepting a + :py:class:`~armi.settings.caseSettings.Settings`, + :py:class:`~armi.reactor.blueprints.Blueprints`, and + :py:class:`~armi.reactor.systemLayoutInput.SystemLayoutInput` and return + the appropriately modified version of these objects. The class attribute + ``FAIL_IF_AFTER`` should be a tuple defining what, if any, modifications + this should fail if performed after. For example, one should not adjust + the smear density (a function of Cladding ID) before adjusting the + Cladding ID. Some generic child classes are provided in this module, but + it is expected that design-specific modifiers are built individually. """ FAIL_IF_AFTER = () diff --git a/armi/cases/suite.py b/armi/cases/suite.py index 5a3008847..7fe1633d5 100644 --- a/armi/cases/suite.py +++ b/armi/cases/suite.py @@ -44,15 +44,17 @@ class CaseSuite: """ A CaseSuite is a collection of possibly related Case objects. - A CaseSuite is intended to be both a pre-processing and post-processing tool to - facilitate case generation and analysis. Under most circumstances one may wish to - subclass a CaseSuite to meet the needs of a specific calculation. - - A CaseSuite is a collection that is keyed off Case titles. - .. impl:: CaseSuite allows for one case to start after another completes. :id: I_ARMI_CASE_SUITE :implements: R_ARMI_CASE_SUITE + + The CaseSuite object allows multiple, often related, + :py:class:`~armi.cases.case.Case` objects to be run sequentially. A + CaseSuite is intended to be both a pre-processing and post-processing + tool to facilitate case generation and analysis. Under most + circumstances one may wish to subclass a CaseSuite to meet the needs of + a specific calculation. A CaseSuite is a collection that is keyed off + Case titles. """ def __init__(self, cs): diff --git a/armi/cases/suiteBuilder.py b/armi/cases/suiteBuilder.py index c0b604e18..fd846086d 100644 --- a/armi/cases/suiteBuilder.py +++ b/armi/cases/suiteBuilder.py @@ -49,6 +49,18 @@ class SuiteBuilder: :id: I_ARMI_CASE_MOD0 :implements: R_ARMI_CASE_MOD + This class provides the capability to create a + :py:class:`~armi.cases.suite.CaseSuite` based on programmatic + perturbations/modifications to case settings. It works by being + constructed with a base or nominal :py:class:`~armi.cases.case.Case` + object. Children classes then append the ``self.modifierSets`` member. + Each entry in ``self.modifierSets`` is a + :py:class:`~armi.cases.inputModifiers.inputModifiers.InputModifier` + representing a case to add to the suite by specifying modifications to + the settings of the base case. :py:meth:`SuiteBuilder.buildSuite` is + then invoked, returning an instance of the :py:class:`~armi.cases.suite.CaseSuite` + containing all the cases with modified settings. + Attributes ---------- baseCase : armi.cases.case.Case @@ -213,14 +225,21 @@ def __call__(self, cs, bp, geom): would result in 6 cases: + +-------+------------------+------------------+ | Index | ``settingName1`` | ``settingName2`` | - | ----- | ---------------- | ---------------- | + +=======+==================+==================+ | 0 | 1 | 3 | + +-------+------------------+------------------+ | 1 | 2 | 3 | + +-------+------------------+------------------+ | 2 | 1 | 4 | + +-------+------------------+------------------+ | 3 | 2 | 4 | + +-------+------------------+------------------+ | 4 | 1 | 5 | + +-------+------------------+------------------+ | 5 | 2 | 5 | + +-------+------------------+------------------+ See Also -------- @@ -299,13 +318,19 @@ def __call__(self, cs, bp, geom): would result in 5 cases: + +-------+------------------+------------------+ | Index | ``settingName1`` | ``settingName2`` | - | ----- | ---------------- | ---------------- | + +=======+==================+==================+ | 0 | 1 | default | + +-------+------------------+------------------+ | 1 | 2 | default | + +-------+------------------+------------------+ | 2 | default | 3 | + +-------+------------------+------------------+ | 3 | default | 4 | + +-------+------------------+------------------+ | 4 | default | 5 | + +-------+------------------+------------------+ See Also -------- From 09f4464864889d3269f0b3b46ddfc0ae86adbd69 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Wed, 24 Jan 2024 08:30:13 -0800 Subject: [PATCH 135/176] Adding CLI implementation text (#1609) --- armi/cli/__init__.py | 11 +++++++---- armi/cli/entryPoint.py | 15 +++++++++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/armi/cli/__init__.py b/armi/cli/__init__.py index 7b2f13bdd..dd46f65c2 100644 --- a/armi/cli/__init__.py +++ b/armi/cli/__init__.py @@ -100,14 +100,17 @@ def print_help(self, file=None): class ArmiCLI: """ - ARMI CLI -- The main entry point into ARMI. There are various commands - available. To get help for the individual commands, run again with - `<command> --help`. Typically, the CLI implements functions that already - exist within ARMI. + ARMI CLI -- The main entry point into ARMI. There are various commands available. To get help + for the individual commands, run again with `<command> --help`. Typically, the CLI implements + functions that already exist within ARMI. .. impl:: The basic ARMI CLI, for running a simulation. :id: I_ARMI_CLI_CS :implements: R_ARMI_CLI_CS + + Provides a basic command-line interface (CLI) for running an ARMI simulation. Available + commands can be listed with ``-l``. Information on individual commands can be obtained by + running with ``<command> --help``. """ def __init__(self): diff --git a/armi/cli/entryPoint.py b/armi/cli/entryPoint.py index e8554ef41..ef1ffef5b 100644 --- a/armi/cli/entryPoint.py +++ b/armi/cli/entryPoint.py @@ -51,12 +51,23 @@ class EntryPoint: """ Generic command line entry point. - A valid subclass must provide at least a ``name`` class attribute, and may also - specify the other class attributes described below. + A valid subclass must provide at least a ``name`` class attribute, and may also specify the + other class attributes described below. .. impl:: Generic CLI base class for developers to use. :id: I_ARMI_CLI_GEN :implements: R_ARMI_CLI_GEN + + Provides a base class for plugin developers to use in creating application-specific CLIs. + Valid subclasses must at least provide a ``name`` class attribute. + + Optional class attributes that a subclass may provide include ``description``, a string + describing the command's actions, ``splash``, a boolean specifying whether to display a + splash screen upon execution, and ``settingsArgument``. If ``settingsArgument`` is specified + as ``required``, then a settings files is a required positional argument. If + ``settingsArgument`` is set to ``optional``, then a settings file is an optional positional + argument. If None is specified for the ``settingsArgument``, then no settings file argument + is added. """ #: The <command-name> that is used to call the command from the command line From a2a10e27d56f534255a0582f7bf1b83c724a2ddf Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Wed, 24 Jan 2024 08:47:48 -0800 Subject: [PATCH 136/176] Adding implementation text for Utils module (#1610) --- armi/utils/densityTools.py | 44 ++++++++++++++++++++++++++++++++------ armi/utils/flags.py | 23 +++++++++++++++----- armi/utils/hexagon.py | 7 +++++- 3 files changed, 62 insertions(+), 12 deletions(-) diff --git a/armi/utils/densityTools.py b/armi/utils/densityTools.py index 5f98e20f9..94b46a82d 100644 --- a/armi/utils/densityTools.py +++ b/armi/utils/densityTools.py @@ -28,13 +28,17 @@ def getNDensFromMasses(rho, massFracs, normalize=False): :id: I_ARMI_UTIL_MASS2N_DENS :implements: R_ARMI_UTIL_MASS2N_DENS + Loops over all provided nuclides (given as keys in the ``massFracs`` vector) and calculates + number densities of each, at a given material ``density``. Mass fractions can be provided + either as normalized to 1, or as unnormalized with subsequent normalization calling + ``normalizeNuclideList`` via the ``normalize`` flag. + Parameters ---------- rho : float density in (g/cc) massFracs : dict - vector of mass fractions -- normalized to 1 -- keyed by their nuclide - name + vector of mass fractions -- normalized to 1 -- keyed by their nuclide name Returns ------- @@ -176,6 +180,21 @@ def formatMaterialCard( :id: I_ARMI_UTIL_MCNP_MAT_CARD :implements: R_ARMI_UTIL_MCNP_MAT_CARD + Loops over a vector of nuclides (of type ``nuclideBase``) provided in ``densities`` and + formats them into a list of strings consistent with MCNP material card syntax, skipping + dummy nuclides and LFPs. + + A ``matNum`` may optionally be provided for the created material card: if not provided, it + is left blank. The desired number of significant figures for the created card can be + optionally provided by ``sigFigs``. Nuclides whose number density falls below a threshold + (optionally specified by ``minDens``) are set to the threshold value. + + The boolean ``mcnp6Compatible`` may optionally be provided to include the nuclide library at + the end of the vector of individual nuclides using the "nlib=" syntax leveraged by MCNP. If + this boolean is turned on, the associated value ``mcnpLibrary`` should generally also be + provided, as otherwise, the library will be left blank in the resulting material card + string. + Parameters ---------- densities : dict @@ -266,6 +285,9 @@ def normalizeNuclideList(nuclideVector, normalization=1.0): :id: I_ARMI_UTIL_DENS_TOOLS :implements: R_ARMI_UTIL_DENS_TOOLS + Given a vector of nuclides ``nuclideVector`` indexed by nuclide identifiers (``nucNames`` or ``nuclideBases``), + normalizes to the provided ``normalization`` value. + Parameters ---------- nuclideVector : dict @@ -303,15 +325,25 @@ def expandElementalMassFracsToNuclides( :id: I_ARMI_UTIL_EXP_MASS_FRACS :implements: R_ARMI_UTIL_EXP_MASS_FRACS + Given a vector of elements and nuclides with associated mass fractions (``massFracs``), + expands the elements in-place into a set of nuclides using + ``expandElementalNuclideMassFracs``. Isotopes to expand into are provided for each element + by specifying them with ``elementExpansionPairs``, which maps each element to a list of + particular NuclideBases; if left unspecified, all naturally-occurring isotopes are included. + + Explicitly specifying the expansion isotopes provides a way for particular + naturally-occurring isotopes to be excluded from the expansion, e.g. excluding O-18 from an + expansion of elemental oxygen. + Parameters ---------- massFracs : dict(str, float) - dictionary of nuclide or element names with mass fractions. - Elements will be expanded in place using natural isotopics. + dictionary of nuclide or element names with mass fractions. Elements will be expanded in + place using natural isotopics. elementExpansionPairs : (Element, [NuclideBase]) pairs - element objects to expand (from nuclidBase.element) and list - of NuclideBases to expand into (or None for all natural) + element objects to expand (from nuclidBase.element) and list of NuclideBases to expand into + (or None for all natural) """ # expand elements for element, isotopicSubset in elementExpansionPairs: diff --git a/armi/utils/flags.py b/armi/utils/flags.py index 55f60c5cd..08ca04532 100644 --- a/armi/utils/flags.py +++ b/armi/utils/flags.py @@ -119,14 +119,23 @@ class Flag(metaclass=_FlagMeta): """ A collection of bitwise flags. - This is intended to emulate ``enum.Flag``, except with the possibility of extension - after the class has been defined. Most docs for ``enum.Flag`` should be relevant here, - but there are sure to be occasional differences. + This is intended to emulate ``enum.Flag``, except with the possibility of extension after the + class has been defined. Most docs for ``enum.Flag`` should be relevant here, but there are sure + to be occasional differences. .. impl:: No two flags have equivalence. :id: I_ARMI_FLAG_DEFINE :implements: R_ARMI_FLAG_DEFINE + A bitwise flag class intended to emulate the standard library's ``enum.Flag``, with the + added functionality that it allows for extension after the class has been defined. Each Flag + is unique; no two Flags are equivalent. + + Note that while Python allows for arbitrary-width integers, exceeding the system-native + integer size can lead to challenges in storing data, e.g. in an HDF5 file. In this case, the + ``from_bytes()`` and ``to_bytes()`` methods are provided to represent a Flag's values in + smaller chunks so that writeability can be maintained. + .. warning:: Python features arbitrary-width integers, allowing one to represent an practically unlimited number of fields. *However*, including more flags than can @@ -224,11 +233,15 @@ def extend(cls, fields: Dict[str, Union[int, auto]]): :id: I_ARMI_FLAG_EXTEND0 :implements: R_ARMI_FLAG_EXTEND + A class method to extend a ``Flag`` with a vector of provided additional ``fields``, + with field names as keys, without loss of uniqueness. Values for the additional + ``fields`` can be explicitly specified, or an instance of ``auto`` can be supplied. + Parameters ---------- fields : dict - A dictionary containing field names as keys, and their desired values, or - an instance of ``auto`` as values. + A dictionary containing field names as keys, and their desired values, or an instance of + ``auto`` as values. Example ------- diff --git a/armi/utils/hexagon.py b/armi/utils/hexagon.py index dc4e706f3..3831d43be 100644 --- a/armi/utils/hexagon.py +++ b/armi/utils/hexagon.py @@ -36,6 +36,8 @@ def area(pitch): :id: I_ARMI_UTIL_HEXAGON0 :implements: R_ARMI_UTIL_HEXAGON + Computes the area of a hexagon given the flat-to-flat ``pitch``. + Notes ----- The pitch is the distance between the center of the hexagons in the lattice. @@ -135,8 +137,11 @@ def numRingsToHoldNumCells(numCells): def numPositionsInRing(ring): """Number of positions in ring (starting at 1) of a hex lattice. - .. impl:: Compute hexagonal area + .. impl:: Compute number of positions in a ring of a hex lattice :id: I_ARMI_UTIL_HEXAGON1 :implements: R_ARMI_UTIL_HEXAGON + + In a hexagonal lattice, calculate the number of positions in a given ``ring``. The number of + rings is indexed to 1, i.e. the centermost position in the lattice is ``ring=1``. """ return (ring - 1) * 6 if ring != 1 else 1 From 2917be13e93ceeb0e15e5bf1457eebf9a622f8d2 Mon Sep 17 00:00:00 2001 From: Tony Alberti <aalberti@terrapower.com> Date: Wed, 24 Jan 2024 08:59:24 -0800 Subject: [PATCH 137/176] Update iml tags for assemblies.py (#1604) * update impl docstrings for assemblies.py * black formatting * update impl description for `add` --- armi/reactor/assemblies.py | 49 +++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/armi/reactor/assemblies.py b/armi/reactor/assemblies.py index 40d7c77cd..cd5162b26 100644 --- a/armi/reactor/assemblies.py +++ b/armi/reactor/assemblies.py @@ -180,6 +180,15 @@ def add(self, obj: blocks.Block): .. impl:: Assemblies are made up of type Block. :id: I_ARMI_ASSEM_BLOCKS :implements: R_ARMI_ASSEM_BLOCKS + + Adds a unique Block to the top of the Assembly. If the Block already + exists in the Assembly, an error is raised in + :py:meth:`armi.reactor.composites.Composite.add`. + The spatialLocator of the Assembly is updated to account for + the new Block. In ``reestablishBlockOrder``, the Assembly spatialGrid + is reinitialized and Block-wise spatialLocator and name objects + are updated. The axial mesh and other Block geometry parameters are + updated in ``calculateZCoords``. """ composites.Composite.add(self, obj) obj.spatialLocator = self.spatialGrid[0, 0, len(self) - 1] @@ -220,12 +229,12 @@ def getLocation(self): :id: I_ARMI_ASSEM_POSI0 :implements: R_ARMI_ASSEM_POSI - Notes - ----- - This function (and its friends) were created before the advent of both the - grid/spatialLocator system and the ability to represent things like the SFP as - siblings of a Core. In future, this will likely be re-implemented in terms of - just spatialLocator objects. + This method returns a string label indicating the location + of an Assembly. There are three options: 1) the Assembly + is not within a Core object and is interpreted as in the + "load queue"; 2) the Assembly is within the spent fuel pool; + 3) the Assembly is within a Core object, so it has a physical + location within the Core. """ # just use ring and position, not axial (which is 0) if not self.parent: @@ -243,6 +252,9 @@ def coords(self): .. impl:: Assembly coordinates are retrievable. :id: I_ARMI_ASSEM_POSI1 :implements: R_ARMI_ASSEM_POSI + + In this method, the spatialLocator of an Assembly is leveraged to return + its physical (x,y) coordinates in cm. """ x, y, _z = self.spatialLocator.getGlobalCoordinates() return (x, y) @@ -257,6 +269,10 @@ def getArea(self): .. impl:: Assembly area is retrievable. :id: I_ARMI_ASSEM_DIMS0 :implements: R_ARMI_ASSEM_DIMS + + Returns the area of the first block in the Assembly. If there are no + block in the Assembly, a warning is issued and a default area of 1.0 + is returned. """ try: return self[0].getArea() @@ -272,6 +288,10 @@ def getVolume(self): .. impl:: Assembly volume is retrievable. :id: I_ARMI_ASSEM_DIMS1 :implements: R_ARMI_ASSEM_DIMS + + The volume of the Assembly is calculated as the product of the + area of the first block (via ``getArea``) and the total height + of the assembly (via ``getTotalHeight``). """ return self.getArea() * self.getTotalHeight() @@ -477,6 +497,10 @@ def getTotalHeight(self, typeSpec=None): :id: I_ARMI_ASSEM_DIMS2 :implements: R_ARMI_ASSEM_DIMS + The height of the Assembly is calculated by taking the sum of the + constituent Blocks. If a ``typeSpec`` is provided, the total height + of the blocks containing Flags that match the ``typeSpec`` is returned. + Parameters ---------- typeSpec : See :py:meth:`armi.composites.Composite.hasFlags` @@ -1153,7 +1177,7 @@ def getParamOfZFunction(self, param, interpType="linear", fillValue=numpy.NaN): def reestablishBlockOrder(self): """ - After children have been mixed up axially, this re-locates each block with the proper axial mesh. + The block ordering has changed, so the spatialGrid and Block-wise spatialLocator and name objects need updating. See Also -------- @@ -1185,14 +1209,21 @@ def countBlocksWithFlags(self, blockTypeSpec=None): def getDim(self, typeSpec, dimName): """ - Search through blocks in this assembly and find the first component of compName. - Then, look on that component for dimName. + With a preference for fuel blocks, find the first component in the Assembly with + flags that match ``typeSpec`` and return dimension as specified by ``dimName``. Example: getDim(Flags.WIRE, 'od') will return a wire's OD in cm. .. impl:: Assembly dimensions are retrievable. :id: I_ARMI_ASSEM_DIMS3 :implements: R_ARMI_ASSEM_DIMS + + This method searches for the first Component that matches the + given ``typeSpec`` and returns the dimension as specified by + ``dimName``. There is a hard-coded preference for Components + to be within fuel Blocks. If there are no Blocks, then ``None`` + is returned. If ``typeSpec`` is not within the first Block, an + error is raised within :py:meth:`armi.reactor.blocksBlock.getDim`. """ # prefer fuel blocks. bList = self.getBlocks(Flags.FUEL) From 64114167d1e7d2649ff4dd98ddd4f28fbf3699af Mon Sep 17 00:00:00 2001 From: bdlafleur <blafleur@umich.edu> Date: Wed, 24 Jan 2024 12:15:02 -0500 Subject: [PATCH 138/176] Adding impl tag descriptions to parameters (#1598) --- .../parameters/parameterDefinitions.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/armi/reactor/parameters/parameterDefinitions.py b/armi/reactor/parameters/parameterDefinitions.py index daa51afc9..f75ed771b 100644 --- a/armi/reactor/parameters/parameterDefinitions.py +++ b/armi/reactor/parameters/parameterDefinitions.py @@ -156,6 +156,17 @@ class Serializer: :id: I_ARMI_PARAM_SERIALIZE :implements: R_ARMI_PARAM_SERIALIZE + Important physical parameters are stored in every ARMI object. + These parameters represent the plant's state during execution + of the model. Currently, this requires that the parameters be serializable to a + numpy array of a datatype supported by the ``h5py`` package so that the data can + be written to, and subsequently read from, an HDF5 file. + + This class allows for these parameters to be serialized in a custom manner by + providing interfaces for packing and unpacking parameter data. The user or + downstream plugin is able to specify how data is serialized if that data is not + naturally serializable. + See Also -------- armi.bookkeeping.db.database3.packSpecialData @@ -344,6 +355,13 @@ def setter(self, setter): :id: I_ARMI_PARAM_PARALLEL :implements: R_ARMI_PARAM_PARALLEL + Parameters need to be handled properly during parallel code execution. This + includes notifying processes if a parameter has been updated by + another process. This method allows for setting a parameter's value as well + as an attribute that signals whether this parameter has been updated. Future + processes will be able to query this attribute so that the parameter's + status is properly communicated. + Notes ----- Unlike the traditional Python ``property`` class, this does not return a new @@ -587,6 +605,11 @@ def toWriteToDB(self, assignedMask: Optional[int] = None): :id: I_ARMI_PARAM_DB :implements: R_ARMI_PARAM_DB + This method is called when writing the parameters to the database file. It + queries the parameter's ``saveToDB`` attribute to ensure that this parameter + is desired for saving to the database file. It returns a list of parameters + that should be included in the database write operation. + Parameters ---------- assignedMask : int From c94276c9aedd83d4f57126f5c89e35357a5453b4 Mon Sep 17 00:00:00 2001 From: kasticrunch <43149783+kasticrunch@users.noreply.github.com> Date: Wed, 24 Jan 2024 09:32:49 -0800 Subject: [PATCH 139/176] Adding implementation details to tags in the xsgm (#1612) --- .../neutronics/crossSectionGroupManager.py | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/armi/physics/neutronics/crossSectionGroupManager.py b/armi/physics/neutronics/crossSectionGroupManager.py index 80c4f116c..bdad6a2f9 100644 --- a/armi/physics/neutronics/crossSectionGroupManager.py +++ b/armi/physics/neutronics/crossSectionGroupManager.py @@ -319,6 +319,14 @@ class AverageBlockCollection(BlockCollection): .. impl:: Create representative blocks using volume-weighted averaging. :id: I_ARMI_XSGM_CREATE_REPR_BLOCKS0 :implements: R_ARMI_XSGM_CREATE_REPR_BLOCKS + + This class constructs new blocks from an existing block list based on a + volume-weighted average. Inheriting functionality from the abstract + :py:class:`Reactor <armi.physics.neutronics.crossSectionGroupManager.BlockCollection>` object, this class + will construct representative blocks using averaged parameters of all blocks in the given collection. + Number density averages can be computed at a component level through `self._performAverageByComponent`, + or at a block level by default. Average nuclide temperatures and burnup are also included when constructing a representative block. + """ def _makeRepresentativeBlock(self): @@ -512,6 +520,13 @@ class CylindricalComponentsAverageBlockCollection(BlockCollection): :id: I_ARMI_XSGM_CREATE_REPR_BLOCKS1 :implements: R_ARMI_XSGM_CREATE_REPR_BLOCKS + This class constructs representative blocks based on a volume-weighted average + using cylindrical blocks from an existing block list. Inheriting functionality from the abstract + :py:class:`Reactor <armi.physics.neutronics.crossSectionGroupManager.BlockCollection>` object, this class + will construct representative blocks using averaged parameters of all blocks in the given collection. + Number density averages are computed at a component level. Nuclide temperatures from a median block-average temperature + are used and the average burnup is evaluated across all blocks in the block list. + Notes ----- When generating the representative block within this collection, the geometry is checked @@ -843,6 +858,11 @@ def interactBOL(self): .. impl:: The lattice physics interface and XSGM are connected at BOL. :id: I_ARMI_XSGM_FREQ0 :implements: R_ARMI_XSGM_FREQ + + This method sets the cross-section block averaging method and and logic for whether all blocks in a cross section group should be used when generating + a representative block. Furthermore, if the control logic for lattice physics frequency updates is set at beginning-of-life (`BOL`) through the :py:class:`LatticePhysicsInterface <armi.physics.neutronics.latticePhysics>`, + the cross-section group manager will construct representative blocks for each cross-section IDs at the beginning of the reactor state. + """ # now that all cs settings are loaded, apply defaults to compound XS settings from armi.physics.neutronics.settings import CONF_XS_BLOCK_REPRESENTATION @@ -869,6 +889,10 @@ def interactBOC(self, cycle=None): :id: I_ARMI_XSGM_FREQ1 :implements: R_ARMI_XSGM_FREQ + This method updates representative blocks and block burnups at the beginning-of-cycle for each cross-section ID if the control logic + for lattice physics frequency updates is set at beginning-of-cycle (`BOC`) through the :py:class:`LatticePhysicsInterface <armi.physics.neutronics.latticePhysics>`. + At the beginning-of-cycle, the cross-section group manager will construct representative blocks for each cross-section IDs for the current reactor state. + Notes ----- The block list each each block collection cannot be emptied since it is used to derive nuclide temperatures. @@ -884,11 +908,16 @@ def interactEOC(self, cycle=None): self.clearRepresentativeBlocks() def interactEveryNode(self, cycle=None, tn=None): - """Interactino at every time now. + """Interaction at every time node. .. impl:: The lattice physics interface and XSGM are connected at every time node. :id: I_ARMI_XSGM_FREQ2 :implements: R_ARMI_XSGM_FREQ + + This method updates representative blocks and block burnups at every node for each cross-section ID if the control logic + for lattices physics frequency updates is set for every node (`everyNode`) through the :py:class:`LatticePhysicsInterface <armi.physics.neutronics.latticePhysics>`. + At every node, the cross-section group manager will construct representative blocks for each cross-section ID in the current reactor state. + """ if self._latticePhysicsFrequency >= LatticePhysicsFrequency.everyNode: self.createRepresentativeBlocks() @@ -900,6 +929,10 @@ def interactCoupled(self, iteration): :id: I_ARMI_XSGM_FREQ3 :implements: R_ARMI_XSGM_FREQ + This method updates representative blocks and block burnups at every node and the first coupled iteration for each cross-section ID + if the control logic for lattices physics frequency updates is set for the first coupled iteration (`firstCoupledIteration`) through the :py:class:`LatticePhysicsInterface <armi.physics.neutronics.latticePhysics>`. + The cross-section group manager will construct representative blocks for each cross-section ID at the first iteration of every time node. + Notes ----- Updating the XS on only the first (i.e., iteration == 0) timenode can be a reasonable approximation to @@ -1077,11 +1110,16 @@ def _getPregeneratedFluxFileLocationData(self, xsID): return (filePath, fileName) def createRepresentativeBlocks(self): - """Get a representative block from each cross section ID managed here. + """Get a representative block from each cross-section ID managed here. .. impl:: Create collections of blocks based on XS type and burn-up group. :id: I_ARMI_XSGM_CREATE_XS_GROUPS :implements: R_ARMI_XSGM_CREATE_XS_GROUPS + + This method constructs the representative blocks and block burnups + for each cross-section ID in the reactor model. Starting with the making of cross-section groups, it will + find candidate blocks and create representative blocks from that selection. + """ representativeBlocks = {} self.avgNucTemperatures = {} From d4919c917efd5af34841b19b529d2c6c0f20497e Mon Sep 17 00:00:00 2001 From: Michael Johnson <157380737+mikepjohnson@users.noreply.github.com> Date: Wed, 24 Jan 2024 12:49:40 -0500 Subject: [PATCH 140/176] Adding implementation text for zones.py (#1601) --- armi/reactor/zones.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/armi/reactor/zones.py b/armi/reactor/zones.py index 8a487bea2..2d27b5fb0 100644 --- a/armi/reactor/zones.py +++ b/armi/reactor/zones.py @@ -32,6 +32,18 @@ class Zone: .. impl:: A user can define a collection of armi locations. :id: I_ARMI_ZONE :implements: R_ARMI_ZONE + + The Zone class facilitates the creation of a Zone object representing a + collection of locations in the Core. A Zone contains a group of locations + in the Core, used to subdivide it for analysis. Each location represents + an Assembly or a Block, where a single Zone must contain items of the same + type (i.e., Assembly or Block). Methods are provided to add or remove + one or more locations to/from the Zone, and similarly, add or remove one or + more items with a Core location (i.e., Assemblies or Blocks) to/from the + Zone. In addition, several methods are provided to facilitate the + retrieval of locations from a Zone by performing functions to check if a + location exists in the Zone, looping through the locations in the Zone in + alphabetical order, and returning the number of locations in the Zone, etc. """ VALID_TYPES = (Assembly, Block) @@ -205,6 +217,16 @@ class Zones: .. impl:: A user can define a collection of armi zones. :id: I_ARMI_ZONES :implements: R_ARMI_ZONES + + The Zones class facilitates the creation of a Zones object representing a + collection of Zone objects. Methods are provided to add or remove one + or more Zone to/from the Zones object. Likewise, methods are provided + to validate that the zones are mutually exclusive, obtain the location + labels of zones, return the Zone object where a particular Assembly or Block + resides, sort the Zone objects alphabetically, and summarize the zone + definitions. In addition, methods are provided to facilitate the + retrieval of Zone objects by name, loop through the Zones in order, and + return the number of Zone objects. """ def __init__(self): @@ -312,7 +334,7 @@ def removeZones(self, names: List) -> None: def checkDuplicates(self) -> None: """ - Validate that the the zones are mutually exclusive. + Validate that the zones are mutually exclusive. That is, make sure that no item appears in more than one Zone. From 96c16fdb0d0d0dd06a5928588222b0267cb3d52b Mon Sep 17 00:00:00 2001 From: Tony Alberti <aalberti@terrapower.com> Date: Wed, 24 Jan 2024 10:25:08 -0800 Subject: [PATCH 141/176] Improving implementation text for axial expansion (#1615) --- armi/reactor/blocks.py | 3 ++ .../converters/axialExpansionChanger.py | 53 ++++++++++++++++--- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py index 4d83e827d..e828450e1 100644 --- a/armi/reactor/blocks.py +++ b/armi/reactor/blocks.py @@ -1565,6 +1565,9 @@ def setAxialExpTargetComp(self, targetComponent): :id: I_ARMI_MANUAL_TARG_COMP :implements: R_ARMI_MANUAL_TARG_COMP + This method sets the provided ``Component`` to be the axial expansion + target ``Component``. + Parameter --------- targetComponent: :py:class:`Component <armi.reactor.components.component.Component>` object diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index 60c1cde49..33b52b220 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -69,6 +69,22 @@ def expandColdDimsToHot( :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>`] @@ -110,10 +126,6 @@ class AxialExpansionChanger: """ Axially expand or contract assemblies or an entire core. - .. impl:: Preserve the total height of an ARMI assembly, during expansion. - :id: I_ARMI_ASSEM_HEIGHT_PRES - :implements: R_ARMI_ASSEM_HEIGHT_PRES - Attributes ---------- linked : :py:class:`AssemblyAxialLinkage` @@ -144,12 +156,19 @@ def __init__(self, detailedAxialExpansion: bool = False): def performPrescribedAxialExpansion( self, a, componentLst: list, percents: list, setFuel=True ): - """Perform axial expansion of an assembly given prescribed expansion percentages. + """Perform axial expansion/contraction of an assembly given prescribed expansion percentages. .. impl:: Perform expansion/contraction, given a list of components and expansion coefficients. :id: I_ARMI_AXIAL_EXP_PRESC :implements: R_ARMI_AXIAL_EXP_PRESC + This method performs component-wise axial expansion for an Assembly given expansion coefficients + and a corresponding list of Components. In ``setAssembly``, the Assembly is prepared + for axial expansion by determining Component-wise axial linkage and checking to see if a dummy Block + is in place (necessary for ensuring conservation properties). The provided expansion factors are + then assigned to their corresponding Components in ``setExpansionFactors``. Finally, the axial + expansion is performed in ``axiallyExpandAssembly`` + Parameters ---------- a : :py:class:`Assembly <armi.reactor.assemblies.Assembly>` @@ -178,12 +197,20 @@ def performThermalAxialExpansion( setFuel: bool = True, expandFromTinputToThot: bool = False, ): - """Perform thermal expansion for an assembly given an axial temperature grid and field. + """Perform thermal expansion/contraction for an assembly given an axial temperature grid and field. - .. impl:: Perform thermal expansion/contraction, given an axial temp distribution over an assembly. + .. impl:: Perform thermal expansion/contraction, given an axial temperature distribution over an assembly. :id: I_ARMI_AXIAL_EXP_THERM :implements: R_ARMI_AXIAL_EXP_THERM + This method performs component-wise thermal expansion for an assembly given a discrete temperature + distribution over the axial length of the Assembly. In ``setAssembly``, the Assembly is prepared + for axial expansion by determining Component-wise axial linkage and checking to see if a dummy Block + is in place (necessary for ensuring conservation properties). The discrete temperature distribution + is then leveraged to update Component temperatures and compute thermal expansion factors + (via ``updateComponentTempsBy1DTempField`` and ``computeThermalExpansionFactors``, respectively). + Finally, the axial expansion is performed in ``axiallyExpandAssembly``. + Parameters ---------- a : :py:class:`Assembly <armi.reactor.assemblies.Assembly>` @@ -274,7 +301,17 @@ def _isTopDummyBlockPresent(self): raise RuntimeError(msg) def axiallyExpandAssembly(self): - """Utilizes assembly linkage to do axial expansion.""" + """Utilizes assembly linkage to do axial expansion. + + .. impl:: Preserve the total height of an ARMI assembly, during expansion. + :id: I_ARMI_ASSEM_HEIGHT_PRES + :implements: R_ARMI_ASSEM_HEIGHT_PRES + + The total height of an Assembly is preserved by not changing the ``ztop`` position + of the top-most Block in an Assembly. The ``zbottom`` of the top-most Block is + adjusted to match the Block immediately below it. The ``height`` of the + top-most Block is is then updated to reflect any expansion/contraction. + """ mesh = [0.0] numOfBlocks = self.linked.a.countBlocksWithFlags() runLog.debug( From 8ba0aaf0a08631de15e25fbb597380c5e69bd1e5 Mon Sep 17 00:00:00 2001 From: Chris Keckler <ckeckler@terrapower.com> Date: Wed, 24 Jan 2024 12:43:03 -0600 Subject: [PATCH 142/176] Adding longer descriptions to impls in flags (#1595) --- armi/reactor/flags.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/armi/reactor/flags.py b/armi/reactor/flags.py index 8ced3de02..6366432a8 100644 --- a/armi/reactor/flags.py +++ b/armi/reactor/flags.py @@ -290,6 +290,16 @@ def fromString(cls, typeSpec): .. impl:: Retrieve flag from a string. :id: I_ARMI_FLAG_TO_STR0 :implements: R_ARMI_FLAG_TO_STR + + For a string passed as ``typeSpec``, first converts the whole string + to uppercase. Then tries to parse the string for any special phrases, as + defined in the module dictionary ``_CONVERSIONS``, and converts those + phrases to flags directly. + + Then it splits the remaining string into separate words based on the presence + of spaces. Looping over each of the words, any numbers are stripped out + and the remaining string is matched up to any class attribute names. + If any matches are found these are returned as flags. """ return _fromString(cls, typeSpec) @@ -301,6 +311,12 @@ def toString(cls, typeSpec): .. impl:: Convert a flag to string. :id: I_ARMI_FLAG_TO_STR1 :implements: R_ARMI_FLAG_TO_STR + + This converts the representation of a bunch of flags from ``typeSpec``, + which might look like ``Flags.A|B``, + into a string with spaces in between the flag names, which would look + like ``'A B'``. This is done via nesting string splitting and replacement + actions. """ return _toString(cls, typeSpec) From 38dc48d549f59ca164f67f66ff7f8e5bbd6e4963 Mon Sep 17 00:00:00 2001 From: Michael Jarrett <mjarrett@terrapower.com> Date: Wed, 24 Jan 2024 10:48:16 -0800 Subject: [PATCH 143/176] Add implementation text for reactors.py. (#1597) Providing longer implementation description in the `impl` tags for reactors.py. Co-authored-by: John Stilley <1831479+john-science@users.noreply.github.com> --- armi/reactor/reactors.py | 209 ++++++++++++++++++++++++++++++--------- 1 file changed, 165 insertions(+), 44 deletions(-) diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index 8d2744885..8fb8f4396 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -67,16 +67,34 @@ class Reactor(composites.Composite): """ - Top level of the composite structure, potentially representing all components in a reactor. + Top level of the composite structure, potentially representing all + components in a reactor. - This class contains the core and any ex-core structures that are to be represented in the ARMI - model. Historically, the `Reactor` contained only the core. To support better representation of - ex-core structures, the old `Reactor` functionality was moved to the newer `Core` class, which - has a `Reactor` parent. + This class contains the core and any ex-core structures that are to be + represented in the ARMI model. Historically, the `Reactor` contained only + the core. To support better representation of ex-core structures, the old + `Reactor` functionality was moved to the newer `Core` class, which has a + `Reactor` parent. .. impl:: The user-specified reactor. :id: I_ARMI_R :implements: R_ARMI_R + + The :py:class:`Reactor <armi.reactor.reactors.Reactor>` is the top + level of the composite structure, which can represent all components + within a reactor core. The reactor contains a :py:class:`Core + <armi.reactor.reactors.Core>`, which contains a collection of + :py:class:`Assembly <armi.reactor.assemblies.Assembly>` objects + arranged in a hexagonal or Cartesian grid. Each Assembly consists of a + stack of :py:class:`Block <armi.reactor.blocks.Block>` objects, which + are each composed of one or more :py:class:`Component + <armi.reactor.components.component.Component>` objects. Each + :py:class:`Interface <armi.interfaces.Interface>` is able to interact + with the reactor and its child :py:class:`Composites + <armi.reactor.composites.Composite>` by retrieving data from it or + writing new data to it. This is the main medium through which input + information and the output of physics calculations is exchanged between + interfaces and written to an ARMI database. """ pDefs = reactorParameters.defineReactorParameters() @@ -240,6 +258,19 @@ class Core(composites.Composite): :id: I_ARMI_R_CORE :implements: R_ARMI_R_CORE + A :py:class:`Core <armi.reactor.reactors.Core>` object is typically a + child of a :py:class:`Reactor <armi.reactor.reactors.Reactor>` object. + A Reactor can contain multiple objects of the Core type. The instance + attribute name `r.core` is reserved for the object representating the + active core. A reactor may also have a spent fuel pool instance + attribute, `r.sfp`, which is also of type + :py:class:`core <armi.reactor.reactors.Core>`. + + Most of the operations to retrieve information from the ARMI reactor + data model are mediated through Core objects. For example, + :py:meth:`getAssemblies() <armi.reactor.reactors.Core.getAssemblies>` is + used to get a list of all assemblies in the Core. + Attributes ---------- params : dict @@ -348,6 +379,26 @@ def symmetry(self) -> geometry.SymmetryType: .. impl:: Get core symmetry. :id: I_ARMI_R_SYMM :implements: R_ARMI_R_SYMM + + This property getter returns the symmetry attribute of the + spatialGrid instance attribute. The spatialGrid is an instance of a + child of the abstract base class :py:class:`Grid + <armi.reactor.grids.grid.Grid>` type. The symmetry attribute is an + instance of the :py:class:`SymmetryType + <armi.reactor.geometry.SymmetryType>` class, which is a wrapper + around the :py:class:`DomainType <armi.reactor.geometry.DomainType>` + and :py:class:`BoundaryType <armi.reactor.geometry.BoundaryType>` + enumerations used to classify the domain (e.g., 1/3 core, quarter + core, full core) and symmetry boundary conditions (e.g., periodic, + reflective, none) of a reactor, respectively. + + Only specific combinations of :py:class:`Grid + <armi.reactor.grids.grid.Grid>` type, :py:class:`DomainType + <armi.reactor.geometry.DomainType>`, and :py:class:`BoundaryType + <armi.reactor.geometry.BoundaryType>` are valid. The validity of a + user-specified geometry and symmetry is verified by a settings: + :py:meth:`Inspector + <armi.operators.settingsValidation.Inspector._inspectSettings`. """ if not self.spatialGrid: raise ValueError("Cannot access symmetry before a spatialGrid is attached.") @@ -778,6 +829,14 @@ def getNumRings(self, indexBased=False): :id: I_ARMI_R_NUM_RINGS :implements: R_ARMI_R_NUM_RINGS + This method determines the number of rings in the reactor. If the + setting `circularRingMode` is enabled (by default it is false), the + assemblies will be grouped into roughly circular rings based on + their positions and the number of circular rings is reteurned. + Otherwise, the number of hex rings is returned. This parameter is + mostly used to facilitate certain fuel management strategies where + the fuel is categorized and moved based on ring indexing. + Warning ------- If you loop through range(maxRing) then ring+1 is the one you want! @@ -1135,6 +1194,11 @@ def getAssemblyByName(self, name): :id: I_ARMI_R_GET_ASSEM_NAME :implements: R_ARMI_R_GET_ASSEM_NAME + This method returns the :py:class:`assembly + <armi.reactor.core.assemblies.Assembly>` with a name matching the + value provided as an input parameter to this function. The `name` of + an assembly is based on the `assemNum` parameter. + Parameters ---------- name : str @@ -1651,6 +1715,18 @@ def getAssemblyWithStringLocation(self, locationString): .. impl:: Get assembly by location. :id: I_ARMI_R_GET_ASSEM_LOC :implements: R_ARMI_R_GET_ASSEM_LOC + + This method returns the :py:class:`assembly + <armi.reactor.core.assemblies.Assembly>` located in the requested + location. The location is provided to this method as an input + parameter in a string with the format "001-001". For a `HexGrid + <armi.reactor.grids.hexagonal.HexGrid>`, the first number indicates + the hexagonal ring and the second number indicates the position + within that ring. For a `CartesianGrid + <armi.reactor.grids.cartesian.CartesianGrid>`, the first number + represents the x index and the second number represents the y index. + If there is no assembly in the grid at the requested location, this + method returns None. """ ring, pos, _ = grids.locatorLabelToIndices(locationString) loc = self.spatialGrid.getLocatorFromRingAndPos(ring, pos) @@ -1679,68 +1755,102 @@ def findNeighbors( """ Find assemblies that are next to this assembly. - Return a list of neighboring assemblies from the 30 degree point (point 1) then - counterclockwise around. + Return a list of neighboring assemblies. + + For a hexagonal grid, the list begins from the 30 degree point (point 1) + then moves counterclockwise around. + + For a Cartesian grid, the order of the neighbors is east, north, west, + south. .. impl:: Retrieve neighboring assemblies of a given assembly. :id: I_ARMI_R_FIND_NEIGHBORS :implements: R_ARMI_R_FIND_NEIGHBORS + This method takes an :py:class:`Assembly + <armi.reactor.assemblies.Assembly>` as an input parameter and returns + a list of the assemblies neighboring that assembly. There are 6 + neighbors in a hexagonal grid and 4 neighbors in a Cartesian grid. + The (i, j) indices of the neighbors are provided by + :py:meth:`getNeighboringCellIndices + <armi.reactor.grids.StructuredGrid.getNeighboringCellIndices>`. For + a hexagonal grid, the (i, j) indices are converted to (ring, + position) indexing using the core.spatialGrid instance attribute. + + The `showBlanks` option determines whether non-existing assemblies + will be indicated with a `None` in the list or just excluded from + the list altogether. + + The `duplicateAssembliesOnReflectiveBoundary` setting only works for + 1/3 core symmetry with periodic boundary conditions. For these types + of geometries, if this setting is `True`, neighbor lists for + assemblies along a periodic boundary will include the assemblies + along the opposite periodic boundary that are effectively neighbors. + Parameters ---------- a : Assembly object The assembly to find neighbors of. showBlanks : Boolean, optional - If True, the returned array of 6 neighbors will return "None" for neighbors - that do not explicitly exist in the 1/3 core model (including many that WOULD - exist in a full core model). + If True, the returned array of 6 neighbors will return "None" for + neighbors that do not explicitly exist in the 1/3 core model + (including many that WOULD exist in a full core model). - If False, the returned array will not include the "None" neighbors. If one or - more neighbors does not explicitly exist in the 1/3 core model, the returned - array will have a length of less than 6. + If False, the returned array will not include the "None" neighbors. + If one or more neighbors does not explicitly exist in the 1/3 core + model, the returned array will have a length of less than 6. duplicateAssembliesOnReflectiveBoundary : Boolean, optional - If True, findNeighbors duplicates neighbor assemblies into their "symmetric - identicals" so that even assemblies that border symmetry lines will have 6 - neighbors. The only assemblies that will have fewer than 6 neighbors are those - that border the outer core boundary (usually vacuum). - - If False, findNeighbors returns None for assemblies that do not exist in a 1/3 - core model (but WOULD exist in a full core model). - - For example, applying findNeighbors for the central assembly (ring, pos) = (1, - 1) in 1/3 core symmetry (with duplicateAssembliesOnReflectiveBoundary = True) - would return a list of 6 assemblies, but those 6 would really only be - assemblies (2, 1) and (2, 2) repeated 3 times each. - - Note that the value of duplicateAssembliesOnReflectiveBoundary only really if - showBlanks = True. This will have no effect if the model is full core since - asymmetric models could find many duplicates in the other thirds + If True, findNeighbors duplicates neighbor assemblies into their + "symmetric identicals" so that even assemblies that border symmetry + lines will have 6 neighbors. The only assemblies that will have + fewer than 6 neighbors are those that border the outer core boundary + (usually vacuum). + + If False, findNeighbors returns None for assemblies that do not + exist in a 1/3 core model (but WOULD exist in a full core model). + + For example, applying findNeighbors for the central assembly (ring, + pos) = (1, 1) in 1/3 core symmetry (with + duplicateAssembliesOnReflectiveBoundary = True) would return a list + of 6 assemblies, but those 6 would really only be assemblies (2, 1) + and (2, 2) repeated 3 times each. + + Note that the value of duplicateAssembliesOnReflectiveBoundary only + really matters if showBlanks == True. This will have no effect if + the model is full core since asymmetric models could find many + duplicates in the other thirds Notes ----- - This only works for 1/3 or full core symmetry. + The duplicateAssembliesOnReflectiveBoundary setting only works for third + core symmetry. - This uses the 'mcnp' index map (MCNP GEODST hex coordinates) instead of the - standard (ring, pos) map. because neighbors have consistent indices this way. We - then convert over to (ring, pos) using the lookup table that a reactor has. + This uses the 'mcnp' index map (MCNP GEODST hex coordinates) instead of + the standard (ring, pos) map. because neighbors have consistent indices + this way. We then convert over to (ring, pos) using the lookup table + that a reactor has. Returns ------- neighbors : list of assembly objects This is a list of "nearest neighbors" to assembly a. - If showBlanks = False, it will return fewer than 6 neighbors if not all 6 - neighbors explicitly exist in the core model. + If showBlanks = False, it will return fewer than the maximum number + of neighbors if not all neighbors explicitly exist in the core + model. For a hexagonal grid, the maximum number of neighbors is 6. + For a Cartesian grid, the maximum number is 4. - If showBlanks = True and duplicateAssembliesOnReflectiveBoundary = False, it - will have a "None" for assemblies that do not exist in the 1/3 model. + If showBlanks = True and duplicateAssembliesOnReflectiveBoundary = + False, it will have a "None" for assemblies that do not exist in the + 1/3 model. - If showBlanks = True and duplicateAssembliesOnReflectiveBoundary = True, it - will return the existing "symmetric identical" assembly of a non-existing - assembly. It will only return "None" for an assembly when that assembly is - non-existing AND has no existing "symmetric identical". + If showBlanks = True and duplicateAssembliesOnReflectiveBoundary = + True, it will return the existing "symmetric identical" assembly of + a non-existing assembly. It will only return "None" for an assembly + when that assembly is non-existing AND has no existing "symmetric + identical". See Also -------- @@ -1773,7 +1883,7 @@ def findNeighbors( def _getReflectiveDuplicateAssembly(self, neighborLoc): """ - Return duplicate assemblies accross symmetry line. + Return duplicate assemblies across symmetry line. Notes ----- @@ -1935,6 +2045,17 @@ def findAllMeshPoints(self, assems=None, applySubMesh=True): :id: I_ARMI_R_MESH :implements: R_ARMI_R_MESH + This method iterates through all of the assemblies provided, or all + assemblies in the core if no list of `assems` is provided, and + constructs a tuple of three lists which contain the unique i, j, and + k mesh coordinates, respectively. The `applySubMesh` setting + controls whether the mesh will include the submesh coordinates. For + a standard assembly-based reactor geometry with a hexagonal or + Cartesian assembly grid, this method is only used to produce axial + (k) mesh points. If multiple assemblies are provided with different + axial meshes, the axial mesh list will contain the union of all + unique mesh points. Duplicate mesh points are removed. + Parameters ---------- assems : list, optional @@ -2146,8 +2267,8 @@ def getMaxNumPins(self): def getMinimumPercentFluxInFuel(self, target=0.005): """ - Goes through the entire reactor to determine what percentage of flux occures at - each ring. Starting with the outer ring, this function helps determine the effective + Goes through the entire reactor to determine what percentage of flux occurs at + each ring. Starting with the outer ring, this function helps determine the effective size of the core where additional assemblies will not help the breeding in the TWR. Parameters From cc830f62957fcedb9bf4b50059aa281a971c671b Mon Sep 17 00:00:00 2001 From: bdlafleur <brandon.lafleur@ge.com> Date: Wed, 24 Jan 2024 13:54:28 -0500 Subject: [PATCH 144/176] Adding impl tag descriptions to ``components`` (#1600) --- armi/reactor/components/basicShapes.py | 37 +++++++++-- armi/reactor/components/complexShapes.py | 23 ++++++- armi/reactor/components/component.py | 79 +++++++++++++++++++++--- armi/reactor/tests/test_components.py | 17 ++++- 4 files changed, 140 insertions(+), 16 deletions(-) diff --git a/armi/reactor/components/basicShapes.py b/armi/reactor/components/basicShapes.py index 02b98c2c6..4e395009b 100644 --- a/armi/reactor/components/basicShapes.py +++ b/armi/reactor/components/basicShapes.py @@ -27,9 +27,14 @@ class Circle(ShapedComponent): """A Circle. - .. impl:: Circle shaped component + .. impl:: Circle shaped Component :id: I_ARMI_COMP_SHAPES0 :implements: R_ARMI_COMP_SHAPES + + This class provides the implementation of a Circle Component. This includes + setting key parameters such as its material, temperature, and dimensions. It + also includes a method to retrieve the area of a Circle + Component via the ``getComponentArea`` method. """ is3D = False @@ -91,9 +96,15 @@ def isEncapsulatedBy(self, other): class Hexagon(ShapedComponent): """A Hexagon. - .. impl:: Hexagon shaped component + .. impl:: Hexagon shaped Component :id: I_ARMI_COMP_SHAPES1 :implements: R_ARMI_COMP_SHAPES + + This class provides the implementation of a hexagonal Component. This + includes setting key parameters such as its material, temperature, and + dimensions. It also includes methods for retrieving geometric + dimension information unique to hexagons such as the ``getPerimeter`` and + ``getPitchData`` methods. """ is3D = False @@ -176,9 +187,15 @@ def getPitchData(self): class Rectangle(ShapedComponent): """A Rectangle. - .. impl:: Rectangle shaped component + .. impl:: Rectangle shaped Component :id: I_ARMI_COMP_SHAPES2 :implements: R_ARMI_COMP_SHAPES + + This class provides the implementation for a rectangular Component. This + includes setting key parameters such as its material, temperature, and + dimensions. It also includes methods for computing geometric + information related to rectangles, such as the + ``getBoundingCircleOuterDiameter`` and ``getPitchData`` methods. """ is3D = False @@ -321,9 +338,14 @@ def getComponentArea(self, cold=False): class Square(Rectangle): """Square component that can be solid or hollow. - .. impl:: Square shaped component + .. impl:: Square shaped Component :id: I_ARMI_COMP_SHAPES3 :implements: R_ARMI_COMP_SHAPES + + This class provides the implementation for a square Component. This class + subclasses the ``Rectangle`` class because a square is a type of rectangle. + This includes setting key parameters such as its material, temperature, and + dimensions. """ is3D = False @@ -397,10 +419,15 @@ class Triangle(ShapedComponent): """ Triangle with defined base and height. - .. impl:: Triangle shaped component + .. impl:: Triangle shaped Component :id: I_ARMI_COMP_SHAPES4 :implements: R_ARMI_COMP_SHAPES + This class provides the implementation for defining a triangular Component. This + includes setting key parameters such as its material, temperature, and + dimensions. It also includes providing a method for retrieving the area of a + Triangle Component via the ``getComponentArea`` method. + Notes ----- The exact angles of the triangle are undefined. The exact side lenths and angles diff --git a/armi/reactor/components/complexShapes.py b/armi/reactor/components/complexShapes.py index 5cd8c742b..2f1ff9f6f 100644 --- a/armi/reactor/components/complexShapes.py +++ b/armi/reactor/components/complexShapes.py @@ -24,9 +24,14 @@ class HoledHexagon(basicShapes.Hexagon): """Hexagon with n uniform circular holes hollowed out of it. - .. impl:: Holed hexagon shaped component + .. impl:: Holed hexagon shaped Component :id: I_ARMI_COMP_SHAPES5 :implements: R_ARMI_COMP_SHAPES + + This class provides an implementation for a holed hexagonal Component. This + includes setting key parameters such as its material, temperature, and + dimensions. It also provides the capability to retrieve the diameter of the + inner hole via the ``getCircleInnerDiameter`` method. """ THERMAL_EXPANSION_DIMS = {"op", "holeOD"} @@ -197,9 +202,15 @@ def getCircleInnerDiameter(self, Tc=None, cold=False): class HoledSquare(basicShapes.Square): """Square with one circular hole in it. - .. impl:: Holed square shaped component + .. impl:: Holed square shaped Component :id: I_ARMI_COMP_SHAPES6 :implements: R_ARMI_COMP_SHAPES + + This class provides an implementation for a holed square Component. This + includes setting key parameters such as its material, temperature, and + dimensions. It also includes methods to retrieve geometric + dimension information unique to holed squares via the ``getComponentArea`` and + ``getCircleInnerDiameter`` methods. """ THERMAL_EXPANSION_DIMS = {"widthOuter", "holeOD"} @@ -252,10 +263,16 @@ def getCircleInnerDiameter(self, Tc=None, cold=False): class Helix(ShapedComponent): """A spiral wire component used to model a pin wire-wrap. - .. impl:: Helix shaped component + .. impl:: Helix shaped Component :id: I_ARMI_COMP_SHAPES7 :implements: R_ARMI_COMP_SHAPES + This class provides the implementation for a helical Component. This + includes setting key parameters such as its material, temperature, and + dimensions. It also includes the ``getComponentArea`` method to retrieve the + area of a helix. Helixes can be used for wire wrapping around fuel pins in fast + reactor designs. + Notes ----- http://mathworld.wolfram.com/Helix.html diff --git a/armi/reactor/components/component.py b/armi/reactor/components/component.py index c79e0e19c..337259cd5 100644 --- a/armi/reactor/components/component.py +++ b/armi/reactor/components/component.py @@ -173,11 +173,21 @@ class Component(composites.Composite, metaclass=ComponentType): :id: I_ARMI_COMP_DEF :implements: R_ARMI_COMP_DEF - .. impl:: Order components by their outermost diameter (using the < operator). + The primitive object in an ARMI reactor is a Component. A Component is comprised + of a shape and composition. This class serves as a base class which all + Component types within ARMI are built upon. All primitive shapes (such as a + square, circle, holed hexagon, helix etc.) are derived from this base class. + + Fundamental capabilities of this class include the ability to store parameters + and attributes which describe the physical state of each Component within the + ARMI data model. + + .. impl:: Order Components by their outermost diameter (using the < operator). :id: I_ARMI_COMP_ORDER :implements: R_ARMI_COMP_ORDER - This is done via the __lt__() method, which is used to control sort() as the + Determining Component order by outermost diameters is implemented via + the __lt__() method, which is used to control sort() as the standard approach in Python. However, __lt__() does not show up in the API. Attributes @@ -294,6 +304,13 @@ def resolveLinkedDims(self, components): .. impl:: The volume of some defined shapes depend on the solid components surrounding them. :id: I_ARMI_COMP_FLUID1 :implements: R_ARMI_COMP_FLUID + + Some Components are fluids and are thus defined by the shapes surrounding + them. This method cycles through each dimension defining the border of this + Component and converts the name of that Component to a link to the object + itself. This series of links is then used downstream to resolve + dimensional information. + """ for dimName in self.DIMENSION_NAMES: value = self.p[dimName] @@ -400,9 +417,13 @@ def getProperties(self): :id: I_ARMI_COMP_MAT0 :implements: R_ARMI_COMP_MAT + This method returns the material object that is assigned to the Component. + .. impl:: Components have one-and-only-one material. :id: I_ARMI_COMP_1MAT :implements: R_ARMI_COMP_1MAT + + This method returns the material object that is assigned to the Component. """ return self.material @@ -441,12 +462,14 @@ def setLumpedFissionProducts(self, lfpCollection): def getArea(self, cold=False): """ - Get the area of a component in cm^2. + Get the area of a Component in cm^2. - .. impl:: Set a dimension of a component. + .. impl:: Get a dimension of a Component. :id: I_ARMI_COMP_VOL0 :implements: R_ARMI_COMP_VOL + This method returns the area of a Component. + See Also -------- block.getVolumeFractions: component coolant is typically the "leftover" and is calculated and set here @@ -466,12 +489,14 @@ def getArea(self, cold=False): def getVolume(self): """ - Return the volume [cm^3] of the component. + Return the volume [cm^3] of the Component. - .. impl:: Set a dimension of a component. + .. impl:: Get a dimension of a Component. :id: I_ARMI_COMP_VOL1 :implements: R_ARMI_COMP_VOL + This method returns the volume of a Component. + Notes ----- ``self.p.volume`` is not set until this method is called, @@ -574,6 +599,10 @@ def containsSolidMaterial(self): .. impl:: Determine if a material is solid. :id: I_ARMI_COMP_SOLID :implements: R_ARMI_COMP_SOLID + + For certain operations it is important to know if a Component is a solid or + fluid material. This method will return a boolean indicating if the material + is solid or not by checking if the material is an instance of the ``material.Fluid`` class. """ return not isinstance(self.material, material.Fluid) @@ -677,6 +706,10 @@ def setNumberDensity(self, nucName, val): :id: I_ARMI_COMP_NUCLIDE_FRACS0 :implements: R_ARMI_COMP_NUCLIDE_FRACS + The method allows a user or plugin to set the number density of a Component. + It also indicates to other processes that may depend on a Component's + status about this change via the ``assigned`` attribute. + Parameters ---------- nucName : str @@ -699,6 +732,10 @@ def setNumberDensities(self, numberDensities): :id: I_ARMI_COMP_NUCLIDE_FRACS1 :implements: R_ARMI_COMP_NUCLIDE_FRACS + The method allows a user or plugin to set the number densities of a + Component. In contrast to the ``setNumberDensity`` method, it sets all + densities within a Component. + Parameters ---------- numberDensities : dict @@ -802,10 +839,22 @@ def setDimension(self, key, val, retainLink=False, cold=True): """ Set a single dimension on the component. - .. impl:: Set a component dimension, considering thermal expansion. + .. impl:: Set a Component dimension, considering thermal expansion. :id: I_ARMI_COMP_EXPANSION1 :implements: R_ARMI_COMP_EXPANSION + Dimensions should be set considering the impact of thermal expansion. This + method allows for a user or plugin to set a dimension and indicate if the + dimension is for a cold configuration or not. If it is not for a cold + configuration, the thermal expansion factor is considered when setting the + dimension. + + If the ``retainLink`` argument is ``True``, any Components linked to this + one will also have its dimensions changed consistently. After a dimension + is updated, the ``clearLinkedCache`` method is called which sets the + volume of this Component to ``None``. This ensures that when the volume is + next accessed it is recomputed using the updated dimensions. + Parameters ---------- key : str @@ -843,6 +892,13 @@ def getDimension(self, key, Tc=None, cold=False): :id: I_ARMI_COMP_DIMS :implements: R_ARMI_COMP_DIMS + Due to thermal expansion, Component dimensions depend on their temperature. + This method retrieves a dimension from the Component at a particular + temperature, if provided. If the Component is a LinkedComponent then the + dimensions are resolved to ensure that any thermal expansion that has + occurred to the Components that the LinkedComponent depends on is reflected + in the returned dimension. + Parameters ---------- key : str @@ -919,6 +975,15 @@ def getThermalExpansionFactor(self, Tc=None, T0=None): :id: I_ARMI_COMP_EXPANSION0 :implements: R_ARMI_COMP_EXPANSION + This method enables the calculation of the thermal expansion factor + for a given material. If the material is solid, the difference + between T0 and Tc is used to calculate the thermal expansion + factor. If a solid material does not have a linear expansion factor + defined and the temperature difference is greater than + :py:attr:`armi.reactor.components.component.Component._TOLERANCE`, an + error is raised. Thermal expansion of fluids or custom materials is + neglected, currently. + Parameters ---------- Tc : float, optional diff --git a/armi/reactor/tests/test_components.py b/armi/reactor/tests/test_components.py index 63ed62512..fdd031f43 100644 --- a/armi/reactor/tests/test_components.py +++ b/armi/reactor/tests/test_components.py @@ -576,7 +576,22 @@ def test_getArea(self): self.assertAlmostEqual(cur, ref) def test_componentInteractionsLinkingByDimensions(self): - """Tests linking of components by dimensions.""" + """Tests linking of Components by dimensions. + + .. test:: Show the dimensions of a liquid Component can be defined to depend on the solid Components that bound it. + :id: T_ARMI_COMP_FLUID1 + :tests: R_ARMI_COMP_FLUID + + The component ``gap``, representing the fuel-clad gap filled with Void, + is defined with dimensions that depend on the fuel outer diameter and + clad inner diameter. The + :py:meth:`~armi.reactor.components.component.Component.resolveLinkedDims` + method links the gap dimensions appropriately when the Component is + constructed, and the test shows the area of the gap is calculated + correctly based on the thermally-expanded dimensions of the fuel and + clad Components. + + """ nPins = 217 fuelDims = {"Tinput": 25.0, "Thot": 430.0, "od": 0.9, "id": 0.0, "mult": nPins} cladDims = {"Tinput": 25.0, "Thot": 430.0, "od": 1.1, "id": 1.0, "mult": nPins} From 75ced0be47044874c86e0571dc2bd421fc0c245e Mon Sep 17 00:00:00 2001 From: bdlafleur <brandon.lafleur@ge.com> Date: Wed, 24 Jan 2024 14:30:07 -0500 Subject: [PATCH 145/176] Adding impl tag descriptions to ``composites`` (#1599) --- armi/reactor/composites.py | 85 +++++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/armi/reactor/composites.py b/armi/reactor/composites.py index 763b5cb9d..229e98e31 100644 --- a/armi/reactor/composites.py +++ b/armi/reactor/composites.py @@ -300,6 +300,14 @@ class ArmiObject(metaclass=CompositeModelType): :id: I_ARMI_PARAM_PART :implements: R_ARMI_PARAM_PART + An ARMI reactor model is composed of collections of ARMIObject objects. These + objects are combined in a hierarchical manner. Each level of the composite tree + is able to be assigned parameters which define it, such as temperature, flux, + or keff values. This class defines an attribute of type ``ParameterCollection``, + which contains all the functionality of an ARMI ``Parameter`` object. Because + the entire model is composed of ARMIObjects at the most basic level, each level + of the Composite tree contains this parameter attribute and can thus be queried. + Attributes ---------- name : str @@ -628,6 +636,10 @@ def getParameterCollection(cls): :id: I_ARMI_CMP_PARAMS :implements: R_ARMI_CMP_PARAMS + This class method allows a user to obtain the + ``paramCollection`` object, which is the object containing the interface for + all parameters of an ARMI object. + See Also -------- :py:meth:`armi.reactor.parameters.parameterCollections.ParameterCollection.__reduce__` @@ -662,6 +674,8 @@ def getName(self): .. impl:: Composite name is accessible. :id: I_ARMI_CMP_GET_NAME :implements: R_ARMI_CMP_GET_NAME + + This method returns the name of a Composite. """ return self.name @@ -672,10 +686,21 @@ def hasFlags(self, typeID: TypeSpec, exact=False): """ Determine if this object is of a certain type. - .. impl:: Composites have queriable flags. + .. impl:: Composites have queryable flags. :id: I_ARMI_CMP_FLAG0 :implements: R_ARMI_CMP_FLAG + This method queries the flags (i.e. the ``typeID``) of the Composite for a + given type, returning a boolean representing whether or not the candidate + flag is present in this ArmiObject. Candidate flags cannot be passed as a + ``string`` type and must be of a type ``Flag``. If no flags exist in the + object then ``False`` is returned. + + If a list of flags is provided, then all input flags will be + checked against the flags of the object. If exact is ``False``, then the + object must have at least one of candidates exactly. If it is ``True`` then + the object flags and candidates must match exactly. + Parameters ---------- typeID : TypeSpec @@ -771,6 +796,8 @@ def setType(self, typ, flags: Optional[Flags] = None): :id: I_ARMI_CMP_FLAG1 :implements: R_ARMI_CMP_FLAG + This method allows for the setting of flags parameter of the Composite. + Parameters ---------- typ : str @@ -889,6 +916,10 @@ def getMass(self, nuclideNames=None): :id: I_ARMI_CMP_GET_MASS :implements: R_ARMI_CMP_GET_MASS + This method allows for the querying of the mass of a Composite. + If the ``nuclideNames`` argument is included, it will filter for the mass + of those nuclide names and provide the sum of the mass of those nuclides. + Parameters ---------- nuclideNames : str, optional @@ -1236,6 +1267,10 @@ def getNumberDensity(self, nucName): :id: I_ARMI_CMP_NUC0 :implements: R_ARMI_CMP_NUC + This method queries the number density + of a specific nuclide within the Composite. It invokes the + ``getNuclideNumberDensities`` method for just the requested nuclide. + Notes ----- This can get called very frequently and has to do volume computations so should @@ -1256,6 +1291,12 @@ def getNuclideNumberDensities(self, nucNames): .. impl:: Get number densities for specific nuclides. :id: I_ARMI_CMP_NUC1 :implements: R_ARMI_CMP_NUC + + This method provides the capability to query the volume weighted number + densities for a list of nuclides within a given Composite. It provides the + result in units of atoms/barn-cm. The volume weighting is accomplished by + multiplying the number densities within each child Composite by the volume + of the child Composite and dividing by the total volume of the Composite. """ volumes = numpy.array( [ @@ -1297,6 +1338,14 @@ def getNumberDensities(self, expandFissionProducts=False): :id: I_ARMI_CMP_GET_NDENS :implements: R_ARMI_CMP_GET_NDENS + This method provides a way for retrieving the number densities + of all nuclides within the Composite. It does this by leveraging the + ``_getNdensHelper`` method, which invokes the ``getNuclideNumberDensities`` + method. This method considers the nuclides within each child Composite of + this composite (if they exist). If the ``expandFissionProducts`` flag is + ``True``, then the lumped fission products are expanded to include their + constituent elements via the ``_expandLFPs`` method. + Parameters ---------- expandFissionProducts : bool (optional) @@ -2456,6 +2505,12 @@ def getComponentByName(self, name): :id: I_ARMI_CMP_BY_NAME :implements: R_ARMI_CMP_BY_NAME + Each Composite has a name, and some Composites are made up + of collections of child Composites. This method retrieves a child + Component from this Composite by searching for it by name. If more than + one Component shares the same name, it raises a ``ValueError``. If no + Components are found by the input name then ``None`` is returned. + Parameters ---------- name : str @@ -2671,6 +2726,15 @@ class Composite(ArmiObject): .. impl:: Composites are a physical part of the reactor in a hierarchical data model. :id: I_ARMI_CMP0 :implements: R_ARMI_CMP + + An ARMI reactor model is composed of collections of ARMIObject objects. This + class is a child-class of the ARMIObject class and provides a structure + allowing a reactor model to be composed of Composites. + + This class provides various methods to query and modify the hierarchical ARMI + reactor model, including but not limited to, iterating, sorting, and adding or + removing child Composites. + """ def __init__(self, name): @@ -2780,6 +2844,18 @@ def getChildren( :id: I_ARMI_CMP1 :implements: R_ARMI_CMP + This method retrieves all children within a given Composite object. Children + of any generation can be retrieved. This is achieved by visiting all + children and calling this method recursively for each generation requested. + + If the method is called with ``includeMaterials``, it will additionally + include information about the material for each child. If a function is + supplied as the ``predicate`` argument, then this method will be used + to evaluate all children as a filter to include or not. For example, if the + caller of this method only desires children with a certain flag, or children + which only contain a certain material, then the ``predicate`` function + can be used to perform this filtering. + Parameters ---------- deep : boolean, optional @@ -2898,6 +2974,13 @@ def syncMpiState(self): :id: I_ARMI_CMP_MPI :implements: R_ARMI_CMP_MPI + Parameters need to be handled properly during parallel code execution.This + method synchronizes all parameters of the composite object across all + processes by cycling through all the children of the Composite and ensuring + that their parameters are properly synchronized. If it fails to synchronize, + an error message is displayed which alerts the user to which Composite has + inconsistent data across the processes. + Returns ------- int From 8bfd04887ea9708575b26ae22a488a27406acd4d Mon Sep 17 00:00:00 2001 From: Chris Keckler <ckeckler@terrapower.com> Date: Wed, 24 Jan 2024 14:41:14 -0600 Subject: [PATCH 146/176] Add longer descriptions for `fuelCycle` impls (#1589) * Add longer descriptions for fuelCycle impls * Clarify the use of case settings in I_ARMI_SHUFFLE * Change impl description voicing Co-authored-by: bsculac <102382931+bsculac@users.noreply.github.com> * Update armi/physics/fuelCycle/fuelHandlers.py Co-authored-by: bsculac <102382931+bsculac@users.noreply.github.com> * Update armi/physics/fuelCycle/fuelHandlers.py Co-authored-by: bsculac <102382931+bsculac@users.noreply.github.com> * Update armi/physics/fuelCycle/fuelHandlers.py Co-authored-by: bsculac <102382931+bsculac@users.noreply.github.com> * Disambiguate the implementations for R_ARMI_SHUFFLE_STATIONARY * black --------- Co-authored-by: bsculac <102382931+bsculac@users.noreply.github.com> --- .../physics/fuelCycle/fuelHandlerInterface.py | 17 +++++++ armi/physics/fuelCycle/fuelHandlers.py | 50 +++++++++++++++---- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/armi/physics/fuelCycle/fuelHandlerInterface.py b/armi/physics/fuelCycle/fuelHandlerInterface.py index 31d17917f..79637c320 100644 --- a/armi/physics/fuelCycle/fuelHandlerInterface.py +++ b/armi/physics/fuelCycle/fuelHandlerInterface.py @@ -36,6 +36,23 @@ class FuelHandlerInterface(interfaces.Interface): :id: I_ARMI_SHUFFLE :implements: R_ARMI_SHUFFLE + This interface allows for a user to define custom shuffle logic that + modifies to the core model. Being based on the :py:class:`~armi.interfaces.Interface` + class, it has direct access to the current core model. + + User logic is able to be executed from within the + :py:meth:`~armi.physics.fuelCycle.fuelHandlerInterface.FuelHandlerInterface.manageFuel` method, + which will use the :py:meth:`~armi.physics.fuelCycle.fuelHandlerFactory.fuelHandlerFactory` + to search for a Python file specified by the case setting ``shuffleLogic``. + If it exists, the fuel handler with name specified by the user via the ``fuelHandlerName`` + case setting will be imported, and any actions in its ``outage`` method + will be executed at the :py:meth:`~armi.physics.fuelCycle.fuelHandlerInterface.FuelHandlerInterface.interactBOC` + hook. + + If no class with the name specified by the ``fuelHandlerName`` setting is found + in the file with path ``shuffleLogic``, an error is returned. + + See the user manual for how the custom shuffle logic file should be constructed. """ name = "fuelHandler" diff --git a/armi/physics/fuelCycle/fuelHandlers.py b/armi/physics/fuelCycle/fuelHandlers.py index ac463bf5e..12b98da69 100644 --- a/armi/physics/fuelCycle/fuelHandlers.py +++ b/armi/physics/fuelCycle/fuelHandlers.py @@ -727,10 +727,31 @@ def swapAssemblies(self, a1, a2): :id: I_ARMI_SHUFFLE_MOVE :implements: R_ARMI_SHUFFLE_MOVE - .. impl:: User-specified blocks can be left in place and not moved. + 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. + + 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. + Parameters ---------- a1 : :py:class:`Assembly <armi.reactor.assemblies.Assembly>` @@ -738,11 +759,6 @@ def swapAssemblies(self, a1, a2): a2 : :py:class:`Assembly <armi.reactor.assemblies.Assembly>` The second assembly - Notes - ----- - The implementation for ``R_ARMI_SHUFFLE_STATIONARY`` occurs within - :py:meth:`<armi.physics.fuelCyle.fuelHandlers.FuelHandler._transferStationaryBlocks`. - See Also -------- dischargeSwap : swap assemblies where one is outside the core and the other is inside @@ -831,14 +847,26 @@ def dischargeSwap(self, incoming, outgoing): outgoing : :py:class:`Assembly <armi.reactor.assemblies.Assembly>` The assembly getting discharged out the core. - .. impl:: User-specified blocks can be left in place and not moved. + .. impl:: User-specified blocks can be left in place for the discharge swap. :id: I_ARMI_SHUFFLE_STATIONARY1 :implements: R_ARMI_SHUFFLE_STATIONARY - Notes - ----- - The implementation for ``R_ARMI_SHUFFLE_STATIONARY`` occurs within - :py:meth:`<armi.physics.fuelCyle.fuelHandlers.FuelHandler._transferStationaryBlocks`. + 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. + + Once this process is complete, the actual assembly movement can take + place. Through this process, the stationary blocks from the outgoing + assembly remain in the original core position, while the stationary + blocks from the incoming assembly are discharged with the outgoing assembly. See Also -------- From 5661fceba802a71dcda0107b0166e92af68b418e Mon Sep 17 00:00:00 2001 From: Chris Keckler <ckeckler@terrapower.com> Date: Wed, 24 Jan 2024 14:50:06 -0600 Subject: [PATCH 147/176] Add longer descriptions to `blueprints` impls (#1606) * Add some impl descriptions, but not done with all of the needed ones in blueprints module yet * Fix a grammar typo * Add impl details for I_ARMI_BP_GRID * Consolidate two impl tags into one and include a long-form description * Fix grammatical error * Add descriptions for impls in isotopicOptions * Add impl description for I_ARMI_BP_TO_DB and reformat the docstring * Fix need tag formatting * Fix formatting of quote * Some editorial and grammatical fixes * This is a spelling change - I'll accept it for Chris Co-authored-by: kasticrunch <43149783+kasticrunch@users.noreply.github.com> --------- Co-authored-by: John Stilley <1831479+john-science@users.noreply.github.com> Co-authored-by: kasticrunch <43149783+kasticrunch@users.noreply.github.com> --- armi/reactor/blueprints/assemblyBlueprint.py | 35 +++++++++++++ armi/reactor/blueprints/blockBlueprint.py | 20 ++++++++ armi/reactor/blueprints/componentBlueprint.py | 51 +++++++++++++++++++ armi/reactor/blueprints/gridBlueprint.py | 45 +++++++++++++--- armi/reactor/blueprints/isotopicOptions.py | 38 ++++++++++++++ armi/reactor/blueprints/reactorBlueprint.py | 28 +++++++--- 6 files changed, 202 insertions(+), 15 deletions(-) diff --git a/armi/reactor/blueprints/assemblyBlueprint.py b/armi/reactor/blueprints/assemblyBlueprint.py index 54744353e..8e13d1fcb 100644 --- a/armi/reactor/blueprints/assemblyBlueprint.py +++ b/armi/reactor/blueprints/assemblyBlueprint.py @@ -79,6 +79,24 @@ class MaterialModifications(yamlize.Map): .. impl:: User-impact on material definitions. :id: I_ARMI_MAT_USER_INPUT0 :implements: R_ARMI_MAT_USER_INPUT + + Defines a yaml map attribute for the assembly portion of the blueprints + (see :py:class:`~armi.blueprints.assemblyBlueprint.AssemblyBlueprint`) that + allows users to specify material attributes as lists corresponding to + each axial block in the assembly. Two types of specifications can be made: + + 1. Key-value pairs can be specified directly, where the key is the + name of the modification and the value is the list of block values. + + 2. The "by component" attribute can be used, in which case the user + can specify material attributes that are specific to individual components + in each block. This is enabled through the :py:class:`~armi.reactor.blueprints.assemblyBlueprint.ByComponentModifications` + class, which basically just allows for one additional layer of attributes + corresponding to the component names. + + These material attributes can be used during the resolution of material + classes during core instantiation (see :py:meth:`~armi.reactor.blueprints.blockBlueprint.BlockBlueprint.construct` + and :py:meth:`~armi.reactor.blueprints.componentBlueprint.ComponentBlueprint.construct`). """ key_type = yamlize.Typed(str) @@ -100,6 +118,23 @@ class AssemblyBlueprint(yamlize.Object): .. impl:: Create assembly from blueprint file. :id: I_ARMI_BP_ASSEM :implements: R_ARMI_BP_ASSEM + + Defines a yaml construct that allows the user to specify attributes of an + assembly from within their blueprints file, including a name, flags, specifier + for use in defining a core map, a list of blocks, a list of block heights, + a list of axial mesh points in each block, a list of cross section identifiers + for each block, and material options (see :need:`I_ARMI_MAT_USER_INPUT0`). + + Relies on the underlying infrastructure from the ``yamlize`` package for + reading from text files, serialization, and internal storage of the data. + + Is implemented as part of a blueprints file by being imported and used + as an attribute within the larger :py:class:`~armi.reactor.blueprints.Blueprints` + class. + + Includes a ``construct`` method, which instantiates an instance of + :py:class:`~armi.reactor.assemblies.Assembly` with the characteristics + as specified in the blueprints. """ name = yamlize.Attribute(type=str) diff --git a/armi/reactor/blueprints/blockBlueprint.py b/armi/reactor/blueprints/blockBlueprint.py index a3326ab06..7d5c504ab 100644 --- a/armi/reactor/blueprints/blockBlueprint.py +++ b/armi/reactor/blueprints/blockBlueprint.py @@ -47,6 +47,26 @@ class BlockBlueprint(yamlize.KeyedList): .. impl:: Create a Block from blueprint file. :id: I_ARMI_BP_BLOCK :implements: R_ARMI_BP_BLOCK + + Defines a yaml construct that allows the user to specify attributes of a + block from within their blueprints file, including a name, flags, a radial + grid to specify locations of pins, and the name of a component which + drives the axial expansion of the block (see :py:mod:`~armi.reactor.converters.axialExpansionChanger`). + + In addition, the user may specify key-value pairs to specify the components + contained within the block, where the keys are component names and the + values are component blueprints (see :py:class:`~armi.reactor.blueprints.ComponentBlueprint.ComponentBlueprint`). + + Relies on the underlying infrastructure from the ``yamlize`` package for + reading from text files, serialization, and internal storage of the data. + + Is implemented into a blueprints file by being imported and used + as an attribute within the larger :py:class:`~armi.reactor.blueprints.Blueprints` + class. + + Includes a ``construct`` method, which instantiates an instance of + :py:class:`~armi.reactor.blocks.Block` with the characteristics + as specified in the blueprints. """ item_type = componentBlueprint.ComponentBlueprint diff --git a/armi/reactor/blueprints/componentBlueprint.py b/armi/reactor/blueprints/componentBlueprint.py index 4994eca18..e1404cd2b 100644 --- a/armi/reactor/blueprints/componentBlueprint.py +++ b/armi/reactor/blueprints/componentBlueprint.py @@ -123,6 +123,30 @@ class ComponentBlueprint(yamlize.Object): .. impl:: Construct component from blueprint file. :id: I_ARMI_BP_COMP :implements: R_ARMI_BP_COMP + + Defines a yaml construct that allows the user to specify attributes of a + component from within their blueprints file, including a name, flags, shape, + material and/or isotopic vector, input temperature, corresponding component dimensions, + and ID for placement in a block lattice (see :py:class:`~armi.reactor.blueprints.blockBlueprint.BlockBlueprint`). + Component dimensions that can be defined for a given component are dependent + on the component's ``shape`` attribute, and the dimensions defining each + shape can be found in the :py:mod:`~armi.reactor.components` module. + + Limited validation on the inputs is performed to ensure that the component + shape corresponds to a valid shape defined by the ARMI application. + + Relies on the underlying infrastructure from the ``yamlize`` package for + reading from text files, serialization, and internal storage of the data. + + Is implemented as part of a blueprints file by being imported and used + as an attribute within the larger :py:class:`~armi.reactor.blueprints.Blueprints` + class. Can also be used within the :py:class:`~armi.reactor.blueprints.blockBlueprint.BlockBlueprint` + class to enable specification of components directly within the "blocks" + portion of the blueprint file. + + Includes a ``construct`` method, which instantiates an instance of + :py:class:`~armi.reactor.components.component.Component` with the characteristics + specified in the blueprints (see :need:`I_ARMI_MAT_USER_INPUT1`). """ name = yamlize.Attribute(type=str) @@ -167,6 +191,19 @@ def construct(self, blueprint, matMods): .. impl:: User-defined on material alterations are applied here. :id: I_ARMI_MAT_USER_INPUT1 :implements: R_ARMI_MAT_USER_INPUT + + Allows for user input to impact a component's materials by applying + the "material modifications" section of a blueprints file (see :need:`I_ARMI_MAT_USER_INPUT0`) + to the material during construction. This takes place during lower + calls to ``_conformKwargs()`` and subsequently ``_constructMaterial()``, + which operate using the component blueprint and associated material + modifications from the component's block. + + Within ``_constructMaterial()``, the material class is resolved into a material + object by calling :py:func:`~armi.materials.resolveMaterialClassByName`. + The ``applyInputParams()`` method of that material class is then called, + passing in the associated material modifications data, which the material + class can then use to modify the isotopics as necessary. """ runLog.debug("Constructing component {}".format(self.name)) kwargs = self._conformKwargs(blueprint, matMods) @@ -300,6 +337,20 @@ def insertDepletableNuclideKeys(c, blueprint): :id: I_ARMI_BP_NUC_FLAGS0 :implements: R_ARMI_BP_NUC_FLAGS + This is called during the component construction process for each component from within + :py:meth:`~armi.reactor.blueprints.componentBlueprint.ComponentBlueprint.construct`. + + For a given initialized component, check its flags to determine if it + has been marked as depletable. If it is, use :py:func:`~armi.nucDirectory.nuclideBases.initReachableActiveNuclidesThroughBurnChain` + to apply the user-specifications in the "nuclide flags" section of the blueprints + to the component such that all active isotopes and derivatives of those + isotopes in the burn chain are initialized to have an entry in the component's + ``numberDensities`` dictionary. + + Note that certain case settings, including ``fpModel`` and ``fpModelLibrary``, + may trigger modifications to the active nuclides specified by the user + in the "nuclide flags" section of the blueprints. + Notes ----- This should be moved to a neutronics/depletion plugin hook but requires some diff --git a/armi/reactor/blueprints/gridBlueprint.py b/armi/reactor/blueprints/gridBlueprint.py index 22882c3c0..0fe012454 100644 --- a/armi/reactor/blueprints/gridBlueprint.py +++ b/armi/reactor/blueprints/gridBlueprint.py @@ -148,6 +148,24 @@ class GridBlueprint(yamlize.Object): :id: I_ARMI_BP_GRID :implements: R_ARMI_BP_GRID + Defines a yaml construct that allows the user to specify a grid + from within their blueprints file, including a name, geometry, dimensions, + symmetry, and a map with the relative locations of components within that grid. + + Relies on the underlying infrastructure from the ``yamlize`` package for + reading from text files, serialization, and internal storage of the data. + + Is implemented as part of a blueprints file by being used in key-value pairs + within the :py:class:`~armi.reactor.blueprints.gridBlueprint.Grid` class, + which is imported and used as an attribute within the larger :py:class:`~armi.reactor.blueprints.Blueprints` + class. + + Includes a ``construct`` method, which instantiates an instance of one + of the subclasses of :py:class:`~armi.reactor.grids.structuredgrid.StructuredGrid`. + This is typically called from within :py:meth:`~armi.reactor.blueprints.blockBlueprint.BlockBlueprint.construct`, + which then also associates the individual components in the block with + locations specifed in the grid. + Attributes ---------- name : str @@ -526,21 +544,34 @@ def _filterOutsideDomain(gridBp): def saveToStream(stream, bluep, full=False, tryMap=False): - """Save the blueprints to the passed stream. + """ + Save the blueprints to the passed stream. This can save either the entire blueprints, or just the `grids:` section of the blueprints, based on the passed ``full`` argument. Saving just the grid blueprints can be useful when cobbling blueprints together with !include flags. - stream: file output stream of some kind - bluep: armi.reactor.blueprints.Blueprints, or Grids - full: bool ~ Is this a full output file, or just a partial/grids? - tryMap: regardless of input form, attempt to output as a lattice map. let's face it; - they're prettier. - .. impl:: Write a blueprint file from a blueprint object. :id: I_ARMI_BP_TO_DB :implements: R_ARMI_BP_TO_DB + + First makes a copy of the blueprints that are passed in. Then modifies + any grids specified in the blueprints into a canonical lattice map style, + if needed. Then uses the ``dump`` method that is inherent to all ``yamlize`` + subclasses to write the blueprints to the given ``stream`` object. + + If called with the ``full`` argument, the entire blueprints is dumped. + If not, only the grids portion is dumped. + + Parameters + ---------- + stream : + file output stream of some kind + bluep : armi.reactor.blueprints.Blueprints, or Grids + full : bool + Is this a full output file, or just a partial/grids? + tryMap : bool + regardless of input form, attempt to output as a lattice map """ # To save, we want to try our best to output our grid blueprints in the lattice # map style. However, we do not want to wreck the state that the current diff --git a/armi/reactor/blueprints/isotopicOptions.py b/armi/reactor/blueprints/isotopicOptions.py index f39f5c9ea..c4784fed2 100644 --- a/armi/reactor/blueprints/isotopicOptions.py +++ b/armi/reactor/blueprints/isotopicOptions.py @@ -65,6 +65,24 @@ class NuclideFlag(yamlize.Object): :id: I_ARMI_BP_NUC_FLAGS1 :implements: R_ARMI_BP_NUC_FLAGS + This class creates a yaml interface for the user to specify in their blueprints + which isotopes should be depleted. It is incorporated into the "nuclide flags" + section of a blueprints file by being included as key-value pairs within + the :py:class:`~armi.reactor.blueprints.isotopicOptions.NuclideFlags` class, + which is in turn included into the overall blueprints within + :py:class:`~armi.reactor.blueprints.Blueprints`. + + This class includes a boolean ``burn`` attribute which can be specified + for any nuclide. This attribute is examined by the :py:meth:`~armi.reactor.blueprints.isotopicOptions.NuclideFlag.fileAsActiveOrInert` + method to sort the nuclides into sets of depletable or not, which is typically + called during construction of assemblies in :py:meth:`~armi.reactor.blueprints.Blueprints.constructAssem`. + + Note that while the ``burn`` attribute can be set by the user in the blueprints, + other methods may also set it based on case settings (see, for instance, + :py:func:`~armi.reactor.blueprints.isotopicOptions.genDefaultNucFlags`, + :py:func:`~armi.reactor.blueprints.isotopicOptions.autoUpdateNuclideFlags`, and + :py:func:`~armi.reactor.blueprints.isotopicOptions.getAllNuclideBasesByLibrary`). + Attributes ---------- nuclideName : str @@ -154,6 +172,26 @@ class CustomIsotopic(yamlize.Map): .. impl:: Certain material modifications will be applied using this code. :id: I_ARMI_MAT_USER_INPUT2 :implements: R_ARMI_MAT_USER_INPUT + + Defines a yaml construct that allows the user to define a custom isotopic vector + from within their blueprints file, including a name and key-value pairs + corresponding to nuclide names and their concentrations. + + Relies on the underlying infrastructure from the ``yamlize`` package for + reading from text files, serialization, and internal storage of the data. + + Is implemented as part of a blueprints file by being used in key-value pairs + within the :py:class:`~armi.reactor.blueprints.isotopicOptions.CustomIsotopics` class, + which is imported and used as an attribute within the larger :py:class:`~armi.reactor.blueprints.Blueprints` + class. + + These isotopics are linked to a component during calls to :py:meth:`~armi.reactor.blueprints.componentBlueprint.ComponentBlueprint.construct`, + where the name specified in the ``isotopics`` attribute of the component blueprint + is searched against the available ``CustomIsotopics`` defined in the + "custom isotopics" section of the blueprints. + Once linked, the :py:meth:`~armi.reactor.blueprints.isotopicOptions.CustomIsotopic.apply` + method is called, which adjusts the ``massFrac`` attribute of the component's + material class. """ key_type = yamlize.Typed(str) diff --git a/armi/reactor/blueprints/reactorBlueprint.py b/armi/reactor/blueprints/reactorBlueprint.py index 4875961de..a52104435 100644 --- a/armi/reactor/blueprints/reactorBlueprint.py +++ b/armi/reactor/blueprints/reactorBlueprint.py @@ -49,6 +49,26 @@ class SystemBlueprint(yamlize.Object): """ The reactor-level structure input blueprint. + .. impl:: Build core and spent fuel pool from blueprints + :id: I_ARMI_BP_SYSTEMS + :implements: R_ARMI_BP_SYSTEMS, R_ARMI_BP_CORE + + This class creates a yaml interface for the user to define systems with + grids, such as cores or spent fuel pools, each having their own name, + type, grid, and position in space. It is incorporated into the "systems" + section of a blueprints file by being included as key-value pairs within + the :py:class:`~armi.reactor.blueprints.reactorBlueprint.Systems` class, + which is in turn included into the overall blueprints within + :py:class:`~armi.reactor.blueprints.Blueprints`. + + This class includes a :py:meth:`~armi.reactor.blueprints.reactorBlueprint.SystemBlueprint.construct` + method, which is typically called from within :py:func:`~armi.reactor.reactors.factory` + during the initialization of the reactor object to instantiate the core + and/or spent fuel pool objects. During that process, a spatial grid is + constructed based on the grid blueprints specified in the "grids" section + of the blueprints (see :need:`I_ARMI_BP_GRID`) and the assemblies needed + to fill the lattice are built from blueprints using :py:meth:`~armi.reactor.blueprints.Blueprints.constructAssem`. + .. note:: We use string keys to link grids to objects that use them. This differs from how blocks/assembies are specified, which use YAML anchors. YAML anchors have proven to be problematic and difficult to work with @@ -101,14 +121,6 @@ def _resolveSystemType(typ: str): def construct(self, cs, bp, reactor, geom=None, loadAssems=True): """Build a core/IVS/EVST/whatever and fill it with children. - .. impl:: Build core and spent fuel pool from blueprint - :id: I_ARMI_BP_SYSTEMS - :implements: R_ARMI_BP_SYSTEMS - - .. impl:: Create core object from blueprint. - :id: I_ARMI_BP_CORE - :implements: R_ARMI_BP_CORE - Parameters ---------- cs : :py:class:`Settings <armi.settings.Settings>` object. From 73803596b049e8d135080c9283a8f71c55d2ee17 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Wed, 24 Jan 2024 13:25:52 -0800 Subject: [PATCH 148/176] Adding implementation text for BlockConverters (#1616) --- armi/reactor/converters/blockConverters.py | 41 ++++++++++++++++++---- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/armi/reactor/converters/blockConverters.py b/armi/reactor/converters/blockConverters.py index 6eadc5bf6..046aa6fb0 100644 --- a/armi/reactor/converters/blockConverters.py +++ b/armi/reactor/converters/blockConverters.py @@ -216,6 +216,20 @@ class ComponentMerger(BlockConverter): .. impl:: Homogenize one component into another. :id: I_ARMI_BLOCKCONV0 :implements: R_ARMI_BLOCKCONV + + This subclass of ``BlockConverter`` is meant as a one-time-use tool, to convert + a ``Block`` into one ``Component``. A ``Block`` is a ``Composite`` that may + probably has multiple ``Components`` somewhere in it. This means averaging the + material properties in the original ``Block``, and ensuring that the final + ``Component`` has the same shape and volume as the original ``Block``. This + subclass essentially just uses the base class method + ``dissolveComponentIntoComponent()`` given prescribed solute and solvent + materials, to define the merger. + + Notes + ----- + It is the job of the developer to determine if merging a Block into one Component + will yield valid or sane results. """ def __init__(self, sourceBlock, soluteName, solventName): @@ -250,17 +264,23 @@ class MultipleComponentMerger(BlockConverter): liner was dissolved first, this would normally cause a ValueError in _verifyExpansion since the clad would be completely expanded over a non void component. - This could be implemented on the regular ComponentMerger, as the Flags system has enough power - in the type specification arguments to things like ``getComponents()``, ``hasFlags()``, etc., to - do single and multiple components with the same code. - - .. impl:: Homogenize one component into another. + .. impl:: Homogenize multiple components into one. :id: I_ARMI_BLOCKCONV1 :implements: R_ARMI_BLOCKCONV + + This subclass of ``BlockConverter`` is meant as a one-time-use tool, to convert + a multiple ``Components`` into one. This means averaging the material + properties in the original ``Components``, and ensuring that the final + ``Component`` has the same shape and volume as all of the originals. This + subclass essentially just uses the base class method + ``dissolveComponentIntoComponent()`` given prescribed solute and solvent + materials, to define the merger. Though care is taken here to ensure the merger + isn't verified until it is completely finished. """ def __init__(self, sourceBlock, soluteNames, solventName, specifiedMinID=0.0): - """ + """Standard constructor method. + Parameters ---------- sourceBlock : :py:class:`armi.reactor.blocks.Block` @@ -540,6 +560,15 @@ def convert(self): .. impl:: Convert hex blocks to cylindrical blocks. :id: I_ARMI_BLOCKCONV_HEX_TO_CYL :implements: R_ARMI_BLOCKCONV_HEX_TO_CYL + + This method converts a ``HexBlock`` to a cylindrical ``Block``. Obviously, + this is not a physically meaningful transition; it is a helpful + approximation tool for analysts. This is a subclass of + ``BlockAvgToCylConverter`` which is a subclass of ``BlockConverter``. This + converter expects the ``sourceBlock`` and ``driverFuelBlock`` to defined + and for the ``sourceBlock`` to have a spatial grid defined. Additionally, + both the ``sourceBlock`` and ``driverFuelBlock`` must be instances of + ``HexBlocks``. """ runLog.info( "Converting representative block {} to its equivalent cylindrical model".format( From c27d47ec7cb885964671ac2fba67373265c3292c Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Wed, 24 Jan 2024 13:51:57 -0800 Subject: [PATCH 149/176] Adding implementation text to bookkeeping (#1611) --- armi/bookkeeping/db/database3.py | 43 +++++++++++++++++++++--- armi/bookkeeping/db/databaseInterface.py | 16 ++++++++- armi/bookkeeping/db/layout.py | 10 ++++++ armi/bookkeeping/historyTracker.py | 17 ++++++++-- armi/bookkeeping/snapshotInterface.py | 8 +++++ 5 files changed, 86 insertions(+), 8 deletions(-) diff --git a/armi/bookkeeping/db/database3.py b/armi/bookkeeping/db/database3.py index f732f9080..ee67f70dd 100644 --- a/armi/bookkeeping/db/database3.py +++ b/armi/bookkeeping/db/database3.py @@ -113,6 +113,12 @@ class Database3: :id: I_ARMI_DB_H51 :implements: R_ARMI_DB_H5 + This class implements a light wrapper around H5 files, so they can be used to + store ARMI outputs. H5 files are commonly used in scientific applications in + Fortran and C++. As such, they are entirely language agnostic binary files. The + implementation here is that ARMI wraps the ``h5py`` library, and uses its + extensive tooling, instead of re-inventing the wheel. + See Also -------- `doc/user/outputs/database` for more details. @@ -228,6 +234,13 @@ def writeSystemAttributes(h5db): .. impl:: Add system attributes to the database. :id: I_ARMI_DB_QA :implements: R_ARMI_DB_QA + + This method writes some basic system information to the H5 file. This is + designed as a starting point, so users can see information about the system + their simulations were run on. As ARMI is used on Windows and Linux, the + tooling here has to be platform independent. The two major sources of + information are the ARMI :py:mod:`context <armi.context>` module and the + Python standard library ``platform``. """ h5db.attrs["user"] = context.USER h5db.attrs["python"] = sys.version @@ -458,10 +471,20 @@ def writeInputsToDB(self, cs, csString=None, geomString=None, bpString=None): :id: I_ARMI_DB_CS :implements: R_ARMI_DB_CS + A ``Settings`` object is passed into this method, and then the settings are + converted into a YAML string stream. That stream is then written to the H5 + file. Optionally, this method can take a pre-build settings string to be + written directly to the file. + .. impl:: The reactor blueprints are saved the settings file. :id: I_ARMI_DB_BP :implements: R_ARMI_DB_BP + A ``Blueprints`` string is optionally passed into this method, and then + written to the H5 file. If it is not passed in, this method will attempt to + find the blueprints input file in the settings, and read the contents of + that file into a stream to be written to the H5 file. + Notes ----- This is hard-coded to read the entire file contents into memory and write that @@ -678,16 +701,25 @@ def load( ): """Load a new reactor from (cycle, node). - Case settings and blueprints can be provided by the client, or read from the database itself. - Providing these from the client could be useful when performing snapshot runs - or where it is expected to use results from a run using different settings and - continue with new settings (or if blueprints are not on the database). - Geometry is read from the database itself. + Case settings and blueprints can be provided by the client, or read from the + database itself. Providing these from the client could be useful when + performing snapshot runs or where it is expected to use results from a run + using different settings and continue with new settings (or if blueprints are + not on the database). Geometry is read from the database itself. .. impl:: Users can load a reactor from a DB. :id: I_ARMI_DB_R_LOAD :implements: R_ARMI_DB_R_LOAD + 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. + Parameters ---------- cycle : int @@ -754,6 +786,7 @@ def load( f"Due to the setting {CONF_SORT_REACTOR}, this Reactor is unsorted. " "But this feature is temporary and will be removed by 2024." ) + return root @staticmethod diff --git a/armi/bookkeeping/db/databaseInterface.py b/armi/bookkeeping/db/databaseInterface.py index 1f5c17dd0..5c90a6d03 100644 --- a/armi/bookkeeping/db/databaseInterface.py +++ b/armi/bookkeeping/db/databaseInterface.py @@ -226,10 +226,24 @@ def prepRestartRun(self): :id: I_ARMI_SNAPSHOT_RESTART :implements: R_ARMI_SNAPSHOT_RESTART + This method loads the state of a reactor from a particular point in time + from a standard ARMI + :py:class:`Database <armi.bookkeeping.db.database3.Database3>`. This is a + major use-case for having ARMI databases in the first case. And restarting + from such a database is easy, you just need to set a few settings:: + + * reloadDBName - Path to existing H5 file to reload from. + * startCycle - Operational cycle to restart from. + * startNode - Time node to start from. + Notes ----- Mixing the use of simple vs detailed cycles settings is allowed, provided that the cycle histories prior to `startCycle`/`startNode` are equivalent. + + ARMI expects the reload DB to have been made in the same version of ARMI as you + are running. ARMI does not gaurantee that a DB from a decade ago will be easily + used to restart a run. """ reloadDBName = self.cs["reloadDBName"] runLog.info( @@ -248,7 +262,6 @@ def prepRestartRun(self): self.cs, ) - # check that cycle histories are equivalent up to this point self._checkThatCyclesHistoriesAreEquivalentUpToRestartTime( loadDbCs, dbCycle, dbNode ) @@ -259,6 +272,7 @@ def prepRestartRun(self): def _checkThatCyclesHistoriesAreEquivalentUpToRestartTime( self, loadDbCs, dbCycle, dbNode ): + """Check that cycle histories are equivalent up to this point.""" dbStepLengths = getStepLengths(loadDbCs) currentCaseStepLengths = getStepLengths(self.cs) dbStepHistory = [] diff --git a/armi/bookkeeping/db/layout.py b/armi/bookkeeping/db/layout.py index c7c5c1c7c..2db3f5ea5 100644 --- a/armi/bookkeeping/db/layout.py +++ b/armi/bookkeeping/db/layout.py @@ -386,6 +386,16 @@ def writeToDB(self, h5group): .. impl:: Write data to the DB for a given time step. :id: I_ARMI_DB_TIME :implements: R_ARMI_DB_TIME + + This method writes a snapshot of the current state of the reactor to the + database. It takes a pointer to an existing HDF5 file as input, and it + writes the reactor data model to the file in depth-first search order. + Other than this search order, there are no guarantees as to what order the + 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. """ if "layout/type" in h5group: # It looks like we have already written the layout to DB, skip for now diff --git a/armi/bookkeeping/historyTracker.py b/armi/bookkeeping/historyTracker.py index ff9193990..bfd52d4bc 100644 --- a/armi/bookkeeping/historyTracker.py +++ b/armi/bookkeeping/historyTracker.py @@ -95,10 +95,22 @@ class HistoryTrackerInterface(interfaces.Interface): """ Makes reports of the state that individual assemblies encounter. - .. impl:: This interface allows users to retrieve run data from somewhere other than the database. + .. impl:: This interface allows users to retrieve run data from somewhere other + than the database. :id: I_ARMI_HIST_TRACK :implements: R_ARMI_HIST_TRACK + This is a special :py:class:`Interface <armi.interfaces.Interface>` that is + designed to store assembly and cross section data throughout time. This is done + directly, with time-based lists of assembly data, and dictionaries of cross- + section data. Users turn this feature on or off using the ``"detailAllAssems"`` + setting. + + Notes + ----- + This pre-dates the ARMI database system, and we would like to stop supporting this. + Please don't find new uses for this; use the databases. + Attributes ---------- detailAssemblyNames : list @@ -112,7 +124,8 @@ class HistoryTrackerInterface(interfaces.Interface): def __init__(self, r, cs): """ - HistoryTracker that uses the database to look up parameter history rather than storing them in memory. + HistoryTracker that uses the database to look up parameter history rather than + storing them in memory. Warning ------- diff --git a/armi/bookkeeping/snapshotInterface.py b/armi/bookkeeping/snapshotInterface.py index fa4462a16..087c0a6b9 100644 --- a/armi/bookkeeping/snapshotInterface.py +++ b/armi/bookkeeping/snapshotInterface.py @@ -48,6 +48,14 @@ class SnapshotInterface(interfaces.Interface): .. impl:: Save extra data to be saved from a run, at specified time nodes. :id: I_ARMI_SNAPSHOT0 :implements: R_ARMI_SNAPSHOT + + This is a special :py:class:`Interface <armi.interfaces.Interface>` that is + designed to run along all the other Interfaces during a simulation, to save off + important or helpful data. By default, this is designed to be used with the + ``"defaultSnapshots"`` and ``""dumpSnapshot""`` settings. These settings were + added so users can control if snapshot data will be recorded during their run. + Broadly, this class is implemented to run the Operator method + :py:meth:`o.snapshotRequest <armi.operators.Operator.snapshotRequest>`. """ name = "snapshot" From a3415afa50e1cb54c4ec3b2d47d7a5dd45eb7fe8 Mon Sep 17 00:00:00 2001 From: Chris Keckler <ckeckler@terrapower.com> Date: Wed, 24 Jan 2024 16:05:04 -0600 Subject: [PATCH 150/176] Add longer descriptions for `executers` impls (#1594) * Remove one useless impl tag and add details to impls for R_ARMI_EX * Update armi/physics/executers.py Co-authored-by: TianJingwd <157168608+TianJingwd@users.noreply.github.com> * Update armi/physics/executers.py Co-authored-by: TianJingwd <157168608+TianJingwd@users.noreply.github.com> * Move impl tag within docstring for proper formatting --------- Co-authored-by: TianJingwd <157168608+TianJingwd@users.noreply.github.com> --- armi/physics/executers.py | 41 +++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/armi/physics/executers.py b/armi/physics/executers.py index 15169c72e..d99c11c6d 100644 --- a/armi/physics/executers.py +++ b/armi/physics/executers.py @@ -33,6 +33,19 @@ class ExecutionOptions: :id: I_ARMI_EX0 :implements: R_ARMI_EX + Implements a basic container to hold and report options to be used in + the execution of an external code (see :need:`I_ARMI_EX1`). + Options are stored as instance attibutes and can be dumped as a string + using :py:meth:`~armi.physics.executers.ExecutionOptions.describe`, which + will include the name and value of all public attributes of the instance. + + Also facilitates the ability to execute parallel instances of a code by + providing the ability to resolve a ``runDir`` that is aware of the + executing MPI rank. This is done via :py:meth:`~armi.physics.executers.ExecutionOptions.setRunDirFromCaseTitle`, + where the user passes in a ``caseTitle`` string, which is hashed and combined + with the MPI rank to provide a unique directory name to be used by each parallel + instance. + Attributes ---------- inputFile : str @@ -127,10 +140,6 @@ class Executer: """ Short-lived object that coordinates a calculation step and updates a reactor. - .. impl:: Tool for executing external calculations. - :id: I_ARMI_EX1 - :implements: R_ARMI_EX - Notes ----- This is deliberately **not** a :py:class:`~mpiActions.MpiAction`. Thus, Executers can run as @@ -162,10 +171,6 @@ class DefaultExecuter(Executer): externally-executed physics codes. It is here for convenience but is not required. The sequence look like: - .. impl:: Default tool for executing external calculations. - :id: I_ARMI_EX2 - :implements: R_ARMI_EX - * Choose modeling options (either from the global run settings input or dictated programmatically) * Apply geometry transformations to the ARMI Reactor as needed * Build run-specific working directory @@ -178,6 +183,26 @@ class DefaultExecuter(Executer): * Clean up run directory * Un-apply geometry transformations as needed * Update ARMI data model as desired + + .. impl:: Default tool for executing external calculations. + :id: I_ARMI_EX1 + :implements: R_ARMI_EX + + Facilitates the execution of external calculations by accepting ``options`` (an + :py:class:`~armi.physics.executers.ExecutionOptions` object) and providing + methods that build run directories and execute a code based on the values in + ``options``. + + The :py:meth:`~armi.physics.executers.DefaultExecuter.run` method will first + resolve any derived options in the ``options`` object and check if the specified + ``executablePath`` option is valid, raising an error if not. If it is, + preparation work for executing the code is performed, such as performing any geometry + transformations specified in subclasses or building the directories needed + to save input and output files. Once the temporary working directory is created, + the executer moves into it and runs the external code, applying any results + from the run as specified in subclasses. + + Finally, any geometry perturbations that were performed are undone. """ def run(self): From 6b95994e520ec6cf4df6144b9c0e93288cf165bf Mon Sep 17 00:00:00 2001 From: Michael Jarrett <mjarrett@terrapower.com> Date: Wed, 24 Jan 2024 15:58:26 -0800 Subject: [PATCH 151/176] Implementation description for nucDirectory (#1607) Provide more detailed implementation descriptions for `impl` tags in nucDirectory files. Co-authored-by: Zachary Prince <zachmprince@gmail.com> --- armi/nucDirectory/elements.py | 27 ++++++++ armi/nucDirectory/nuclideBases.py | 102 ++++++++++++++++++++++++++++-- 2 files changed, 122 insertions(+), 7 deletions(-) diff --git a/armi/nucDirectory/elements.py b/armi/nucDirectory/elements.py index 0eee7642c..b1f84fbef 100644 --- a/armi/nucDirectory/elements.py +++ b/armi/nucDirectory/elements.py @@ -20,6 +20,20 @@ :id: I_ARMI_ND_ELEMENTS0 :implements: R_ARMI_ND_ELEMENTS + The :py:mod:`elements <armi.nucDirectory.elements>` module defines the + :py:class:`Element <armi.nucDirectory.elements.Element>` class which acts as + a data structure for organizing information about an individual element, + including number of protons, name, chemical symbol, phase (at STP), periodic + table group, standard weight, and a list of isotope :py:class:`nuclideBase + <armi.nucDirectory.nuclideBases.NuclideBase>` instances. The module includes + a factory that generates the :py:class:`Element + <armi.nucDirectory.elements.Element>` instances by reading from the + ``elements.dat`` file stored in the ARMI resources folder. When an + :py:class:`Element <armi.nucDirectory.elements.Element>` instance is + initialized, it is added to a set of global dictionaries that are keyed by + number of protons, element name, and element symbol. The module includes + several helper functions for querying these global dictionaries. + The element class structure is outlined :ref:`here <elements-class-diagram>`. .. _elements-class-diagram: @@ -157,6 +171,19 @@ def __init__(self, z, symbol, name, phase="UNKNOWN", group="UNKNOWN"): :id: I_ARMI_ND_ELEMENTS1 :implements: R_ARMI_ND_ELEMENTS + The :py:class:`Element <armi.nucDirectory.elements.Element>` class + acts as a data structure for organizing information about an + individual element, including number of protons, name, chemical + symbol, phase (at STP), periodic table group, standard weight, and a + list of isotope + :py:class:`nuclideBase <armi.nucDirectory.nuclideBases.NuclideBase>` + instances. + + The :py:class:`Element <armi.nucDirectory.elements.Element>` class + has a few methods for appending additional isotopes, checking + whether an isotope is naturally occurring, retrieving the natural + isotopic abundance, or whether the element is a heavy metal. + Parameters ---------- z : int diff --git a/armi/nucDirectory/nuclideBases.py b/armi/nucDirectory/nuclideBases.py index 5f83b7ed6..e1c30c34b 100644 --- a/armi/nucDirectory/nuclideBases.py +++ b/armi/nucDirectory/nuclideBases.py @@ -11,15 +11,49 @@ # 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. -""" -This module provides fundamental nuclide information to be used throughout the framework -and applications. +r""" +This module provides fundamental nuclide information to be used throughout the +framework and applications. .. impl:: Isotopes and isomers can be queried by name, label, MC2-3 ID, MCNP ID, and AAAZZZS ID. :id: I_ARMI_ND_ISOTOPES0 :implements: R_ARMI_ND_ISOTOPES -The nuclide class structure is outlined :ref:`here <nuclide-bases-class-diagram>`. + The :py:mod:`nuclideBases <armi.nucDirectory.nuclideBases>` module defines + the :py:class:`NuclideBase <armi.nucDirectory.nuclideBases.NuclideBase>` + class which is used to organize and store metadata about each nuclide. The + metadata is read from ``nuclides.dat`` file in the ARMI resources folder, + which contains metadata for 4,614 isotopes. The module also contains classes + for special types of nuclides, including :py:class:`DummyNuclideBase + <armi.nucDirectory.nuclideBases.DummyNuclideBase>` for dummy nuclides, + :py:class:`LumpNuclideBase + <armi.nucDirectory.nuclideBases.LumpNuclideBase>`, for lumped fission + product nuclides, and :py:class:`NaturalNuclideBase + <armi.nucDirectory.nuclideBases.NaturalNuclideBase>` for when data is given + collectively for an element at natural abundance rather than for individual + isotopes. + + The :py:class:`NuclideBase <armi.nucDirectory.nuclideBases.NuclideBase>` + provides a data structure for information about a single nuclide, including + the atom number, atomic weight, element, isomeric state, half-life, and + name. + + The :py:mod:`nuclideBases <armi.nucDirectory.nuclideBases>` module provides + a factory and associated functions for instantiating the + :py:class:`NuclideBase <armi.nucDirectory.nuclideBases.NuclideBase>` objects + and building the global nuclide dictionaries, including: + + * ``instances`` (list of nuclides) + * ``byName`` (keyed by name, e.g., ``U235``) + * ``byDBName`` (keyed by database name, e.g., ``nU235``) + * ``byLabel`` (keyed by label, e.g., ``U235``) + * ``byMcc2Id`` (keyed by MC\ :sup:`2`-2 ID, e.g., ``U-2355``) + * ``byMcc3Id`` (keyed by MC\ :sup:`2`-3 ID, e.g., ``U235_7``) + * ``byMcnpId`` (keyed by MCNP ID, e.g., ``92235``) + * ``byAAAZZZSId`` (keyed by AAAZZZS, e.g., ``2350920``) + +The nuclide class structure is outlined :ref:`here +<nuclide-bases-class-diagram>`. .. _nuclide-bases-class-diagram: @@ -497,11 +531,21 @@ def getAAAZZZSId(self): class NuclideBase(INuclide, IMcnpNuclide): - """Represents an individual nuclide/isotope. + r"""Represents an individual nuclide/isotope. .. impl:: Isotopes and isomers can be queried by name and label. :id: I_ARMI_ND_ISOTOPES1 :implements: R_ARMI_ND_ISOTOPES + + The :py:class:`NuclideBase <armi.nucDirectory.nuclideBases.NuclideBase>` + class provides a data structure for information about a single nuclide, + including the atom number, atomic weight, element, isomeric state, + half-life, and name. The class contains static methods for creating an + internal ARMI name or label for a nuclide. There are instance methods + for generating the nuclide ID for external codes, e.g. MCNP or Serpent, + and retrieving the nuclide ID for MC\ :sup:`2`-2 or MC\ :sup:`2`-3. + There are also instance methods for generating an AAAZZZS ID and an ENDF + MAT number. """ def __init__(self, element, a, weight, abundance, state, halflife): @@ -572,6 +616,11 @@ def getMcc2Id(self): .. impl:: Isotopes and isomers can be queried by MC2-2 ID. :id: I_ARMI_ND_ISOTOPES2 :implements: R_ARMI_ND_ISOTOPES + + This method returns the ``mcc2id`` attribute of a + :py:class:`NuclideBase <armi.nucDirectory.nuclideBases.NuclideBase>` + instance. This attribute is initially populated by reading from the + mcc-nuclides.yaml file in the ARMI resources folder. """ return self.mcc2id @@ -581,6 +630,11 @@ def getMcc3Id(self): .. impl:: Isotopes and isomers can be queried by MC2-3 ID. :id: I_ARMI_ND_ISOTOPES3 :implements: R_ARMI_ND_ISOTOPES + + This method returns the ``mcc3id`` attribute of a + :py:class:`NuclideBase <armi.nucDirectory.nuclideBases.NuclideBase>` + instance. This attribute is initially populated by reading from the + mcc-nuclides.yaml file in the ARMI resources folder. """ return self.mcc3id @@ -592,6 +646,12 @@ def getMcnpId(self): :id: I_ARMI_ND_ISOTOPES4 :implements: R_ARMI_ND_ISOTOPES + This method generates the MCNP ID for an isotope using the standard + MCNP format based on the atomic number A, number of protons Z, and + excited state. The implementation includes the special rule for + Am-242m, which is 95242. 95642 is used for the less common ground + state Am-242. + Returns ------- id : str @@ -602,7 +662,7 @@ def getMcnpId(self): if z == 95 and a == 242: # Am242 has special rules if self.state != 1: - # MCNP uses base state for the common metastable state AM242M , so AM242M is just 95242 + # MCNP uses base state for the common metastable state AM242M, so AM242M is just 95242 # AM242 base state is called 95642 (+400) in mcnp. # see https://mcnp.lanl.gov/pdf_files/la-ur-08-1999.pdf # New ACE-Formatted Neutron and Proton Libraries Based on ENDF/B-VII.0 @@ -621,6 +681,11 @@ def getAAAZZZSId(self): :id: I_ARMI_ND_ISOTOPES5 :implements: R_ARMI_ND_ISOTOPES + This method generates the AAAZZZS format ID for an isotope. Where + AAA is the mass number, ZZZ is the atomic number, and S is the + isomeric state. This is a general format independent of any code that + precisely defines an isotope or isomer. + Notes ----- An example would be for U235, where A=235, Z=92, and S=0, returning ``2350920``. @@ -1164,6 +1229,14 @@ def addNuclideBases(): .. impl:: Separating natural abundance data from code. :id: I_ARMI_ND_DATA0 :implements: R_ARMI_ND_DATA + + This function reads the `nuclides.dat` file from the ARMI resources + folder. This file contains metadata for 4,614 nuclides, including + number of protons, number of neutrons, atomic number, excited + state, element symbol, atomic mass, natural abundance, half-life, + and spontaneous fission yield. The data in `nuclides.dat` have been + collected from multiple different sources; the references are given + in comments at the top of that file. """ with open(os.path.join(context.RES, "nuclides.dat")) as f: for line in f: @@ -1220,11 +1293,20 @@ def __addLumpedFissionProductNuclideBases(): def readMCCNuclideData(): - """Read in the label data for the MC2-2 and MC2-3 cross section codes to the nuclide bases. + r"""Read in the label data for the MC2-2 and MC2-3 cross section codes to the nuclide bases. .. impl:: Separating MCC data from code. :id: I_ARMI_ND_DATA1 :implements: R_ARMI_ND_DATA + + This function reads the mcc-nuclides.yaml file from the ARMI resources + folder. This file contains the MC\ :sup:`2`-2 ID (from ENDF/B-V.2) and MC\ :sup:`2`-3 ID + (from ENDF/B-VII.0) for all nuclides in MC\ :sup:`2`. The ``mcc2id`` and + ``mcc3id`` attributes of each :py:class:`NuclideBase + <armi.nucDirectory.nuclideBases.NuclideBase>` instance are updated as + the data is read, and the global dictionaries ``byMcc2Id`` and + ``byMcc3Id`` are populated with the nuclide bases keyed by their + corresponding ID for each code. """ with open(os.path.join(context.RES, "mcc-nuclides.yaml"), "r") as f: yaml = YAML(typ="rt") @@ -1250,6 +1332,12 @@ def updateNuclideBasesForSpecialCases(): :id: I_ARMI_ND_ISOTOPES6 :implements: R_ARMI_ND_ISOTOPES + This function updates the keys for the :py:class:`NuclideBase + <armi.nucDirectory.nuclideBases.NuclideBase>` instances for Am-242m and + Am-242 in the ``byName`` and ``byDBName`` global dictionaries. This + function associates the more common isomer Am-242m with the name + "AM242", and uses "AM242G" to denote the ground state. + Notes ----- This function is specifically added to change the definition of From b944499c1dc1bb3514f12f21e16788e6755268eb Mon Sep 17 00:00:00 2001 From: Tony Alberti <aalberti@terrapower.com> Date: Wed, 24 Jan 2024 16:05:54 -0800 Subject: [PATCH 152/176] Add impl tag descriptions for uniformMesh.py (#1618) Provide more detailed implementation descriptions for uniformMesh.py. --------- Co-authored-by: Michael Jarrett <jarremic@umich.edu> --- armi/reactor/converters/uniformMesh.py | 41 ++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/armi/reactor/converters/uniformMesh.py b/armi/reactor/converters/uniformMesh.py index dac128566..260414239 100644 --- a/armi/reactor/converters/uniformMesh.py +++ b/armi/reactor/converters/uniformMesh.py @@ -123,15 +123,31 @@ def generateCommonMesh(self): :id: I_ARMI_UMC_NON_UNIFORM :implements: R_ARMI_UMC_NON_UNIFORM + A core-wide mesh is computed via ``_computeAverageAxialMesh`` which + operates by first collecting all the mesh points for every assembly + (``allMeshes``) and then averaging them together using + ``average1DWithinTolerance``. An attempt to preserve fuel and control + material boundaries is accomplished by moving fuel region boundaries + to accomodate control rod boundaries. Note this behavior only occurs + by calling ``_decuspAxialMesh`` which is dependent on ``minimumMeshSize`` + being defined (this is controlled by the ``uniformMeshMinimumSize`` setting). + .. impl:: Produce a mesh with a size no smaller than a user-specified value. :id: I_ARMI_UMC_MIN_MESH :implements: R_ARMI_UMC_MIN_MESH + If a minimum mesh size ``minimumMeshSize`` is provided, calls + ``_decuspAxialMesh`` on the core-wide mesh to maintain that minimum size + while still attempting to honor fuel and control material boundaries. Relies + ultimately on ``_filterMesh`` to remove mesh points that violate the minimum + size. Note that ``_filterMesh`` will always respect the minimum mesh size, + even if this means losing a mesh point that represents a fuel or control + material boundary. + Notes ----- Attempts to reduce the effect of fuel and control rod absorber smearing - ("cusping" effect) by keeping important material boundaries in the - common mesh. + ("cusping" effect) by keeping important material boundaries in the common mesh. """ self._computeAverageAxialMesh() if self.minimumMeshSize is not None: @@ -433,9 +449,24 @@ def convert(self, r=None): :id: I_ARMI_UMC :implements: R_ARMI_UMC + Given a source Reactor, ``r``, as input and when ``_hasNonUniformAssems`` is ``False``, + a new Reactor is created in ``initNewReactor``. This new Reactor contains copies of select + information from the input source Reactor (e.g., Operator, Blueprints, cycle, timeNode, etc). + The uniform mesh to be applied to the new Reactor is calculated in ``_generateUniformMesh`` + (see :need:`I_ARMI_UMC_NON_UNIFORM` and :need:`I_ARMI_UMC_MIN_MESH`). New assemblies with this + uniform mesh are created in ``_buildAllUniformAssemblies`` and added to the new Reactor. + Core-level parameters are then mapped from the source Reactor to the new Reactor in + ``_mapStateFromReactorToOther``. Finally, the core-wide axial mesh is updated on the new Reactor + via ``updateAxialMesh``. + + .. impl:: Map select parameters from composites on the original mesh to the new mesh. :id: I_ARMI_UMC_PARAM_FORWARD :implements: R_ARMI_UMC_PARAM_FORWARD + + In ``_mapStateFromReactorToOther``, Core-level parameters are mapped from the source Reactor + to the new Reactor. If requested, block-level parameters can be mapped using an averaging + equation as described in ``setAssemblyStateFromOverlaps``. """ if r is None: raise ValueError(f"No reactor provided in {self}") @@ -555,6 +586,12 @@ def applyStateToOriginal(self): .. impl:: Map select parameters from composites on the new mesh to the original mesh. :id: I_ARMI_UMC_PARAM_BACKWARD :implements: R_ARMI_UMC_PARAM_BACKWARD + + To ensure that the parameters on the original Reactor are from the converted Reactor, + the first step is to clear the Reactor-level parameters on the original Reactor + (see ``_clearStateOnReactor``). ``_mapStateFromReactorToOther`` is then called + to map Core-level parameters and, optionally, averaged Block-level parameters + (see :need:`I_ARMI_UMC_PARAM_FORWARD`). """ runLog.extra( f"Applying uniform neutronics results from {self.convReactor} to {self._sourceReactor}" From 998dd9ee8d396d6fb3e7a9df1b2804e4dd53148c Mon Sep 17 00:00:00 2001 From: TianJingwd <157168608+TianJingwd@users.noreply.github.com> Date: Wed, 24 Jan 2024 16:55:32 -0800 Subject: [PATCH 153/176] adding implementation details to geometryConverters (#1621) * adding implementation details to geometryConverters * black formatiing * black formatting (newer version) * review comments --- armi/reactor/converters/geometryConverters.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/armi/reactor/converters/geometryConverters.py b/armi/reactor/converters/geometryConverters.py index 18fac9e99..3b5d0e40d 100644 --- a/armi/reactor/converters/geometryConverters.py +++ b/armi/reactor/converters/geometryConverters.py @@ -434,6 +434,13 @@ def convert(self, r): :id: I_ARMI_CONV_3DHEX_TO_2DRZ :implements: R_ARMI_CONV_3DHEX_TO_2DRZ + This method converts the hex-z mesh to r-theta-z mesh. + It first verifies that the geometry type of the input reactor ``r`` + has the expected HEX geometry. Upon conversion, it determines the inner + and outer diameters of each ring in the r-theta-z mesh and calls + ``_createRadialThetaZone`` to create a radial theta zone with a homogenized mixture. + The axial dimension of the r-theta-z mesh is then updated by ``updateAxialMesh``. + Attributes ---------- r : Reactor object @@ -1255,6 +1262,19 @@ def convert(self, r): :id: I_ARMI_THIRD_TO_FULL_CORE0 :implements: R_ARMI_THIRD_TO_FULL_CORE + This method first checks if the input reactor is already full core. + If full-core symmetry is detected, the input reactor is returned. + If not, it then verifies that the input reactor has the expected one-third + core symmetry and HEX geometry. + + Upon conversion, it loops over the assembly vector of the source + one-third core model, copies and rotates each source assembly to create + new assemblies, and adds them on the full-core grid. For the center assembly, + it modifies its parameters. + + Finally, it sets the domain type to full core. + + Parameters ---------- sourceReactor : Reactor object @@ -1346,6 +1366,11 @@ def restorePreviousGeometry(self, r=None): .. impl:: Restore a one-third-core geometry to a full-core geometry. :id: I_ARMI_THIRD_TO_FULL_CORE1 :implements: R_ARMI_THIRD_TO_FULL_CORE + + This method is a reverse process of the method ``convert``. It converts + the full-core reactor model back to the original one-third core reactor model by removing + the added assemblies and changing the parameters of the center + assembly from full core to one third core. """ r = r or self._sourceReactor @@ -1388,6 +1413,11 @@ def addEdgeAssemblies(self, core): :id: I_ARMI_ADD_EDGE_ASSEMS0 :implements: R_ARMI_ADD_EDGE_ASSEMS + Edge assemblies on the 120-degree symmetric line of a one-third core reactor model are added + because they are needed for DIF3D-finite difference or MCNP models. This is done + by copying the assemblies from the lower boundary and placing them in their + reflective positions on the upper boundary of the symmetry line. + Parameters ---------- reactor : Reactor @@ -1458,6 +1488,10 @@ def removeEdgeAssemblies(self, core): :id: I_ARMI_ADD_EDGE_ASSEMS1 :implements: R_ARMI_ADD_EDGE_ASSEMS + This method is the reverse process of the method ``addEdgeAssemblies``. It is + needed for the DIF3D-Nodal calculation. It removes the assemblies on the 120-degree + symmetry line. + See Also -------- addEdgeAssemblies : adds the edge assemblies From 31aa0206dfd92b3c1a8bd1be37646213ecd998d3 Mon Sep 17 00:00:00 2001 From: Michael Jarrett <mjarrett@terrapower.com> Date: Wed, 24 Jan 2024 17:25:35 -0800 Subject: [PATCH 154/176] Fix getNeighboringCellIndices (#1614) --- armi/reactor/grids/structuredgrid.py | 2 +- armi/reactor/tests/test_reactors.py | 27 +++++++++++++++++++++++++++ doc/release/0.3.rst | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/armi/reactor/grids/structuredgrid.py b/armi/reactor/grids/structuredgrid.py index 91b569696..420a83f90 100644 --- a/armi/reactor/grids/structuredgrid.py +++ b/armi/reactor/grids/structuredgrid.py @@ -379,7 +379,7 @@ def _meshBaseByBounds(index, bounds): @staticmethod def getNeighboringCellIndices(i, j=0, k=0): """Return the indices of the immediate neighbors of a mesh point in the plane.""" - return ((i + 1, j, k), (1, j + 1, k), (i - 1, j, k), (i, j - 1, k)) + return ((i + 1, j, k), (i, j + 1, k), (i - 1, j, k), (i, j - 1, k)) @staticmethod def getAboveAndBelowCellIndices(indices): diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 085525bfd..2052a8b1b 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -1376,3 +1376,30 @@ def test_getNuclideCategoriesLogging(self): self.assertIn("Nuclide categorization", messages) self.assertIn("Structure", messages) + + +class CartesianReactorNeighborTests(ReactorTests): + def setUp(self): + self.r = loadTestReactor(TEST_ROOT, inputFileName="zpprTest.yaml")[1] + + def test_findNeighborsCartesian(self): + """Find neighbors of a given assembly in a Cartesian grid.""" + loc = self.r.core.spatialGrid[1, 1, 0] + a = self.r.core.childrenByLocator[loc] + neighbs = self.r.core.findNeighbors(a) + locs = [tuple(a.spatialLocator.indices[:2]) for a in neighbs] + self.assertEqual(len(neighbs), 4) + self.assertIn((2, 1), locs) + self.assertIn((1, 2), locs) + self.assertIn((0, 1), locs) + self.assertIn((1, 0), locs) + + # try with edge assembly + loc = self.r.core.spatialGrid[0, 0, 0] + a = self.r.core.childrenByLocator[loc] + neighbs = self.r.core.findNeighbors(a, showBlanks=False) + locs = [tuple(a.spatialLocator.indices[:2]) for a in neighbs] + self.assertEqual(len(neighbs), 2) + # in this case no locations that aren't actually in the core should be returned + self.assertIn((1, 0), locs) + self.assertIn((0, 1), locs) diff --git a/doc/release/0.3.rst b/doc/release/0.3.rst index 2f5ff1b45..d974809c6 100644 --- a/doc/release/0.3.rst +++ b/doc/release/0.3.rst @@ -12,7 +12,7 @@ What's new in ARMI? Bug Fixes --------- -#. TBD +#. ``StructuredGrid.getNeighboringCellIndices()`` was incorrectly implemented for the second neighbor. (`PR#1614 <https://github.com/terrapower/armi/pull/1614>`_) Changes that Affect Requirements -------------------------------- From c7d02f98c29e2983dc5e6c471f72a35d675ca232 Mon Sep 17 00:00:00 2001 From: Arrielle Opotowsky <c-aopotowsky@terrapower.com> Date: Wed, 24 Jan 2024 19:26:13 -0600 Subject: [PATCH 155/176] FluxFiles Deserve Hashes Too! (#1613) --- armi/bookkeeping/report/reportingUtils.py | 23 ++++++++++++++------ armi/bookkeeping/report/tests/test_report.py | 1 + 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/armi/bookkeeping/report/reportingUtils.py b/armi/bookkeeping/report/reportingUtils.py index 5a37f69e3..864519282 100644 --- a/armi/bookkeeping/report/reportingUtils.py +++ b/armi/bookkeeping/report/reportingUtils.py @@ -138,14 +138,23 @@ def _listInputFiles(cs): inputInfo.append((label, fName, shaHash)) # bonus: grab the files stored in the crossSectionControl section - for fluxSection, fluxData in cs["crossSectionControl"].items(): - if fluxData.xsFileLocation is not None: - label = f"crossSectionControl-{fluxSection}" - fName = fluxData.xsFileLocation - if isinstance(fName, list): - fName = fName[0] + for xsID, xsSetting in cs["crossSectionControl"].items(): + fNames = [] + # Users shouldn't ever have both of these defined, but this is not the place + # for code to fail if they do. Allow for both to not be None. + if xsSetting.xsFileLocation is not None: + # possibly a list of files + if isinstance(xsSetting.xsFileLocation, list): + fNames.extend(xsSetting.xsFileLocation) + else: + fNames.append(xsSetting.xsFileLocation) + if xsSetting.fluxFileLocation is not None: + # single file + fNames.append(xsSetting.fluxFileLocation) + for fName in fNames: + label = f"crossSectionControl-{xsID}" if fName and os.path.exists(fName): - shaHash = getFileSHA1Hash(fName, digits=10) + shaHash = getFileSHA1Hash(os.path.abspath(fName), digits=10) inputInfo.append((label, fName, shaHash)) return inputInfo diff --git a/armi/bookkeeping/report/tests/test_report.py b/armi/bookkeeping/report/tests/test_report.py index 73f0f5a5f..648d1bde3 100644 --- a/armi/bookkeeping/report/tests/test_report.py +++ b/armi/bookkeeping/report/tests/test_report.py @@ -142,6 +142,7 @@ def test_writeWelcomeHeaders(self): # pass that random file into the settings o.cs["crossSectionControl"]["DA"].xsFileLocation = randoFile + o.cs["crossSectionControl"]["DA"].fluxFileLocation = randoFile with mockRunLogs.BufferLog() as mock: # we should start with a clean slate From 3d49274d8334b166e3a923770b4f4991333f6d69 Mon Sep 17 00:00:00 2001 From: Zachary Prince <zachmprince@gmail.com> Date: Thu, 25 Jan 2024 09:06:58 -0700 Subject: [PATCH 156/176] Fixing `aligned` equations in impl content since they don't render properly with needextract (#1620) --- armi/physics/neutronics/globalFlux/globalFluxInterface.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/armi/physics/neutronics/globalFlux/globalFluxInterface.py b/armi/physics/neutronics/globalFlux/globalFluxInterface.py index b34acbfc8..21fbd1a5a 100644 --- a/armi/physics/neutronics/globalFlux/globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/globalFluxInterface.py @@ -1248,11 +1248,14 @@ def computeDpaRate(mgFlux, dpaXs): Displacements calculated by displacement XS: .. math:: + :nowrap: + \begin{aligned} \text{Displacement rate} &= \phi N_{\text{HT9}} \sigma \\ &= (\#/\text{cm}^2/s) \cdot (1/cm^3) \cdot (\text{barn})\\ &= (\#/\text{cm}^5/s) \cdot \text{(barn)} * 10^{-24} \text{cm}^2/\text{barn} \\ &= \#/\text{cm}^3/s + \end{aligned} :: From c2af60495072d938015b9922695a974150901992 Mon Sep 17 00:00:00 2001 From: Arrielle Opotowsky <c-aopotowsky@terrapower.com> Date: Thu, 25 Jan 2024 11:14:54 -0600 Subject: [PATCH 157/176] Formatting fixes to the RST docs (#1622) --- armi/nucDirectory/nuclideBases.py | 4 +-- armi/physics/fuelCycle/fuelHandlers.py | 2 +- .../neutronics/crossSectionGroupManager.py | 2 +- armi/reactor/components/component.py | 6 ++-- armi/reactor/reactors.py | 28 +++++++++---------- armi/runLog.py | 4 +-- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/armi/nucDirectory/nuclideBases.py b/armi/nucDirectory/nuclideBases.py index e1c30c34b..17e8e9c2c 100644 --- a/armi/nucDirectory/nuclideBases.py +++ b/armi/nucDirectory/nuclideBases.py @@ -1230,11 +1230,11 @@ def addNuclideBases(): :id: I_ARMI_ND_DATA0 :implements: R_ARMI_ND_DATA - This function reads the `nuclides.dat` file from the ARMI resources + This function reads the ``nuclides.dat`` file from the ARMI resources folder. This file contains metadata for 4,614 nuclides, including number of protons, number of neutrons, atomic number, excited state, element symbol, atomic mass, natural abundance, half-life, - and spontaneous fission yield. The data in `nuclides.dat` have been + and spontaneous fission yield. The data in ``nuclides.dat`` have been collected from multiple different sources; the references are given in comments at the top of that file. """ diff --git a/armi/physics/fuelCycle/fuelHandlers.py b/armi/physics/fuelCycle/fuelHandlers.py index 12b98da69..0c271bf8f 100644 --- a/armi/physics/fuelCycle/fuelHandlers.py +++ b/armi/physics/fuelCycle/fuelHandlers.py @@ -745,7 +745,7 @@ def swapAssemblies(self, a1, a2): 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`` + 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 diff --git a/armi/physics/neutronics/crossSectionGroupManager.py b/armi/physics/neutronics/crossSectionGroupManager.py index bdad6a2f9..5b83c40ba 100644 --- a/armi/physics/neutronics/crossSectionGroupManager.py +++ b/armi/physics/neutronics/crossSectionGroupManager.py @@ -324,7 +324,7 @@ class AverageBlockCollection(BlockCollection): volume-weighted average. Inheriting functionality from the abstract :py:class:`Reactor <armi.physics.neutronics.crossSectionGroupManager.BlockCollection>` object, this class will construct representative blocks using averaged parameters of all blocks in the given collection. - Number density averages can be computed at a component level through `self._performAverageByComponent`, + Number density averages can be computed at a component level through ``self._performAverageByComponent``, or at a block level by default. Average nuclide temperatures and burnup are also included when constructing a representative block. """ diff --git a/armi/reactor/components/component.py b/armi/reactor/components/component.py index 337259cd5..bfdccd42b 100644 --- a/armi/reactor/components/component.py +++ b/armi/reactor/components/component.py @@ -187,8 +187,8 @@ class Component(composites.Composite, metaclass=ComponentType): :implements: R_ARMI_COMP_ORDER Determining Component order by outermost diameters is implemented via - the __lt__() method, which is used to control sort() as the - standard approach in Python. However, __lt__() does not show up in the API. + the ``__lt__()`` method, which is used to control ``sort()`` as the + standard approach in Python. However, ``__lt__()`` does not show up in the API. Attributes ---------- @@ -977,7 +977,7 @@ def getThermalExpansionFactor(self, Tc=None, T0=None): This method enables the calculation of the thermal expansion factor for a given material. If the material is solid, the difference - between T0 and Tc is used to calculate the thermal expansion + between ``T0`` and ``Tc`` is used to calculate the thermal expansion factor. If a solid material does not have a linear expansion factor defined and the temperature difference is greater than :py:attr:`armi.reactor.components.component.Component._TOLERANCE`, an diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index 8fb8f4396..c96c97109 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -261,9 +261,9 @@ class Core(composites.Composite): A :py:class:`Core <armi.reactor.reactors.Core>` object is typically a child of a :py:class:`Reactor <armi.reactor.reactors.Reactor>` object. A Reactor can contain multiple objects of the Core type. The instance - attribute name `r.core` is reserved for the object representating the + attribute name ``r.core`` is reserved for the object representating the active core. A reactor may also have a spent fuel pool instance - attribute, `r.sfp`, which is also of type + attribute, ``r.sfp``, which is also of type :py:class:`core <armi.reactor.reactors.Core>`. Most of the operations to retrieve information from the ARMI reactor @@ -830,7 +830,7 @@ def getNumRings(self, indexBased=False): :implements: R_ARMI_R_NUM_RINGS This method determines the number of rings in the reactor. If the - setting `circularRingMode` is enabled (by default it is false), the + setting ``circularRingMode`` is enabled (by default it is false), the assemblies will be grouped into roughly circular rings based on their positions and the number of circular rings is reteurned. Otherwise, the number of hex rings is returned. This parameter is @@ -1196,8 +1196,8 @@ def getAssemblyByName(self, name): This method returns the :py:class:`assembly <armi.reactor.core.assemblies.Assembly>` with a name matching the - value provided as an input parameter to this function. The `name` of - an assembly is based on the `assemNum` parameter. + value provided as an input parameter to this function. The ``name`` of + an assembly is based on the ``assemNum`` parameter. Parameters ---------- @@ -1719,10 +1719,10 @@ def getAssemblyWithStringLocation(self, locationString): This method returns the :py:class:`assembly <armi.reactor.core.assemblies.Assembly>` located in the requested location. The location is provided to this method as an input - parameter in a string with the format "001-001". For a `HexGrid + parameter in a string with the format "001-001". For a :py:class:`HexGrid <armi.reactor.grids.hexagonal.HexGrid>`, the first number indicates the hexagonal ring and the second number indicates the position - within that ring. For a `CartesianGrid + within that ring. For a :py:class:`CartesianGrid <armi.reactor.grids.cartesian.CartesianGrid>`, the first number represents the x index and the second number represents the y index. If there is no assembly in the grid at the requested location, this @@ -1775,15 +1775,15 @@ def findNeighbors( :py:meth:`getNeighboringCellIndices <armi.reactor.grids.StructuredGrid.getNeighboringCellIndices>`. For a hexagonal grid, the (i, j) indices are converted to (ring, - position) indexing using the core.spatialGrid instance attribute. + position) indexing using the ``core.spatialGrid`` instance attribute. - The `showBlanks` option determines whether non-existing assemblies - will be indicated with a `None` in the list or just excluded from + The ``showBlanks`` option determines whether non-existing assemblies + will be indicated with a ``None`` in the list or just excluded from the list altogether. - The `duplicateAssembliesOnReflectiveBoundary` setting only works for + The ``duplicateAssembliesOnReflectiveBoundary`` setting only works for 1/3 core symmetry with periodic boundary conditions. For these types - of geometries, if this setting is `True`, neighbor lists for + of geometries, if this setting is ``True``, neighbor lists for assemblies along a periodic boundary will include the assemblies along the opposite periodic boundary that are effectively neighbors. @@ -2046,9 +2046,9 @@ def findAllMeshPoints(self, assems=None, applySubMesh=True): :implements: R_ARMI_R_MESH This method iterates through all of the assemblies provided, or all - assemblies in the core if no list of `assems` is provided, and + assemblies in the core if no list of ``assems`` is provided, and constructs a tuple of three lists which contain the unique i, j, and - k mesh coordinates, respectively. The `applySubMesh` setting + k mesh coordinates, respectively. The ``applySubMesh`` setting controls whether the mesh will include the submesh coordinates. For a standard assembly-based reactor geometry with a hexagonal or Cartesian assembly grid, this method is only used to produce axial diff --git a/armi/runLog.py b/armi/runLog.py index 8a3204ccf..dcf0e1ff6 100644 --- a/armi/runLog.py +++ b/armi/runLog.py @@ -545,8 +545,8 @@ class RunLogger(logging.Logger): At any place in the ARMI application, developers can interject a plain text logging message, and when that code is hit during an ARMI simulation, the text will be piped to screen and a log file. By default, the ``logging`` module only - logs to screen, but ARMI adds a ``FileHandler` in the ``RunLog`` constructor - and in py:meth:`armi.runLog._RunLog.startLog`. + logs to screen, but ARMI adds a ``FileHandler`` in the ``RunLog`` constructor + and in ``_RunLog.startLog``. """ FMT = "%(levelname)s%(message)s" From 17a2afead979899314525ccf64445fc03ff4d349 Mon Sep 17 00:00:00 2001 From: Zachary Prince <zachmprince@gmail.com> Date: Thu, 25 Jan 2024 10:41:20 -0700 Subject: [PATCH 158/176] Details for `nuclearDataIO` impl tags (#1605) * Editorial updates * Add implementation details for the DIF3D file reader * Trivial modifications to dif3d reader impl * Adding details to rest of nuclearDataIO impls * Trying to write a generalized implementation tag for CCCC * Finishing CCCC impl, thanks @mgjarret! * Addressing reviewer comments * Reviewer comments round 2 * Last reviewer comment --------- Co-authored-by: Virinder Sandhu <vsandhu@terrapower.com> --- armi/nuclearDataIO/cccc/cccc.py | 75 +++++++++++++++++++++++++++-- armi/nuclearDataIO/cccc/dif3d.py | 35 +++++++++++++- armi/nuclearDataIO/cccc/dlayxs.py | 34 ++++++++++++- armi/nuclearDataIO/cccc/gamiso.py | 7 +++ armi/nuclearDataIO/cccc/geodst.py | 39 +++++++++++++-- armi/nuclearDataIO/cccc/isotxs.py | 47 +++++++++++++++++- armi/nuclearDataIO/cccc/pmatrx.py | 19 ++++++++ armi/nuclearDataIO/xsCollections.py | 36 ++++++++++++-- 8 files changed, 275 insertions(+), 17 deletions(-) diff --git a/armi/nuclearDataIO/cccc/cccc.py b/armi/nuclearDataIO/cccc/cccc.py index 0f97ae39f..7820a9610 100644 --- a/armi/nuclearDataIO/cccc/cccc.py +++ b/armi/nuclearDataIO/cccc/cccc.py @@ -13,8 +13,69 @@ # limitations under the License. """ -Defines containers for the reading and writing standard interface files +Defines containers for the reading and writing standard interface files for reactor physics codes. + +.. impl:: Generic tool for reading and writing Committee on Computer Code Coordination (CCCC) format files for reactor physics codes + :id: I_ARMI_NUCDATA + :implements: R_ARMI_NUCDATA_ISOTXS, + R_ARMI_NUCDATA_GAMISO, + R_ARMI_NUCDATA_GEODST, + R_ARMI_NUCDATA_DIF3D, + R_ARMI_NUCDATA_PMATRX, + R_ARMI_NUCDATA_DLAYXS + + This module provides a number of base classes that implement general + capabilities for binary and ASCII file I/O. The :py:class:`IORecord` serves + as an abstract base class that instantiates a number of methods that the + binary and ASCII children classes are meant to implement. These methods, + prefixed with ``rw``, are meant to convert literal data types, e.g. float or + int, to either binary or ASCII. This base class does its own conversion for + container data types, e.g. list or matrix, relying on the child + implementation of the literal types that the container possesses. The binary + conversion is implemented in :py:class:`BinaryRecordReader` and + :py:class`BinaryRecordWriter`. The ASCII conversion is implemented in + :py:class:`AsciiRecordReader` and :py:class:`AsciiRecordWriter`. + + These :py:class`IORecord` classes are used within :py:class:`Stream` objects + for the data conversion. :py:class:`Stream` is a context manager that opens + a file for reading or writing on the ``__enter__`` and closes that file upon + ``__exit__``. :py:class:`Stream` is an abstract base class that is + subclassed for each CCCC file. It is subclassed directly for the CCCC files + that contain XS data: + + * :py:class:`ISOTXS <armi.nuclearDataIO.cccc.isotxs.IsotxsIO>`, + * :py:mod:`GAMISO <armi.nuclearDataIO.cccc.gamiso>`, + * :py:class:`PMATRX <armi.nuclearDataIO.cccc.pmatrx.PmatrxIO>`. + * :py:class:`DLAYXS <armi.nuclearDataIO.cccc.dlayxs.DlayxsIO>`. + * :py:mod:`COMPXS <armi.nuclearDataIO.cccc.compxs>`. + + For the CCCC file types that are outputs from a flux solver such as DIF3D + (e.g., GEODST, DIF3D, NHFLUX) the streams are subclassed from + :py:class:`StreamWithDataContainer`, which is a special abstract subclass of + :py:class:`Stream` that implements a common pattern used for these file + types. In a :py:class:`StreamWithDataContainer`, the data is directly read + to or written from a specialized data container. + + The data container structure for each type of CCCC file is implemented in + the module for that file, as a subclass of :py:class:`DataContainer`. The + subclasses for each CCCC file type define standard attribute names for the + data that will be read from or written to the CCCC file. CCCC file types + that follow this pattern include: + + * :py:class:`GEODST <armi.nuclearDataIO.cccc.geodst.GeodstData>` + * :py:class:`DIF3D <armi.nuclearDataIO.cccc.dif3d.Dif3dData>` + * :py:class:`NHFLUX <armi.nuclearDataIO.cccc.nhflux.NHFLUX>` + (and multiple sub-classes thereof) + * :py:class:`LABELS <armi.nuclearDataIO.cccc.labels.LabelsData>` + * :py:class:`PWDINT <armi.nuclearDataIO.cccc.pwdint.PwdintData>` + * :py:class:`RTFLUX <armi.nuclearDataIO.cccc.rtflux.RtfluxData>` + * :py:class:`RZFLUX <armi.nuclearDataIO.cccc.rzflux.RzfluxData>` + * :py:class:`RTFLUX <armi.nuclearDataIO.cccc.rtflux.RtfluxData>` + + The logic to parse or write each specific file format is contained within + the :py:meth:`Stream.readWrite` implementations of the respective + subclasses. """ import io import itertools @@ -274,7 +335,8 @@ def rwImplicitlyTypedMap(self, keys: List[str], contents) -> dict: class BinaryRecordReader(IORecord): - """Writes a single CCCC record in binary format. + """ + Writes a single CCCC record in binary format. Notes ----- @@ -345,7 +407,8 @@ def rwString(self, val, length): class BinaryRecordWriter(IORecord): - r"""a single record from a CCCC file. + r""" + Reads a single CCCC record in binary format. Reads binary information sequentially. """ @@ -406,7 +469,8 @@ def rwString(self, val, length): class AsciiRecordReader(BinaryRecordReader): - """Reads a single CCCC record in ASCII format. + """ + Reads a single CCCC record in ASCII format. See Also -------- @@ -441,7 +505,8 @@ def rwString(self, val, length): class AsciiRecordWriter(IORecord): - r"""Writes a single CCCC record in ASCII format. + r""" + Writes a single CCCC record in ASCII format. Since there is no specific format of an ASCII CCCC record, the format is roughly the same as the :py:class:`BinaryRecordWriter`, except that the :class:`AsciiRecordReader` puts a space in diff --git a/armi/nuclearDataIO/cccc/dif3d.py b/armi/nuclearDataIO/cccc/dif3d.py index 81848e28a..e5d8443ee 100644 --- a/armi/nuclearDataIO/cccc/dif3d.py +++ b/armi/nuclearDataIO/cccc/dif3d.py @@ -199,11 +199,44 @@ def _rw5DRecord(self) -> None: ) def readWrite(self): - """Reads or writes metadata and data from 5 records. + """Reads or writes metadata and data from the five records of the DIF3D binary file. .. impl:: Tool to read and write DIF3D files. :id: I_ARMI_NUCDATA_DIF3D :implements: R_ARMI_NUCDATA_DIF3D + + The reading and writing of the DIF3D binary file is performed using + :py:class:`StreamWithDataContainer <.cccc.StreamWithDataContainer>` + from the :py:mod:`~armi.nuclearDataIO.cccc` package. This class + allows for the reading and writing of CCCC binary files, processing + one record at a time using subclasses of the :py:class:`IORecord + <.cccc.IORecord>`. Each record in a CCCC binary file consists of + words that represent integers (short or long), floating-point + numbers (single or double precision), or strings of data. One or + more of these words are parsed one at a time by the reader. Multiple + words processed together have meaning, such as such as groupwise + overrelaxation factors. While reading, the data is stored in a + Python dictionary as an attribute on the object, one for each + record. The keys in each dictionary represent the parsed grouping of + words in the records; for example, for the 4D record (stored as the + attribute ``fourD``), each groupwise overrelaxation factor is stored + as the key ``OMEGA{i}``, where ``i`` is the group number. See + :need:`I_ARMI_NUCDATA` for more details on the general + implementation. + + Each record is also embedded with the record size at the beginning + and end of the record (always assumed to be present), which is used + for error checking at the end of processing each record. + + The DIF3D reader processes the file identification record (stored as + the attribute ``_metadata``) and the five data records for the DIF3D + file, as defined in the specification for the file distributed with + the DIF3D software. + + This class can also read and write an ASCII version of the DIF3D + file. While this format is not used by the DIF3D software, it can be + a useful representation for users to access the file in a + human-readable format. """ msg = f"{'Reading' if 'r' in self._fileMode else 'Writing'} DIF3D binary data {self}" runLog.info(msg) diff --git a/armi/nuclearDataIO/cccc/dlayxs.py b/armi/nuclearDataIO/cccc/dlayxs.py index 51736b851..f62cc3750 100644 --- a/armi/nuclearDataIO/cccc/dlayxs.py +++ b/armi/nuclearDataIO/cccc/dlayxs.py @@ -224,11 +224,43 @@ def __init__(self, fileName, fileMode, dlayxs): self.metadata = dlayxs.metadata def readWrite(self): - """Read and write DLAYXS files. + r"""Read and write DLAYXS files. .. impl:: Tool to read and write DLAYXS files. :id: I_ARMI_NUCDATA_DLAYXS :implements: R_ARMI_NUCDATA_DLAYXS + + Reading and writing DLAYXS delayed neutron data files is performed + using the general nuclear data I/O functionalities described in + :need:`I_ARMI_NUCDATA`. Reading/writing a DLAYXS file is performed + through the following steps: + + #. Read/write the data ``label`` for identification. + + .. note:: + + MC\ :sup:`2`-3 file does not use the expected number of + characters for the ``label``, so its length needs to be + stored in the :py:class:`~.cccc.IORecord`. + + #. Read/write file control information, i.e. the 1D record, which includes: + + * Number of energy groups + * Number of nuclides + * Number of precursor families + + #. Read/write spectral data, including: + + * Nuclide IDs + * Decay constants + * Emission spectra + * Energy group bounds + * Number of families to which fission in a given nuclide + contributes delayed neutron precursors + + #. Read/write 3D delayed neutron yield matrix on the 3D record, + indexed by nuclide, precursor family, and outgoing neutron energy + group. """ runLog.info( "{} DLAYXS library {}".format( diff --git a/armi/nuclearDataIO/cccc/gamiso.py b/armi/nuclearDataIO/cccc/gamiso.py index b53eb0827..af5e7f493 100644 --- a/armi/nuclearDataIO/cccc/gamiso.py +++ b/armi/nuclearDataIO/cccc/gamiso.py @@ -22,6 +22,13 @@ :id: I_ARMI_NUCDATA_GAMISO :implements: R_ARMI_NUCDATA_GAMISO + The majority of the functionality in this module is inherited from the + :py:mod:`~armi.nuclearDataIO.cccc.isotxs` module. See + :py:class:`~armi.nuclearDataIO.cccc.isotxs.IsotxsIO` and its associated + implementation :need:`I_ARMI_NUCDATA_ISOTXS` for more information. The only + difference from ISOTXS neutron data is a special treatment for gamma + velocities, which is done by overriding ``_rwLibraryEnergies``. + See [GAMSOR]_. .. [GAMSOR] Smith, M. A., Lee, C. H., and Hill, R. N. GAMSOR: Gamma Source Preparation and DIF3D Flux Solution. United States: diff --git a/armi/nuclearDataIO/cccc/geodst.py b/armi/nuclearDataIO/cccc/geodst.py index 00122cad2..2207f48e3 100644 --- a/armi/nuclearDataIO/cccc/geodst.py +++ b/armi/nuclearDataIO/cccc/geodst.py @@ -136,6 +136,39 @@ def readWrite(self): .. impl:: Tool to read and write GEODST files. :id: I_ARMI_NUCDATA_GEODST :implements: R_ARMI_NUCDATA_GEODST + + Reading and writing GEODST files is performed using the general + nuclear data I/O functionalities described in + :need:`I_ARMI_NUCDATA`. Reading/writing a GEODST file is performed + through the following steps: + + #. Read/write file ID record + + #. Read/write file specifications on 1D record. + + #. Based on the geometry type (``IGOM``), one of following records + are read/written: + + * Slab (1), cylinder (3), or sphere (3): Read/write 1-D coarse + mesh boundaries and fine mesh intervals. + * X-Y (6), R-Z (7), Theta-R (8), uniform triangular (9), + hexagonal (10), or R-Theta (11): Read/write 2-D coarse mesh + boundaries and fine mesh intervals. + * R-Theta-Z (12, 15), R-Theta-Alpha (13, 16), X-Y-Z (14), + uniform triangular-Z (17), hexagonal-Z(18): Read/write 3-D + coarse mesh boundaries and fine mesh intervals. + + #. If the geometry is not zero-dimensional (``IGOM`` > 0) and + buckling values are specified (``NBS`` > 0): Read/write geometry + data from 5D record. + + #. If the geometry is not zero-dimensional (``IGOM`` > 0) and region + assignments are coarse-mesh-based (``NRASS`` = 0): Read/write + region assignments to coarse mesh interval. + + #. If the geometry is not zero-dimensional (``IGOM`` > 0) and region + assignments are fine-mesh-based (``NRASS`` = 1): Read/write + region assignments to fine mesh interval. """ self._rwFileID() self._rw1DRecord() @@ -162,8 +195,7 @@ def _rwFileID(self): Notes ----- - The username, version, etc are embedded in this string but it's - usually blank. The number 28 was actually obtained from + The number 28 was actually obtained from a hex editor and may be code specific. """ with self.createRecord() as record: @@ -182,7 +214,6 @@ def _rw1DRecord(self): def _rw2DRecord(self): """Read/write 1-D coarse mesh boundaries and fine mesh intervals.""" with self.createRecord() as record: - self._data.xmesh = record.rwList( self._data.xmesh, "double", self._metadata["NCINTI"] + 1 ) @@ -193,7 +224,6 @@ def _rw2DRecord(self): def _rw3DRecord(self): """Read/write 2-D coarse mesh boundaries and fine mesh intervals.""" with self.createRecord() as record: - self._data.xmesh = record.rwList( self._data.xmesh, "double", self._metadata["NCINTI"] + 1 ) @@ -210,7 +240,6 @@ def _rw3DRecord(self): def _rw4DRecord(self): """Read/write 3-D coarse mesh boundaries and fine mesh intervals.""" with self.createRecord() as record: - self._data.xmesh = record.rwList( self._data.xmesh, "double", self._metadata["NCINTI"] + 1 ) diff --git a/armi/nuclearDataIO/cccc/isotxs.py b/armi/nuclearDataIO/cccc/isotxs.py index 3b3f42ee4..213529c0d 100644 --- a/armi/nuclearDataIO/cccc/isotxs.py +++ b/armi/nuclearDataIO/cccc/isotxs.py @@ -268,6 +268,48 @@ def readWrite(self): .. impl:: Tool to read and write ISOTXS files. :id: I_ARMI_NUCDATA_ISOTXS :implements: R_ARMI_NUCDATA_ISOTXS + + Reading and writing ISOTXS files is performed using the general + nuclear data I/O functionalities described in + :need:`I_ARMI_NUCDATA`. Reading/writing a ISOTXS file is performed + through the following steps: + + #. Read/write file ID record + #. Read/write file 1D record, which includes: + + * Number of energy groups (``NGROUP``) + * Maximum number of up-scatter groups (``MAXUP``) + * Maximum number of down-scatter groups (``MAXDN``) + * Maximum scattering order (``MAXORD``) + * File-wide specification on fission spectrum type, i.e. vector + or matrix (``ICHIST``) + * Maximum number of blocks of scattering data (``MSCMAX``) + * Subblocking control for scatter matrices (``NSBLOK``) + + #. Read/write file 2D record, which includes: + + * Library IDs for each isotope (``HSETID(I)``) + * Isotope names (``HISONM(I)``) + * Global fission spectrum (``CHI(J)``) if file-wide spectrum is + specified (``ICHIST`` = 1) + * Energy group structure (``EMAX(J)`` and ``EMIN``) + * Locations of each nuclide record in the file (``LOCA(I)``) + + .. note:: + + The offset data is not read from the binary file because + the ISOTXS reader can dynamically calculate the offset + itself. Therefore, during a read operation, this data is + ignored. + + #. Read/write file 4D record for each nuclide, which includes + isotope-dependent, group-independent data. + #. Read/write file 5D record for each nuclide, which includes + principal cross sections. + #. Read/write file 6D record for each nuclide, which includes + fission spectrum if it is flagged as a matrix (``ICHI`` > 1). + #. Read/write file 7D record for each nuclide, which includes the + scattering matrices. """ self._rwMessage() properties.unlockImmutableProperties(self._lib) @@ -369,8 +411,9 @@ def _computeNuclideRecordOffset(self): Notes ----- - This is not used within ARMI, because it can compute it arbitrarily. Other codes use this to seek to a - specific position within an ISOTXS file. + The offset data is not read from the binary file because the ISOTXS + reader can dynamically calculate the offset itself. Therefore, during a + read operation, this data is ignored. """ recordsPerNuclide = [ self._computeNumIsotxsRecords(nuc) for nuc in self._lib.nuclides diff --git a/armi/nuclearDataIO/cccc/pmatrx.py b/armi/nuclearDataIO/cccc/pmatrx.py index 83836ba98..9bdbe5f25 100644 --- a/armi/nuclearDataIO/cccc/pmatrx.py +++ b/armi/nuclearDataIO/cccc/pmatrx.py @@ -189,6 +189,25 @@ def readWrite(self): .. impl:: Tool to read and write PMATRX files. :id: I_ARMI_NUCDATA_PMATRX :implements: R_ARMI_NUCDATA_PMATRX + + Reading and writing PMATRX files is performed using the general + nuclear data I/O functionalities described in + :need:`I_ARMI_NUCDATA`. Reading/writing a PMATRX file is performed + through the following steps: + + #. Read/write global information including: + + * Number of gamma energy groups + * Number of neutron energy groups + * Maximum scattering order + * Maximum number of compositions + * Maximum number of materials + * Maximum number of regions + + #. Read/write energy group structure for neutrons and gammas + #. Read/write dose conversion factors + #. Read/write gamma production matrices for each nuclide, as well as + other reaction constants related to neutron-gamma production. """ self._rwMessage() properties.unlockImmutableProperties(self._lib) diff --git a/armi/nuclearDataIO/xsCollections.py b/armi/nuclearDataIO/xsCollections.py index a6672f872..02a263909 100644 --- a/armi/nuclearDataIO/xsCollections.py +++ b/armi/nuclearDataIO/xsCollections.py @@ -55,7 +55,7 @@ NT = "nt" # (n, triton) FISSION_XS = "fission" # (n, fission) N2N_XS = "n2n" # (n,2n) -NUSIGF = "nuSigF" +NUSIGF = "nuSigF" NU = "neutronsPerFission" # fmt: on CAPTURE_XS = [NGAMMA, NAPLHA, NP, ND, NT] @@ -274,7 +274,6 @@ def compare(self, other, flux, relativeTolerance=0, verbose=False): """Compare the cross sections between two XSCollections objects.""" equal = True for xsName in ALL_COLLECTION_DATA: - myXsData = self.__dict__[xsName] theirXsData = other.__dict__[xsName] @@ -785,13 +784,44 @@ def computeMacroscopicGroupConstants( multConstant=None, multLib=None, ): - """ + r""" Compute any macroscopic group constants given number densities and a microscopic library. .. impl:: Compute macroscopic cross sections from microscopic cross sections and number densities. :id: I_ARMI_NUCDATA_MACRO :implements: R_ARMI_NUCDATA_MACRO + This function computes the macroscopic cross sections of a specified + reaction type from inputted microscopic cross sections and number + densities. The ``constantName`` parameter specifies what type of + reaction is requested. The ``numberDensities`` parameter is dictionary + mapping the nuclide to its number density. The ``lib`` parameter is a library + object like :py:class:`~armi.nuclearDataIO.xsLibraries.IsotxsLibrary` or + :py:class:`~armi.nuclearDataIO.xsLibraries.CompxsLibrary` that holds the + microscopic cross-section data. The ``microSuffix`` parameter specifies + from which part of the library the microscopic cross sections are + gathered; this is typically gathered from a components + ``getMicroSuffix`` method like :py:meth:`Block.getMicroSuffix + <armi.reactor.blocks.Block.getMicroSuffix>`. ``libType`` is an optional + parameter specifying whether the reaction is for neutrons or gammas. + This function also has the optional parameters ``multConstant`` and + ``multLib``, which allows another constant from the library, such as + neutrons per fission (nu) or energy per fission (kappa), to be + multiplied to the primary one. The macroscopic cross sections are then + computed as: + + .. math:: + + \Sigma_{g} = \sum_{n} N_n \sigma_{n,g}\nu_n \quad g=1,...,G + + where :math:`n` is the isotope index, :math:`g` is the energy group + index, :math:`\sigma` is the microscopic cross section, and :math:`\nu` + is the scalar multiplier. If the library (``lib``) with suffix + ``microSuffix`` is missing a cross section for the ``constantName`` + reaction for one or more of the nuclides in ``numberDensities`` an error + is raised; but if ``multConstant`` is missing that cross section, then + those nuclides are printed as a warning. + Parameters ---------- constantName : str From 161bc61f64e4fdd9e42c64a8981bc5d6641947a2 Mon Sep 17 00:00:00 2001 From: Chris Keckler <ckeckler@terrapower.com> Date: Thu, 25 Jan 2024 12:00:00 -0600 Subject: [PATCH 159/176] Add descriptions for impls in the `blocks` module (#1619) * Add descriptions for some impls in the blocks module * finishing off blocks * black formatting * reviewer feedback * reviewer feedback * reviewer feedback * doh! * typos --------- Co-authored-by: Tony Alberti <aalberti@terrapower.com> Co-authored-by: aalberti <c-aalberti@terrapower.com> --- armi/reactor/blocks.py | 174 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 159 insertions(+), 15 deletions(-) diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py index e828450e1..55040174f 100644 --- a/armi/reactor/blocks.py +++ b/armi/reactor/blocks.py @@ -586,6 +586,17 @@ def getLocation(self): .. impl:: Location of a block is retrievable. :id: I_ARMI_BLOCK_POSI0 :implements: R_ARMI_BLOCK_POSI + + If the block does not have its ``core`` attribute set, if the block's + parent does not have a ``spatialGrid`` attribute, or if the block + does not have its location defined by its ``spatialLocator`` attribute, + return a string indicating that it is outside of the core. + + Otherwise, use the :py:class:`~armi.reactor.grids.Grid.getLabel` static + method to convert the block's indices into a string like "XXX-YYY-ZZZ". + For hexagonal geometry, "XXX" is the zero-padded hexagonal core ring, + "YYY" is the zero-padded position in that ring, and "ZZZ" is the zero-padded + block axial index from the bottom of the core. """ if self.core and self.parent.spatialGrid and self.spatialLocator: return self.core.spatialGrid.getLabel( @@ -601,6 +612,13 @@ def coords(self, rotationDegreesCCW=0.0): .. impl:: Coordinates of a block are queryable. :id: I_ARMI_BLOCK_POSI1 :implements: R_ARMI_BLOCK_POSI + + Calls to the :py:meth:`~armi.reactor.grids.locations.IndexLocation.getGlobalCoordinates` + method of the block's ``spatialLocator`` attribute, which recursively + calls itself on all parents of the block to get the coordinates of the + block's centroid in 3D cartesian space. + + If ``rotationDegreesCCW`` is non-zero, an error is raised. """ if rotationDegreesCCW: raise NotImplementedError("Cannot get coordinates with rotation.") @@ -686,6 +704,12 @@ def getVolume(self): :id: I_ARMI_BLOCK_DIMS0 :implements: R_ARMI_BLOCK_DIMS + Loops over all the components in the block, calling + :py:meth:`~armi.reactor.components.component.Component.getVolume` on + each and summing the result. The summed value is then divided by + the symmetry factor of the block to account for reduced volumes of + blocks in certain symmetric representations. + Returns ------- volume : float @@ -1083,9 +1107,21 @@ def getSortedComponentsInsideOfComponent(self, component): def getNumPins(self): """Return the number of pins in this block. - .. impl:: Get the number of pins in a block; potentially zero. + .. impl:: Get the number of pins in a block. :id: I_ARMI_BLOCK_NPINS :implements: R_ARMI_BLOCK_NPINS + + Uses some simple criteria to infer the number of pins in the block. + + For every flag in the module list :py:data:`~armi.reactor.blocks.PIN_COMPONENTS`, + loop over all components of that type in the block. If the component + is an instance of :py:class:`~armi.reactor.components.basicShapes.Circle`, + add its multiplicity to a list, and sum that list over all components + with each given flag. + + After looping over all possibilities, return the maximum value returned + from the process above, or if no compatible components were found, + return zero. """ nPins = [ sum( @@ -1239,6 +1275,21 @@ def getPitch(self, returnComp=False): """ Return the center-to-center hex pitch of this block. + .. impl:: Pitch of block is retrievable. + :id: I_ARMI_BLOCK_DIMS1 + :implements: R_ARMI_BLOCK_DIMS + + Uses the block's ``_pitchDefiningComponent`` to identify the component + in the block that defines the pitch. Then uses the + :py:meth:`~armi.reactor.components.component.Component.getPitchData` + method of that component to return the pitch for the block, accounting + for the component's current temperature. + + The ``_pitchDefiningComponent`` attribute can be set by + :py:meth:`~armi.reactor.blocks.Block.setPitch`, but is typically + set via a calls to :py:meth:`~armi.reactor.blocks.Block._updatePitchComponent` + as components are added to the block with :py:meth:`~armi.reactor.blocks.Block.add`. + Parameters ---------- returnComp : bool, optional @@ -1252,10 +1303,6 @@ def getPitch(self, returnComp=False): Component that has the max pitch, if returnComp == True. If no component is found to define the pitch, returns None - .. impl:: Pitch of block is retrievable. - :id: I_ARMI_BLOCK_DIMS1 - :implements: R_ARMI_BLOCK_DIMS - Notes ----- The block stores a reference to the component that defines the pitch, making the assumption @@ -1565,8 +1612,13 @@ def setAxialExpTargetComp(self, targetComponent): :id: I_ARMI_MANUAL_TARG_COMP :implements: R_ARMI_MANUAL_TARG_COMP - This method sets the provided ``Component`` to be the axial expansion - target ``Component``. + Sets the ``axialExpTargetComponent`` parameter on the block to the name + of the Component which is passed in. This is then used by the + :py:class:`~armi.reactor.converters.axialExpansionChanger.AxialExpansionChanger` + class during axial expansion. + + This method is typically called from within :py:meth:`~armi.reactor.blueprints.blockBlueprint.BlockBlueprint.construct` + during the process of building a Block from the blueprints. Parameter --------- @@ -1613,6 +1665,11 @@ class HexBlock(Block): .. impl:: ARMI has the ability to create hex shaped blocks. :id: I_ARMI_BLOCK_HEX :implements: R_ARMI_BLOCK_HEX + + This class defines hexagonal-shaped Blocks. It inherits functionality from the parent + class, Block, and defines hexagonal-specific methods including, but not limited to, + querying pin pitch, pin linear power densities, hydraulic diameter, and retrieving + inner and outer pitch. """ PITCH_COMPONENT_TYPE: ClassVar[_PitchDefiningComponent] = (components.Hexagon,) @@ -1627,6 +1684,16 @@ def coords(self, rotationDegreesCCW=0.0): .. impl:: Coordinates of a block are queryable. :id: I_ARMI_BLOCK_POSI2 :implements: R_ARMI_BLOCK_POSI + + Calls to the :py:meth:`~armi.reactor.grids.locations.IndexLocation.getGlobalCoordinates` + method of the block's ``spatialLocator`` attribute, which recursively + calls itself on all parents of the block to get the coordinates of the + block's centroid in 3D cartesian space. + + Will additionally adjust the x and y coordinates based on the block + parameters ``displacementX`` and ``displacementY``. + + Note that the ``rotationDegreesCCW`` argument is unused. """ x, y, _z = self.spatialLocator.getGlobalCoordinates() x += self.p.displacementX * 100.0 @@ -1644,6 +1711,15 @@ def createHomogenizedCopy(self, pinSpatialLocators=False): :id: I_ARMI_BLOCK_HOMOG :implements: R_ARMI_BLOCK_HOMOG + This method creates and returns a homogenized representation of itself in the form of a new Block. + The homogenization occurs in the following manner. A single Hexagon Component is created + add added to the new Block. This Hexagon Component is given the + :py:class:`armi.materials.mixture._Mixture` material and a volume averaged temperature + (``getAverageTempInC``). The number densities of the original Block are also stored on + this new Component (:need:`I_ARMI_CMP_GET_NDENS`). Several parameters from the original block + are copied onto the homogenized block (e.g., macros, lumped fission products, burnup group, + number of pins, and spatial grid). + Notes ----- This can be used to improve performance when a new copy of a reactor needs to be @@ -1668,6 +1744,12 @@ def createHomogenizedCopy(self, pinSpatialLocators=False): .. note: If you make a new block, you must add it to an assembly and a reactor. + Returns + ------- + b + A homogenized block containing a single Hexagon Component that contains an + average temperature and the number densities from the original block. + See Also -------- armi.reactor.converters.uniformMesh.UniformMeshGeometryConverter.makeAssemWithUniformMesh @@ -1728,6 +1810,11 @@ def getMaxArea(self): .. impl:: Area of block is retrievable. :id: I_ARMI_BLOCK_DIMS2 :implements: R_ARMI_BLOCK_DIMS + + This method first retrieves the pitch of the hexagonal Block + (:need:I_ARMI_UTIL_HEXAGON0) and then leverages the + area calculation via :need:I_ARMI_UTIL_HEXAGON0. + """ pitch = self.getPitch() if not pitch: @@ -1741,6 +1828,10 @@ def getDuctIP(self): .. impl:: IP dimension is retrievable. :id: I_ARMI_BLOCK_DIMS3 :implements: R_ARMI_BLOCK_DIMS + + This method retrieves the duct Component and quieries + it's inner pitch directly. If the duct is missing or if there + are multiple duct Components, an error will be raised. """ duct = self.getComponent(Flags.DUCT, exact=True) return duct.getDimension("ip") @@ -1752,6 +1843,10 @@ def getDuctOP(self): .. impl:: OP dimension is retrievable. :id: I_ARMI_BLOCK_DIMS4 :implements: R_ARMI_BLOCK_DIMS + + This method retrieves the duct Component and quieries + its outer pitch directly. If the duct is missing or if there + are multiple duct Components, an error will be raised. """ duct = self.getComponent(Flags.DUCT, exact=True) return duct.getDimension("op") @@ -2057,6 +2152,14 @@ def getPinToDuctGap(self, cold=False): :id: I_ARMI_BLOCK_DIMS5 :implements: R_ARMI_BLOCK_DIMS + Requires that the outer most duct be Hexagonal and wire and clad Components + be present. The flat-to-flat distance between the radial exterior of opposing + pins in the outermost ring is computed by computing the distance between + pin centers (``getPinCenterFlatToFlat``) and adding the outer diameter of + the clad Component and the outer diameter of the wire Component twice. The + total margin between the inner pitch of the duct Component and the wire is then + computed. The pin to duct gap is then half this distance. + Parameters ---------- cold : boolean @@ -2241,6 +2344,11 @@ def getPinPitch(self, cold=False): :id: I_ARMI_BLOCK_DIMS6 :implements: R_ARMI_BLOCK_DIMS + This implementation requires that clad and wire Components are present. + If not, an error is raised. If present, the pin pitch is calculated + as the sum of the outer diameter of the clad and outer diameter of + the wire. + Parameters ---------- cold : boolean @@ -2272,11 +2380,41 @@ def getPinPitch(self, cold=False): ) def getWettedPerimeter(self): - """Return the total wetted perimeter of the block in cm. + r"""Return the total wetted perimeter of the block in cm. .. impl:: Wetted perimeter of block is retrievable. :id: I_ARMI_BLOCK_DIMS7 :implements: R_ARMI_BLOCK_DIMS + + This implementation computes wetted perimeters for specific Components, as specified + by their Flags (:need:`R_ARMI_FLAG_DEFINE`). Hollow hexagons and circular pin Components + are supported. The latter supports both instances where the exterior is wetted + (e.g., clad, wire) as well as when the interior and exterior are wetted (hollow circle). + + Hollow hexagons are calculated via, + + .. math:: + + \frac{6 \times \text{ip}}{\sqrt{3}}, + + where :math:`\text{ip}` is the inner pitch of the hollow hexagon. Circular pin Components + where the exterior is wetted is calculated via, + + .. math:: + + N \pi \left( \text{OD}_c + \text{OD}_w \right), + + where :math:`N` is the total number of pins, :math:`\text{OD}_c` is the outer diameter + of the clad, and :math:`\text{OD}_w` is the outer diameter of the wire, respectively. + When both the interior and exterior are wetted, the wetted perimeter is calculated as + + .. math:: + + \pi \left( \text{OD} + \text{ID} \right), + + where :math:`\text{OD}` and :math:`\text{ID}` are the outer and inner diameters of the pin + Component, respectively. + """ # flags pertaining to hexagon components where the interior of the hexagon is wetted wettedHollowHexagonComponentFlags = ( @@ -2355,11 +2493,14 @@ def getFlowArea(self): .. impl:: Flow area of block is retrievable. :id: I_ARMI_BLOCK_DIMS8 :implements: R_ARMI_BLOCK_DIMS + + Retrieving the flow area requires that there be a single coolant Component. + If available, the area is calculated (:need:I_ARMI_COMP_VOL0). """ return self.getComponent(Flags.COOLANT, exact=True).getArea() def getHydraulicDiameter(self): - """ + r""" Return the hydraulic diameter in this block in cm. Hydraulic diameter is 4A/P where A is the flow area and P is the wetted perimeter. @@ -2367,15 +2508,18 @@ def getHydraulicDiameter(self): inside of the duct. The flow area is the inner area of the duct minus the area of the pins and the wire. - To convert the inner hex pitch into a perimeter, first convert to side, then - multiply by 6. - - p = sqrt(3)*s - l = 6*p/sqrt(3) - .. impl:: Hydraulic diameter of block is retrievable. :id: I_ARMI_BLOCK_DIMS9 :implements: R_ARMI_BLOCK_DIMS + + The hydraulic diamter is calculated via + + .. math:: + + 4\frac{A}{P}, + + where :math:`A` is the flow area (:need:I_ARMI_BLOCK_DIMS8) and :math:`P` is the + wetted perimeter (:need:I_ARMI_BLOCK_DIMS7). """ return 4.0 * self.getFlowArea() / self.getWettedPerimeter() From 068584163ad3aedc3897824b74760995f18707b8 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 25 Jan 2024 10:54:17 -0800 Subject: [PATCH 160/176] Removing the non-Python term class method (#1617) --- armi/plugins.py | 2 +- armi/reactor/blueprints/__init__.py | 2 +- armi/reactor/blueprints/blockBlueprint.py | 2 +- armi/reactor/blueprints/reactorBlueprint.py | 2 +- armi/reactor/composites.py | 2 +- armi/reactor/parameters/__init__.py | 2 +- armi/reactor/parameters/parameterDefinitions.py | 2 +- armi/settings/setting.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/armi/plugins.py b/armi/plugins.py index 918ccabbb..61d0830c5 100644 --- a/armi/plugins.py +++ b/armi/plugins.py @@ -698,7 +698,7 @@ def __enforceLimitations(self): assert ( len(self.__class__.defineSettings()) == 0 ), "UserPlugins cannot define new Settings, consider using an ArmiPlugin." - # NOTE: These are the class methods that we are staunchly _not_ allowing people + # NOTE: These are the methods that we are staunchly _not_ allowing people # to change in this class. If you need these, please use a regular ArmiPlugin. self.defineParameterRenames = lambda: {} self.defineSettings = lambda: [] diff --git a/armi/reactor/blueprints/__init__.py b/armi/reactor/blueprints/__init__.py index 29020a9a2..e600c4f75 100644 --- a/armi/reactor/blueprints/__init__.py +++ b/armi/reactor/blueprints/__init__.py @@ -546,7 +546,7 @@ def migrate(cls, inp: typing.TextIO): @classmethod def load(cls, stream, roundTrip=False): - """This class method is a wrapper around the `yamlize.Object.load()` method. + """This method is a wrapper around the `yamlize.Object.load()` method. The reason for the wrapper is to allow us to default to `Cloader`. Essentially, the `CLoader` class is 10x faster, but doesn't allow for "round trip" (read- diff --git a/armi/reactor/blueprints/blockBlueprint.py b/armi/reactor/blueprints/blockBlueprint.py index 7d5c504ab..7c11ded56 100644 --- a/armi/reactor/blueprints/blockBlueprint.py +++ b/armi/reactor/blueprints/blockBlueprint.py @@ -271,7 +271,7 @@ def _getMaterialModsFromBlockChildren(self, c: Composite) -> Set[str]: materialParentClass.applyInputParams ).parameters.keys() ) - # self is a parameter to class methods, so it gets picked up here + # self is a parameter to methods, so it gets picked up here # but that's obviously not a real material modifier perChildModifiers.discard("self") return perChildModifiers diff --git a/armi/reactor/blueprints/reactorBlueprint.py b/armi/reactor/blueprints/reactorBlueprint.py index a52104435..40e802cc5 100644 --- a/armi/reactor/blueprints/reactorBlueprint.py +++ b/armi/reactor/blueprints/reactorBlueprint.py @@ -133,7 +133,7 @@ def construct(self, cs, bp, reactor, geom=None, loadAssems=True): loadAssems : bool, optional whether to fill reactor with assemblies, as defined in blueprints, or not. Is False in :py:class:`UniformMeshGeometryConverter <armi.reactor.converters.uniformMesh.UniformMeshGeometryConverter>` - within the initNewReactor() class method. + within the initNewReactor() method. Raises ------ diff --git a/armi/reactor/composites.py b/armi/reactor/composites.py index 229e98e31..147d323d4 100644 --- a/armi/reactor/composites.py +++ b/armi/reactor/composites.py @@ -629,7 +629,7 @@ def getParameterCollection(cls): ``paramCollectionType`` is not a top-level object and therefore cannot be trivially pickled. Since we know that by the time we want to make any instances of/unpickle a given ``ArmiObject``, such a class attribute will have been - created and associated. So, we use this top-level class method to dig + created and associated. So, we use this top-level method to dig dynamically down to the underlying parameter collection type. .. impl:: Composites (and all ARMI objects) have parameter collections. diff --git a/armi/reactor/parameters/__init__.py b/armi/reactor/parameters/__init__.py index 5fe4de07e..d03773a20 100644 --- a/armi/reactor/parameters/__init__.py +++ b/armi/reactor/parameters/__init__.py @@ -179,7 +179,7 @@ class instance to have a ``__dict__``. This saves memory when there are many * - Parameters are just fancy properties with meta data. - Implementing the descriptor interface on a :py:class:`Parameter` removes the need to construct a :py:class:`Parameter` without a name, then come back through - with the ``applyParameters()`` class method to apply the + with the ``applyParameters()`` method to apply the :py:class:`Parameter` as a descriptor. .. _thefreedictionary: http://www.thefreedictionary.com/parameter diff --git a/armi/reactor/parameters/parameterDefinitions.py b/armi/reactor/parameters/parameterDefinitions.py index f75ed771b..a1d6c3d88 100644 --- a/armi/reactor/parameters/parameterDefinitions.py +++ b/armi/reactor/parameters/parameterDefinitions.py @@ -767,6 +767,6 @@ def defParam( # Container for all parameter definition collections that have been bound to an -# ArmiObject or subclass. These are added from the applyParameters() class method on +# ArmiObject or subclass. These are added from the applyParameters() method on # the ParameterCollection class. ALL_DEFINITIONS = ParameterDefinitionCollection() diff --git a/armi/settings/setting.py b/armi/settings/setting.py index 4f3c45210..9146c4e09 100644 --- a/armi/settings/setting.py +++ b/armi/settings/setting.py @@ -51,7 +51,7 @@ class Setting: :implements: R_ARMI_SETTINGS_DEFAULTS Setting objects hold all associated information of a setting in ARMI and should - typically be accessed through the Settings class methods rather than directly. + typically be accessed through the Settings methods rather than directly. Settings require a mandatory default value. Setting subclasses can implement custom ``load`` and ``dump`` methods that can From 9b9f82bc44293743e58f40afd9da76afcc417f4a Mon Sep 17 00:00:00 2001 From: Zachary Prince <zachmprince@gmail.com> Date: Thu, 25 Jan 2024 12:54:44 -0700 Subject: [PATCH 161/176] Hiding attribute values that are not useful (#1625) --- armi/nucDirectory/nuclideBases.py | 77 ++++++++++--------- armi/physics/constants.py | 2 + armi/physics/neutronics/energyGroups.py | 3 + .../blueprints/tests/test_customIsotopics.py | 2 +- 4 files changed, 45 insertions(+), 39 deletions(-) diff --git a/armi/nucDirectory/nuclideBases.py b/armi/nucDirectory/nuclideBases.py index 17e8e9c2c..1ccdaf290 100644 --- a/armi/nucDirectory/nuclideBases.py +++ b/armi/nucDirectory/nuclideBases.py @@ -36,7 +36,7 @@ class which is used to organize and store metadata about each nuclide. The The :py:class:`NuclideBase <armi.nucDirectory.nuclideBases.NuclideBase>` provides a data structure for information about a single nuclide, including the atom number, atomic weight, element, isomeric state, half-life, and - name. + name. The :py:mod:`nuclideBases <armi.nucDirectory.nuclideBases>` module provides a factory and associated functions for instantiating the @@ -91,44 +91,45 @@ class which is used to organize and store metadata about each nuclide. The >>> nuclideBases.byAAAZZZSId['2350920'] <NuclideBase U235: Z:92, A:235, S:0, W:2.350439e+02, Label:U235>, HL:2.22160758861e+16, Abund:7.204000e-03> -.. _nuclide-bases-table: - -.. exec:: - import numpy - from tabulate import tabulate - from armi.nucDirectory import nuclideBases - - attributes = ['name', - 'type', - 'a', - 'z', - 'state', - 'abundance', - 'weight', - 'halflife'] - - def getAttributes(nuc): - if nuc.halflife == numpy.inf: - halflife = "inf" - else: - halflife = f'{nuc.halflife:<12.6e}' - return [ - f'``{nuc.name}``', - f':py:class:`~armi.nucDirectory.nuclideBases.{nuc.__class__.__name__}`', - f'``{nuc.a}``', - f'``{nuc.z}``', - f'``{nuc.state}``', - f'``{nuc.abundance:<12.6e}``', - f'``{nuc.weight:<12.6e}``', - f'``{halflife}``', - ] - - sortedNucs = sorted(nuclideBases.instances) - return create_table(tabulate(tabular_data=[getAttributes(nuc) for nuc in sortedNucs], - headers=attributes, - tablefmt='rst'), - caption='List of nuclides') +.. only:: html + + .. _nuclide-bases-table: + + .. exec:: + import numpy + from tabulate import tabulate + from armi.nucDirectory import nuclideBases + attributes = ['name', + 'type', + 'a', + 'z', + 'state', + 'abundance', + 'weight', + 'halflife'] + + def getAttributes(nuc): + if nuc.halflife == numpy.inf: + halflife = "inf" + else: + halflife = f'{nuc.halflife:<12.6e}' + return [ + f'``{nuc.name}``', + f':py:class:`~armi.nucDirectory.nuclideBases.{nuc.__class__.__name__}`', + f'``{nuc.a}``', + f'``{nuc.z}``', + f'``{nuc.state}``', + f'``{nuc.abundance:<12.6e}``', + f'``{nuc.weight:<12.6e}``', + f'``{halflife}``', + ] + + sortedNucs = sorted(nuclideBases.instances) + return create_table(tabulate(tabular_data=[getAttributes(nuc) for nuc in sortedNucs], + headers=attributes, + tablefmt='rst'), + caption='List of nuclides') """ import os diff --git a/armi/physics/constants.py b/armi/physics/constants.py index 6e26747ff..30ee6db73 100644 --- a/armi/physics/constants.py +++ b/armi/physics/constants.py @@ -22,6 +22,8 @@ Notes ----- This data structure can be updated by plugins with design-specific dpa data. + +:meta hide-value: """ # The following are multigroup DPA XS for EBR II. They were generated using an ultra hard MCC spectrum diff --git a/armi/physics/neutronics/energyGroups.py b/armi/physics/neutronics/energyGroups.py index f528cf093..9cee0d887 100644 --- a/armi/physics/neutronics/energyGroups.py +++ b/armi/physics/neutronics/energyGroups.py @@ -131,6 +131,8 @@ def getGroupStructureType(neutronEnergyBoundsInEv): Values are the upper bound of each energy in eV from highest energy to lowest (because neutrons typically downscatter...) + +:meta hide-value: """ GROUP_STRUCTURE["2"] = [HIGH_ENERGY_EV, 6.25e-01] @@ -314,6 +316,7 @@ def getGroupStructureType(neutronEnergyBoundsInEv): itertools.repeat(1, 2082) ) + # fmt: on def _create_multigroup_structures_on_finegroup_energies( multigroup_energy_bounds, finegroup_energy_bounds diff --git a/armi/reactor/blueprints/tests/test_customIsotopics.py b/armi/reactor/blueprints/tests/test_customIsotopics.py index 01b6cca17..3366a3c36 100644 --- a/armi/reactor/blueprints/tests/test_customIsotopics.py +++ b/armi/reactor/blueprints/tests/test_customIsotopics.py @@ -25,7 +25,6 @@ class TestCustomIsotopics(unittest.TestCase): - yamlString = r""" nuclide flags: U238: {burn: true, xs: true} @@ -185,6 +184,7 @@ class TestCustomIsotopics(unittest.TestCase): axial mesh points: [1, 1, 1, 1, 1, 1,1] xs types: [A, A, A, A, A, A,A] """ + """:meta hide-value:""" @classmethod def setUpClass(cls): From e42a7d77f35e28b66244e4eeb48609ca472563fb Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 25 Jan 2024 11:59:42 -0800 Subject: [PATCH 162/176] Cleaning up various docstring errors (#1626) --- armi/cases/suite.py | 22 +++++++++++----------- armi/physics/fuelCycle/fuelHandlers.py | 17 +++++++++-------- armi/reactor/assemblies.py | 22 +++++++++++++++------- armi/settings/settingsIO.py | 8 ++++++-- 4 files changed, 41 insertions(+), 28 deletions(-) diff --git a/armi/cases/suite.py b/armi/cases/suite.py index 7fe1633d5..75701d2b9 100644 --- a/armi/cases/suite.py +++ b/armi/cases/suite.py @@ -49,12 +49,11 @@ class CaseSuite: :implements: R_ARMI_CASE_SUITE The CaseSuite object allows multiple, often related, - :py:class:`~armi.cases.case.Case` objects to be run sequentially. A - CaseSuite is intended to be both a pre-processing and post-processing - tool to facilitate case generation and analysis. Under most - circumstances one may wish to subclass a CaseSuite to meet the needs of - a specific calculation. A CaseSuite is a collection that is keyed off - Case titles. + :py:class:`~armi.cases.case.Case` objects to be run sequentially. A CaseSuite + is intended to be both a pre-processing or a post-processing tool to facilitate + case generation and analysis. Under most circumstances one may wish to subclass + a CaseSuite to meet the needs of a specific calculation. A CaseSuite is a + collection that is keyed off Case titles. """ def __init__(self, cs): @@ -92,7 +91,8 @@ def discover( self, rootDir=None, patterns=None, ignorePatterns=None, recursive=True ): """ - Finds case objects by searching for a pattern of inputs, and adds them to the suite. + Finds case objects by searching for a pattern of file paths, and adds them to + the suite. This searches for Settings input files and loads them to create Case objects. @@ -126,8 +126,8 @@ def echoConfiguration(self): Notes ----- - Some of these printouts won't make sense for all users, and may - make sense to be delegated to the plugins/app. + Some of these printouts won't make sense for all users, and may make sense to + be delegated to the plugins/app. """ for setting in self.cs.environmentSettings: runLog.important( @@ -158,8 +158,8 @@ def clone(self, oldRoot=None, writeStyle="short"): Creates a clone for each case within a CaseSuite. If ``oldRoot`` is not specified, then each case clone is made in a directory with the title of the - case. If ``oldRoot`` is specified, then a relative path from ``oldRoot`` will be - used to determine a new relative path to the current directory ``oldRoot``. + case. If ``oldRoot`` is specified, then a relative path from ``oldRoot`` will + be used to determine a new relative path to the current directory ``oldRoot``. Parameters ---------- diff --git a/armi/physics/fuelCycle/fuelHandlers.py b/armi/physics/fuelCycle/fuelHandlers.py index 0c271bf8f..3479b8fbf 100644 --- a/armi/physics/fuelCycle/fuelHandlers.py +++ b/armi/physics/fuelCycle/fuelHandlers.py @@ -371,14 +371,15 @@ def findAssembly( Examples -------- - feed = self.findAssembly(targetRing=4, - width=(0,0), - param='maxPercentBu', - compareTo=100, - typeSpec=Flags.FEED | Flags.FUEL) - - returns the feed fuel assembly in ring 4 that has a burnup closest to 100% (the highest - burnup assembly) + This returns the feed fuel assembly in ring 4 that has a burnup closest to 100% + (the highest burnup assembly):: + + feed = self.findAssembly(targetRing=4, + width=(0,0), + param='maxPercentBu', + compareTo=100, + typeSpec=Flags.FEED | Flags.FUEL) + """ def compareAssem(candidate, current): diff --git a/armi/reactor/assemblies.py b/armi/reactor/assemblies.py index cd5162b26..84d62941d 100644 --- a/armi/reactor/assemblies.py +++ b/armi/reactor/assemblies.py @@ -1246,12 +1246,18 @@ def getSymmetryFactor(self): def rotate(self, rad): """Rotates the spatial variables on an assembly by the specified angle. - Each block on the assembly is rotated in turn. + Each Block on the Assembly is rotated in turn. .. impl:: An assembly can be rotated about its z-axis. :id: I_ARMI_SHUFFLE_ROTATE :implements: R_ARMI_SHUFFLE_ROTATE + This method loops through every ``Block`` in this ``Assembly`` and rotates + it by a given angle (in radians). The rotation angle is positive in the + counter-clockwise direction, and must be divisible by increments of PI/6 + (60 degrees). To actually perform the ``Block`` rotation, the + :py:meth:`armi.reactor.blocks.Block.rotate` method is called. + Parameters ---------- rad: float @@ -1259,14 +1265,14 @@ def rotate(self, rad): Warning ------- - rad must be in 60-degree increments! (i.e., PI/6, PI/3, PI, 2 * PI/3, 5 * PI/6, etc) + rad must be in 60-degree increments! (i.e., PI/6, PI/3, PI, 2 * PI/3, etc) """ for b in self.getBlocks(): b.rotate(rad) class HexAssembly(Assembly): - """Placeholder, so users can explicitly define a hex-based assembly.""" + """Placeholder, so users can explicitly define a hex-based Assembly.""" pass @@ -1281,7 +1287,9 @@ class RZAssembly(Assembly): HexAssembly because they use different locations and need to have Radial Meshes in their setting. - note ThRZAssemblies should be a subclass of Assemblies (similar to Hex-Z) because + Notes + ----- + ThRZAssemblies should be a subclass of Assemblies (similar to Hex-Z) because they should have a common place to put information about subdividing the global mesh for transport - this is similar to how blocks have 'AxialMesh' in their blocks. """ @@ -1292,7 +1300,7 @@ def __init__(self, name, assemNum=None): def radialOuter(self): """ - returns the outer radial boundary of this assembly. + Returns the outer radial boundary of this assembly. See Also -------- @@ -1339,8 +1347,8 @@ class ThRZAssembly(RZAssembly): Notes ----- - This is a subclass of RZAssemblies, which is its a subclass of the Generics Assembly - Object + This is a subclass of RZAssemblies, which is itself a subclass of the generic + Assembly class. """ def __init__(self, assemType, assemNum=None): diff --git a/armi/settings/settingsIO.py b/armi/settings/settingsIO.py index 7f871ce2d..4447979e4 100644 --- a/armi/settings/settingsIO.py +++ b/armi/settings/settingsIO.py @@ -145,6 +145,10 @@ class SettingsReader: :id: I_ARMI_SETTINGS_IO_TXT :implements: R_ARMI_SETTINGS_IO_TXT + ARMI uses the YAML standard for settings files. ARMI uses industry-standard + ``ruamel.yaml`` Python libraray to read these files. ARMI does not bend or + change the YAML file format standard in any way. + Parameters ---------- cs : Settings @@ -171,9 +175,9 @@ def __init__(self, cs): self._renamer = SettingRenamer(dict(self.cs.items())) - # the input version will be overwritten if explicitly stated in input file. + # The input version will be overwritten if explicitly stated in input file. # otherwise, it's assumed to precede the version inclusion change and should be - # treated as alright + # treated as alright. def __getitem__(self, key): return self.cs[key] From 62befb4ae76484e8bacb4e6013ae3bd85259937a Mon Sep 17 00:00:00 2001 From: Arrielle Opotowsky <c-aopotowsky@terrapower.com> Date: Thu, 25 Jan 2024 15:01:16 -0600 Subject: [PATCH 163/176] Fix a couple of RST formatting issues (#1627) --- armi/nuclearDataIO/cccc/cccc.py | 10 +++++----- armi/reactor/blocks.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/armi/nuclearDataIO/cccc/cccc.py b/armi/nuclearDataIO/cccc/cccc.py index 7820a9610..90cd8f6cf 100644 --- a/armi/nuclearDataIO/cccc/cccc.py +++ b/armi/nuclearDataIO/cccc/cccc.py @@ -44,11 +44,11 @@ subclassed for each CCCC file. It is subclassed directly for the CCCC files that contain XS data: - * :py:class:`ISOTXS <armi.nuclearDataIO.cccc.isotxs.IsotxsIO>`, - * :py:mod:`GAMISO <armi.nuclearDataIO.cccc.gamiso>`, - * :py:class:`PMATRX <armi.nuclearDataIO.cccc.pmatrx.PmatrxIO>`. - * :py:class:`DLAYXS <armi.nuclearDataIO.cccc.dlayxs.DlayxsIO>`. - * :py:mod:`COMPXS <armi.nuclearDataIO.cccc.compxs>`. + * :py:class:`ISOTXS <armi.nuclearDataIO.cccc.isotxs.IsotxsIO>` + * :py:mod:`GAMISO <armi.nuclearDataIO.cccc.gamiso>` + * :py:class:`PMATRX <armi.nuclearDataIO.cccc.pmatrx.PmatrxIO>` + * :py:class:`DLAYXS <armi.nuclearDataIO.cccc.dlayxs.DlayxsIO>` + * :py:mod:`COMPXS <armi.nuclearDataIO.cccc.compxs>` For the CCCC file types that are outputs from a flux solver such as DIF3D (e.g., GEODST, DIF3D, NHFLUX) the streams are subclassed from diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py index 55040174f..435f108db 100644 --- a/armi/reactor/blocks.py +++ b/armi/reactor/blocks.py @@ -1812,8 +1812,8 @@ def getMaxArea(self): :implements: R_ARMI_BLOCK_DIMS This method first retrieves the pitch of the hexagonal Block - (:need:I_ARMI_UTIL_HEXAGON0) and then leverages the - area calculation via :need:I_ARMI_UTIL_HEXAGON0. + (:need:`I_ARMI_UTIL_HEXAGON0`) and then leverages the + area calculation via :need:`I_ARMI_UTIL_HEXAGON0`. """ pitch = self.getPitch() @@ -2495,7 +2495,7 @@ def getFlowArea(self): :implements: R_ARMI_BLOCK_DIMS Retrieving the flow area requires that there be a single coolant Component. - If available, the area is calculated (:need:I_ARMI_COMP_VOL0). + If available, the area is calculated (:need:`I_ARMI_COMP_VOL0`). """ return self.getComponent(Flags.COOLANT, exact=True).getArea() @@ -2518,8 +2518,8 @@ def getHydraulicDiameter(self): 4\frac{A}{P}, - where :math:`A` is the flow area (:need:I_ARMI_BLOCK_DIMS8) and :math:`P` is the - wetted perimeter (:need:I_ARMI_BLOCK_DIMS7). + where :math:`A` is the flow area (:need:`I_ARMI_BLOCK_DIMS8`) and :math:`P` is the + wetted perimeter (:need:`I_ARMI_BLOCK_DIMS7`). """ return 4.0 * self.getFlowArea() / self.getWettedPerimeter() From 01455503fcc7492e52f2d272cc32b09965cf34a9 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 25 Jan 2024 15:46:53 -0800 Subject: [PATCH 164/176] Improving docs: settings report and missing impl text (#1629) Co-authored-by: Arrielle Opotowsky <c-aopotowsky@terrapower.com> --- armi/reactor/components/__init__.py | 13 +++++- doc/user/inputs.rst | 68 ++++++++++++++++++++--------- 2 files changed, 59 insertions(+), 22 deletions(-) diff --git a/armi/reactor/components/__init__.py b/armi/reactor/components/__init__.py index 2c9aab3c9..ccb1b8c22 100644 --- a/armi/reactor/components/__init__.py +++ b/armi/reactor/components/__init__.py @@ -325,9 +325,19 @@ def getBoundingCircleOuterDiameter(self, Tc=None, cold=False): def computeVolume(self): """Cannot compute volume until it is derived. - .. impl:: The volume of a DerivedShape depends on the solid shapes surrounding them. + .. impl:: The volume of a DerivedShape depends on the solid shapes surrounding + them. :id: I_ARMI_COMP_FLUID0 :implements: R_ARMI_COMP_FLUID + + Computing the volume of a ``DerivedShape`` means looking at the solid + materials around it, and finding what shaped space is left over in between + them. This method calls the method ``_deriveVolumeAndArea``, which makes + use of the fact that the ARMI reactor data model is hierarchical. It starts + by finding the parent of this object, and then finding the volume of all + the other objects at this level. Whatever is left over, is the volume of + this object. Obviously, you can only have one ``DerivedShape`` child of any + parent for this logic to work. """ return self._deriveVolumeAndArea() @@ -400,6 +410,7 @@ def _deriveVolumeAndArea(self): self.p.area = remainingArea else: self.p.area = remainingVolume / height + return remainingVolume def getVolume(self): diff --git a/doc/user/inputs.rst b/doc/user/inputs.rst index ee62f2006..5a49e303f 100644 --- a/doc/user/inputs.rst +++ b/doc/user/inputs.rst @@ -1423,8 +1423,8 @@ To use it, just say:: a = self.findAssembly(param='maxPercentBu',compareTo=20) -This will return the assembly in the reactor that has a maximum burnup closest to 20%. Other -inputs to findAssembly are summarized in the API docs of +This will return the assembly in the reactor that has a maximum burnup closest to 20%. +Other inputs to findAssembly are summarized in the API docs of :py:meth:`~armi.physics.fuelCycle.fuelHandlers.FuelHandler.findAssembly`. @@ -1433,30 +1433,40 @@ Fuel Management Examples Convergent-Divergent ^^^^^^^^^^^^^^^^^^^^ -Convergent-divergent shuffling is when fresh assemblies march in from the outside until they approach the jump ring, -at which point they jump to the center and diverge until they reach the jump ring again, where they now jump to the -outer periphery of the core, or become discharged. +Convergent-divergent shuffling is when fresh assemblies march in from the outside until +they approach the jump ring, at which point they jump to the center and diverge until +they reach the jump ring again, where they now jump to the outer periphery of the core, +or become discharged. If the jump ring is 6, the order of target rings is:: [6, 5, 4, 3, 2, 1, 6, 7, 8, 9, 10, 11, 12, 13] -In this case, assemblies converge from ring 13 to 12, to 11, to 10, ..., to 6, and then jump to 1 and diverge -until they get back to 6. In a discharging equilibrium case, the highest burned assembly in the jumpRing should -get discharged and the lowest should jump by calling a dischargeSwap on cascade[0] and a fresh feed after this -cascade is run. +In this case, assemblies converge from ring 13 to 12, to 11, to 10, ..., to 6, and then +jump to 1 and diverge until they get back to 6. In a discharging equilibrium case, the +highest burned assembly in the jumpRing should get discharged and the lowest should +jump by calling a dischargeSwap on cascade[0] and a fresh feed after this cascade is +run. -The convergent rings in this case are 7 through 13 and the divergent ones are 1 through 5 are the divergent ones. +The convergent rings in this case are 7 through 13 and the divergent ones are 1 +through 5 are the divergent ones. Fuel Management Tips -------------------- Some mistakes are common. Follow these tips. - * Always make sure your assembly-level types in the settings file are up to date with the grids in your bluepints file. Otherwise you'll be moving feeds when you want to move igniters, or something. - * Use the exclusions list! If you move a cascade and then the next cascade tries to run, it will choose your newly-moved assemblies if they fit your criteria in ``findAssemblies``. This leads to very confusing results. Therefore, once you move assemblies, you should default to adding them to the exclusions list. - * Print cascades during debugging. After you've built a cascade to swap, print it out and check the locations and types of each assembly in it. Is it what you want? - * Watch ``typeNum`` in the database. You can get good intuition about what is getting moved by viewing this parameter. + * Always make sure your assembly-level types in the settings file are up to date + with the grids in your bluepints file. Otherwise you'll be moving feeds when you + want to move igniters, or something. + * Use the exclusions list! If you move a cascade and then the next cascade tries + to run, it will choose your newly-moved assemblies if they fit your criteria in + ``findAssemblies``. This leads to very confusing results. Therefore, once you move + assemblies, you should default to adding them to the exclusions list. + * Print cascades during debugging. After you've built a cascade to swap, print it + out and check the locations and types of each assembly in it. Is it what you want? + * Watch ``typeNum`` in the database. You can get good intuition about what is + getting moved by viewing this parameter. Running a branch search ----------------------- @@ -1477,8 +1487,8 @@ should be used to fabricate new assemblies. Given a fuel handler that can thusly interpret factors between 0 and 1, the concept of branch searches is simple. They simply build uniformly distributed lists between 0 and 1 across however many CPUs are available and cases on all -of them, passing one of each of the factors to each CPU in parallel. When the cases finish, -the branch search determines the optimal result and selects the corresponding +of them, passing one of each of the factors to each CPU in parallel. When the cases +finish, the branch search determines the optimal result and selects the corresponding value of the factor to proceed. Branch searches are controlled by custom `getFactorList` methods specified in the @@ -1527,27 +1537,43 @@ Settings Report This document lists all the `settings <#the-settings-input-file>`_ in ARMI. They are all accessible to developers -through the :py:class:`armi.settings.caseSettings.Settings` object, which is typically stored in a variable named -``cs``. Interfaces have access to a simulation's settings through ``self.cs``. +through the :py:class:`armi.settings.caseSettings.Settings` object, which is typically +stored in a variable named ``cs``. Interfaces have access to a simulation's settings +through ``self.cs``. .. exec:: from armi import settings import textwrap + def looks_like_path(s): + """Super quick, not robust, check if a string looks like a file path.""" + if s.startswith("\\\\") or s.startswith("//"): + return True + elif s[1:].startswith(":\\") + return True + return False + subclassTables = {} cs = settings.Settings() + # User textwrap to split up long words that mess up the table. wrapper = textwrap.TextWrapper(width=25, subsequent_indent='') wrapper2 = textwrap.TextWrapper(width=10, subsequent_indent='') - content = '\n.. list-table:: ARMI Settings\n :header-rows: 1\n :widths: 30 30 10 10\n \n' + content = '\n.. list-table:: ARMI Settings\n :header-rows: 1\n :widths: 20 30 15 15\n \n' content += ' * - Name\n - Description\n - Default\n - Options\n' for setting in sorted(cs.values(), key=lambda s: s.name): content += ' * - {}\n'.format(' '.join(wrapper.wrap(setting.name))) content += ' - {}\n'.format(' '.join(wrapper.wrap(str(setting.description) or ''))) - content += ' - {}\n'.format(' '.join(['``{}``'.format(wrapped) for wrapped in wrapper2.wrap(str(getattr(setting, 'default', None)).split("/")[-1])])) - content += ' - {}\n'.format(' '.join(['``{}``'.format(wrapped) for wrapped in wrapper.wrap(str(getattr(setting,'options','') or ''))])) + default = str(getattr(setting, 'default', None)).split("/")[-1] + options = str(getattr(setting,'options','') or '') + if looks_like_path(defaults): + # We don't want to display default file paths in this table. + default = "" + options = "" + content += ' - {}\n'.format(' '.join(['``{}``'.format(wrapped) for wrapped in wrapper2.wrap(default)])) + content += ' - {}\n'.format(' '.join(['``{}``'.format(wrapped) for wrapped in wrapper.wrap(options)])) content += '\n' From ffa519b947f75733d5924503e46e787f22211348 Mon Sep 17 00:00:00 2001 From: Tony Alberti <aalberti@terrapower.com> Date: Thu, 25 Jan 2024 16:08:45 -0800 Subject: [PATCH 165/176] Removing undefined acronyms from reqs & fix broken links (#1628) --- armi/materials/mixture.py | 2 ++ armi/nuclearDataIO/cccc/cccc.py | 2 +- .../neutronics/crossSectionGroupManager.py | 18 +++++++++--------- .../globalFlux/globalFluxInterface.py | 6 +++--- .../isotopicDepletion/crossSectionTable.py | 2 +- .../tests/test_crossSectionManager.py | 14 +++++++------- armi/reactor/assemblies.py | 2 +- armi/reactor/components/component.py | 2 +- armi/reactor/reactors.py | 6 +++--- 9 files changed, 28 insertions(+), 26 deletions(-) diff --git a/armi/materials/mixture.py b/armi/materials/mixture.py index b3a7c7c50..91f5a8613 100644 --- a/armi/materials/mixture.py +++ b/armi/materials/mixture.py @@ -21,6 +21,8 @@ class _Mixture(materials.Material): """ Homogenized mixture of materials. + :meta public: + .. warning:: This class is meant to be used for homogenized block models for neutronics and other physics solvers. diff --git a/armi/nuclearDataIO/cccc/cccc.py b/armi/nuclearDataIO/cccc/cccc.py index 90cd8f6cf..d7623cca4 100644 --- a/armi/nuclearDataIO/cccc/cccc.py +++ b/armi/nuclearDataIO/cccc/cccc.py @@ -42,7 +42,7 @@ a file for reading or writing on the ``__enter__`` and closes that file upon ``__exit__``. :py:class:`Stream` is an abstract base class that is subclassed for each CCCC file. It is subclassed directly for the CCCC files - that contain XS data: + that contain cross-section data: * :py:class:`ISOTXS <armi.nuclearDataIO.cccc.isotxs.IsotxsIO>` * :py:mod:`GAMISO <armi.nuclearDataIO.cccc.gamiso>` diff --git a/armi/physics/neutronics/crossSectionGroupManager.py b/armi/physics/neutronics/crossSectionGroupManager.py index 5b83c40ba..54028511a 100644 --- a/armi/physics/neutronics/crossSectionGroupManager.py +++ b/armi/physics/neutronics/crossSectionGroupManager.py @@ -324,7 +324,7 @@ class AverageBlockCollection(BlockCollection): volume-weighted average. Inheriting functionality from the abstract :py:class:`Reactor <armi.physics.neutronics.crossSectionGroupManager.BlockCollection>` object, this class will construct representative blocks using averaged parameters of all blocks in the given collection. - Number density averages can be computed at a component level through ``self._performAverageByComponent``, + Number density averages can be computed at a component level or at a block level by default. Average nuclide temperatures and burnup are also included when constructing a representative block. """ @@ -855,7 +855,7 @@ def __init__(self, r, cs): def interactBOL(self): """Called at the Beginning-of-Life of a run, before any cycles start. - .. impl:: The lattice physics interface and XSGM are connected at BOL. + .. impl:: The lattice physics interface and cross-section group manager are connected at BOL. :id: I_ARMI_XSGM_FREQ0 :implements: R_ARMI_XSGM_FREQ @@ -885,7 +885,7 @@ def interactBOC(self, cycle=None): """ Update representative blocks and block burnup groups. - .. impl:: The lattice physics interface and XSGM are connected at BOC. + .. impl:: The lattice physics interface and cross-section group manager are connected at BOC. :id: I_ARMI_XSGM_FREQ1 :implements: R_ARMI_XSGM_FREQ @@ -910,7 +910,7 @@ def interactEOC(self, cycle=None): def interactEveryNode(self, cycle=None, tn=None): """Interaction at every time node. - .. impl:: The lattice physics interface and XSGM are connected at every time node. + .. impl:: The lattice physics interface and cross-section group manager are connected at every time node. :id: I_ARMI_XSGM_FREQ2 :implements: R_ARMI_XSGM_FREQ @@ -923,19 +923,19 @@ def interactEveryNode(self, cycle=None, tn=None): self.createRepresentativeBlocks() def interactCoupled(self, iteration): - """Update XS groups on each physics coupling iteration to get latest temperatures. + """Update cross-section groups on each physics coupling iteration to get latest temperatures. - .. impl:: The lattice physics interface and XSGM are connected during coupling. + .. impl:: The lattice physics interface and cross-section group manager are connected during coupling. :id: I_ARMI_XSGM_FREQ3 :implements: R_ARMI_XSGM_FREQ This method updates representative blocks and block burnups at every node and the first coupled iteration for each cross-section ID - if the control logic for lattices physics frequency updates is set for the first coupled iteration (`firstCoupledIteration`) through the :py:class:`LatticePhysicsInterface <armi.physics.neutronics.latticePhysics>`. + if the control logic for lattices physics frequency updates is set for the first coupled iteration (``firstCoupledIteration``) through the :py:class:`LatticePhysicsInterface <armi.physics.neutronics.latticePhysics>`. The cross-section group manager will construct representative blocks for each cross-section ID at the first iteration of every time node. Notes ----- - Updating the XS on only the first (i.e., iteration == 0) timenode can be a reasonable approximation to + Updating the cross-section on only the first (i.e., iteration == 0) timenode can be a reasonable approximation to get new cross sections with some temperature updates but not have to run lattice physics on each coupled iteration. If the user desires to have the cross sections updated with every coupling iteration, the ``latticePhysicsFrequency: all`` option. @@ -1112,7 +1112,7 @@ def _getPregeneratedFluxFileLocationData(self, xsID): def createRepresentativeBlocks(self): """Get a representative block from each cross-section ID managed here. - .. impl:: Create collections of blocks based on XS type and burn-up group. + .. impl:: Create collections of blocks based on cross-section type and burn-up group. :id: I_ARMI_XSGM_CREATE_XS_GROUPS :implements: R_ARMI_XSGM_CREATE_XS_GROUPS diff --git a/armi/physics/neutronics/globalFlux/globalFluxInterface.py b/armi/physics/neutronics/globalFlux/globalFluxInterface.py index 21fbd1a5a..1b202f91c 100644 --- a/armi/physics/neutronics/globalFlux/globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/globalFluxInterface.py @@ -1245,7 +1245,7 @@ def computeDpaRate(mgFlux, dpaXs): :implements: R_ARMI_FLUX_DPA This method calculates DPA rates using the inputted multigroup flux and DPA cross sections. - Displacements calculated by displacement XS: + Displacements calculated by displacement cross-section: .. math:: :nowrap: @@ -1269,8 +1269,8 @@ def computeDpaRate(mgFlux, dpaXs): \frac{\text{dpa}}{s} = \frac{\phi N \sigma}{N} = \phi * \sigma - the Number density of the structural material cancels out. It's in the macroscopic - XS and in the original number of atoms. + the number density of the structural material cancels out. It's in the macroscopic + cross-section and in the original number of atoms. Raises ------ diff --git a/armi/physics/neutronics/isotopicDepletion/crossSectionTable.py b/armi/physics/neutronics/isotopicDepletion/crossSectionTable.py index 2f159db01..efec30290 100644 --- a/armi/physics/neutronics/isotopicDepletion/crossSectionTable.py +++ b/armi/physics/neutronics/isotopicDepletion/crossSectionTable.py @@ -187,7 +187,7 @@ def makeReactionRateTable(obj, nuclides: List = None): that is stored there. If ``obj`` does not belong to a ``Core``, a warning is printed. - For each child of ``obj``, use the ISOTXS library and the xsID for the associated block + For each child of ``obj``, use the ISOTXS library and the cross-section ID for the associated block to produce a reaction rate dictionary in units of inverse seconds for the nuclide specified in the original call to ``obj.getReactionRates()``. Because ``nDensity`` was originally specified as diff --git a/armi/physics/neutronics/tests/test_crossSectionManager.py b/armi/physics/neutronics/tests/test_crossSectionManager.py index 40be69726..dc4f80379 100644 --- a/armi/physics/neutronics/tests/test_crossSectionManager.py +++ b/armi/physics/neutronics/tests/test_crossSectionManager.py @@ -485,7 +485,7 @@ def setUp(self): ] def test_ComponentAverage1DCylinder(self): - """Tests that the XS group manager calculates the expected component atom density + """Tests that the cross-section group manager calculates the expected component atom density and component area correctly. Order of components is also checked since in 1D cases the order of the components matters. @@ -852,7 +852,7 @@ def test_createRepresentativeBlocksUsingExistingBlocks(self): def test_interactBOL(self): """Test `BOL` lattice physics update frequency. - .. test:: The XSGM frequency depends on the LPI frequency at BOL. + .. test:: The cross-section group manager frequency depends on the LPI frequency at BOL. :id: T_ARMI_XSGM_FREQ0 :tests: R_ARMI_XSGM_FREQ """ @@ -865,7 +865,7 @@ def test_interactBOL(self): def test_interactBOC(self): """Test `BOC` lattice physics update frequency. - .. test:: The XSGM frequency depends on the LPI frequency at BOC. + .. test:: The cross-section group manager frequency depends on the LPI frequency at BOC. :id: T_ARMI_XSGM_FREQ1 :tests: R_ARMI_XSGM_FREQ """ @@ -879,7 +879,7 @@ def test_interactBOC(self): def test_interactEveryNode(self): """Test `everyNode` lattice physics update frequency. - .. test:: The XSGM frequency depends on the LPI frequency at every time node. + .. test:: The cross-section group manager frequency depends on the LPI frequency at every time node. :id: T_ARMI_XSGM_FREQ2 :tests: R_ARMI_XSGM_FREQ """ @@ -895,7 +895,7 @@ def test_interactEveryNode(self): def test_interactFirstCoupledIteration(self): """Test `firstCoupledIteration` lattice physics update frequency. - .. test:: The XSGM frequency depends on the LPI frequency during first coupled iteration. + .. test:: The cross-section group manager frequency depends on the LPI frequency during first coupled iteration. :id: T_ARMI_XSGM_FREQ3 :tests: R_ARMI_XSGM_FREQ """ @@ -911,7 +911,7 @@ def test_interactFirstCoupledIteration(self): def test_interactAllCoupled(self): """Test `all` lattice physics update frequency. - .. test:: The XSGM frequency depends on the LPI frequency during coupling. + .. test:: The cross-section group manager frequency depends on the LPI frequency during coupling. :id: T_ARMI_XSGM_FREQ4 :tests: R_ARMI_XSGM_FREQ """ @@ -927,7 +927,7 @@ def test_interactAllCoupled(self): def test_xsgmIsRunBeforeXS(self): """Test that the XSGM is run before the cross sections are calculated. - .. test:: Test that the XSGM is run before the cross sections are calculated. + .. test:: Test that the cross-section group manager is run before the cross sections are calculated. :id: T_ARMI_XSGM_FREQ5 :tests: R_ARMI_XSGM_FREQ """ diff --git a/armi/reactor/assemblies.py b/armi/reactor/assemblies.py index 84d62941d..bd27ae65b 100644 --- a/armi/reactor/assemblies.py +++ b/armi/reactor/assemblies.py @@ -1223,7 +1223,7 @@ def getDim(self, typeSpec, dimName): ``dimName``. There is a hard-coded preference for Components to be within fuel Blocks. If there are no Blocks, then ``None`` is returned. If ``typeSpec`` is not within the first Block, an - error is raised within :py:meth:`armi.reactor.blocksBlock.getDim`. + error is raised within :py:meth:`~armi.reactor.blocks.Block.getDim`. """ # prefer fuel blocks. bList = self.getBlocks(Flags.FUEL) diff --git a/armi/reactor/components/component.py b/armi/reactor/components/component.py index bfdccd42b..033a948d2 100644 --- a/armi/reactor/components/component.py +++ b/armi/reactor/components/component.py @@ -980,7 +980,7 @@ def getThermalExpansionFactor(self, Tc=None, T0=None): between ``T0`` and ``Tc`` is used to calculate the thermal expansion factor. If a solid material does not have a linear expansion factor defined and the temperature difference is greater than - :py:attr:`armi.reactor.components.component.Component._TOLERANCE`, an + a predetermined tolerance, an error is raised. Thermal expansion of fluids or custom materials is neglected, currently. diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index c96c97109..394d82211 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -396,9 +396,9 @@ def symmetry(self) -> geometry.SymmetryType: <armi.reactor.grids.grid.Grid>` type, :py:class:`DomainType <armi.reactor.geometry.DomainType>`, and :py:class:`BoundaryType <armi.reactor.geometry.BoundaryType>` are valid. The validity of a - user-specified geometry and symmetry is verified by a settings: - :py:meth:`Inspector - <armi.operators.settingsValidation.Inspector._inspectSettings`. + user-specified geometry and symmetry is verified by a settings + :py:class:`Inspector + <armi.operators.settingsValidation.Inspector`. """ if not self.spatialGrid: raise ValueError("Cannot access symmetry before a spatialGrid is attached.") From 5879f956d6e57164ac84e7fd571be04a8a43fc29 Mon Sep 17 00:00:00 2001 From: Arrielle Opotowsky <c-aopotowsky@terrapower.com> Date: Fri, 26 Jan 2024 14:56:29 -0600 Subject: [PATCH 166/176] Remove empty sections from docs (#1631) --- doc/user/radial_and_axial_expansion.rst | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/doc/user/radial_and_axial_expansion.rst b/doc/user/radial_and_axial_expansion.rst index ea43e858a..2b47c6006 100644 --- a/doc/user/radial_and_axial_expansion.rst +++ b/doc/user/radial_and_axial_expansion.rst @@ -64,13 +64,3 @@ Equation :eq:`linearExpansionFactor` is the expression used by ARMI in :py:meth: .. note:: :py:meth:`linearExpansionPercent <armi.materials.material.Material.linearExpansionPercent>` returns :math:`\frac{L - L_0}{L_0}` in %. - -.. _radialExpansion: - -Radial Expansion -================ - -.. _axialExpansion: - -Axial Expansion -=============== From f15d00ebe534e5f28e168643a016c8a5bc67885a Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Fri, 26 Jan 2024 13:39:11 -0800 Subject: [PATCH 167/176] Fixing release notes for 0.3.0 (#1632) --- doc/release/0.3.rst | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/doc/release/0.3.rst b/doc/release/0.3.rst index d974809c6..e27d98c6b 100644 --- a/doc/release/0.3.rst +++ b/doc/release/0.3.rst @@ -3,7 +3,7 @@ ARMI v0.3 Release Notes *********************** ARMI v0.3.1 -============ +=========== Release Date: TBD What's new in ARMI? @@ -12,17 +12,16 @@ What's new in ARMI? Bug Fixes --------- -#. ``StructuredGrid.getNeighboringCellIndices()`` was incorrectly implemented for the second neighbor. (`PR#1614 <https://github.com/terrapower/armi/pull/1614>`_) +#. TBD Changes that Affect Requirements -------------------------------- - #. TBD ARMI v0.3.0 -============ -Release Date: 2023-12-21 +=========== +Release Date: 2024-01-26 What's new in ARMI? ------------------- @@ -31,6 +30,10 @@ What's new in ARMI? #. Use ``functools`` to preserve function attributes when wrapping with ``codeTiming.timed`` (`PR#1466 <https://github.com/terrapower/armi/pull/1466>`_) #. Remove a number of deprecated block, assembly, and core parameters related to a defunct internal plugin. +Bug Fixes +--------- +#. ``StructuredGrid.getNeighboringCellIndices()`` was incorrectly implemented for the second neighbor. (`PR#1614 <https://github.com/terrapower/armi/pull/1614>`_) + Quality Work ------------ #. ARMI now mandates ``ruff`` linting. (`PR#1419 <https://github.com/terrapower/armi/pull/1419>`_) From c37483854dfa97f384d7f8466cdf6de4cf353b3a Mon Sep 17 00:00:00 2001 From: Michael Jarrett <mjarrett@terrapower.com> Date: Mon, 29 Jan 2024 09:46:46 -0800 Subject: [PATCH 168/176] Catchall PR for minor formatting/typo updates to docstrings (#1634) * Fixing settings table dump * Fixing various things in API docs * Fixing typos --------- Co-authored-by: zachmprince <zachmprince@gmail.com> Co-authored-by: jyang <jyang@terrapower.com> --- armi/materials/thU.py | 4 +- armi/materials/thorium.py | 4 +- armi/materials/thoriumOxide.py | 4 +- armi/nucDirectory/elements.py | 70 +++++++++++----------- armi/nuclearDataIO/cccc/cccc.py | 4 +- armi/nuclearDataIO/xsCollections.py | 2 +- armi/reactor/blocks.py | 2 +- armi/reactor/components/component.py | 2 +- armi/reactor/composites.py | 6 +- armi/reactor/converters/blockConverters.py | 31 ++++++---- armi/reactor/reactors.py | 6 +- doc/user/inputs.rst | 6 +- 12 files changed, 75 insertions(+), 66 deletions(-) diff --git a/armi/materials/thU.py b/armi/materials/thU.py index abb3301f6..e46b9c6a6 100644 --- a/armi/materials/thU.py +++ b/armi/materials/thU.py @@ -15,9 +15,9 @@ """ Thorium Uranium metal. -Data is from [#IAEA-TECDOCT-1450]_. +Data is from [IAEA-TECDOCT-1450]_. -.. [#IAEA-TECDOCT-1450] Thorium fuel cycle -- Potential benefits and challenges, IAEA-TECDOC-1450 (2005). +.. [IAEA-TECDOCT-1450] Thorium fuel cycle -- Potential benefits and challenges, IAEA-TECDOC-1450 (2005). https://www-pub.iaea.org/mtcd/publications/pdf/te_1450_web.pdf """ diff --git a/armi/materials/thorium.py b/armi/materials/thorium.py index 74723201f..62807a873 100644 --- a/armi/materials/thorium.py +++ b/armi/materials/thorium.py @@ -15,9 +15,9 @@ """ Thorium Metal. -Data is from [#IAEA-TECDOCT-1450]_. +Data is from [IAEA-TECDOCT-1450]_. -.. [#IAEA-TECDOCT-1450] Thorium fuel cycle -- Potential benefits and challenges, IAEA-TECDOC-1450 (2005). +.. [IAEA-TECDOCT-1450] Thorium fuel cycle -- Potential benefits and challenges, IAEA-TECDOC-1450 (2005). https://www-pub.iaea.org/mtcd/publications/pdf/te_1450_web.pdf """ from armi.materials.material import FuelMaterial diff --git a/armi/materials/thoriumOxide.py b/armi/materials/thoriumOxide.py index df2dc6c91..157f4a0c6 100644 --- a/armi/materials/thoriumOxide.py +++ b/armi/materials/thoriumOxide.py @@ -15,9 +15,9 @@ """ Thorium Oxide solid ceramic. -Data is from [#IAEA-TECDOCT-1450]_. +Data is from [IAEA-TECDOCT-1450]_. -.. [#IAEA-TECDOCT-1450] Thorium fuel cycle -- Potential benefits and challenges, IAEA-TECDOC-1450 (2005). +.. [IAEA-TECDOCT-1450] Thorium fuel cycle -- Potential benefits and challenges, IAEA-TECDOC-1450 (2005). https://www-pub.iaea.org/mtcd/publications/pdf/te_1450_web.pdf """ from armi import runLog diff --git a/armi/nucDirectory/elements.py b/armi/nucDirectory/elements.py index b1f84fbef..cee177402 100644 --- a/armi/nucDirectory/elements.py +++ b/armi/nucDirectory/elements.py @@ -90,41 +90,41 @@ <Element LR (Z=103), Lawrencium, ChemicalGroup.ACTINIDE, ChemicalPhase.SOLID>] -For specific data on nuclides within each element, refer to the -:ref:`nuclide bases summary table <nuclide-bases-table>`. - - -.. exec:: - from tabulate import tabulate - from armi.nucDirectory import elements - - attributes = ['z', - 'name', - 'symbol', - 'phase', - 'group', - 'is naturally occurring?', - 'is heavy metal?', - 'num. nuclides',] - - def getAttributes(element): - return [ - f'``{element.z}``', - f'``{element.name}``', - f'``{element.symbol}``', - f'``{element.phase}``', - f'``{element.group}``', - f'``{element.isNaturallyOccurring()}``', - f'``{element.isHeavyMetal()}``', - f'``{len(element.nuclides)}``', - ] - - sortedElements = sorted(elements.byZ.values()) - return create_table(tabulate(tabular_data=[getAttributes(elem) for elem in sortedElements], - headers=attributes, - tablefmt='rst'), - caption='List of elements') - +.. only:: html + + For specific data on nuclides within each element, refer to the + :ref:`nuclide bases summary table <nuclide-bases-table>`. + + .. exec:: + from tabulate import tabulate + from armi.nucDirectory import elements + + attributes = ['z', + 'name', + 'symbol', + 'phase', + 'group', + 'is naturally occurring?', + 'is heavy metal?', + 'num. nuclides',] + + def getAttributes(element): + return [ + f'``{element.z}``', + f'``{element.name}``', + f'``{element.symbol}``', + f'``{element.phase}``', + f'``{element.group}``', + f'``{element.isNaturallyOccurring()}``', + f'``{element.isHeavyMetal()}``', + f'``{len(element.nuclides)}``', + ] + + sortedElements = sorted(elements.byZ.values()) + return create_table(tabulate(tabular_data=[getAttributes(elem) for elem in sortedElements], + headers=attributes, + tablefmt='rst'), + caption='List of elements') """ import os diff --git a/armi/nuclearDataIO/cccc/cccc.py b/armi/nuclearDataIO/cccc/cccc.py index d7623cca4..dd3133481 100644 --- a/armi/nuclearDataIO/cccc/cccc.py +++ b/armi/nuclearDataIO/cccc/cccc.py @@ -34,10 +34,10 @@ container data types, e.g. list or matrix, relying on the child implementation of the literal types that the container possesses. The binary conversion is implemented in :py:class:`BinaryRecordReader` and - :py:class`BinaryRecordWriter`. The ASCII conversion is implemented in + :py:class:`BinaryRecordWriter`. The ASCII conversion is implemented in :py:class:`AsciiRecordReader` and :py:class:`AsciiRecordWriter`. - These :py:class`IORecord` classes are used within :py:class:`Stream` objects + These :py:class:`IORecord` classes are used within :py:class:`Stream` objects for the data conversion. :py:class:`Stream` is a context manager that opens a file for reading or writing on the ``__enter__`` and closes that file upon ``__exit__``. :py:class:`Stream` is an abstract base class that is diff --git a/armi/nuclearDataIO/xsCollections.py b/armi/nuclearDataIO/xsCollections.py index 02a263909..c8116ed92 100644 --- a/armi/nuclearDataIO/xsCollections.py +++ b/armi/nuclearDataIO/xsCollections.py @@ -794,7 +794,7 @@ def computeMacroscopicGroupConstants( This function computes the macroscopic cross sections of a specified reaction type from inputted microscopic cross sections and number densities. The ``constantName`` parameter specifies what type of - reaction is requested. The ``numberDensities`` parameter is dictionary + reaction is requested. The ``numberDensities`` parameter is a dictionary mapping the nuclide to its number density. The ``lib`` parameter is a library object like :py:class:`~armi.nuclearDataIO.xsLibraries.IsotxsLibrary` or :py:class:`~armi.nuclearDataIO.xsLibraries.CompxsLibrary` that holds the diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py index 435f108db..2a36161cd 100644 --- a/armi/reactor/blocks.py +++ b/armi/reactor/blocks.py @@ -1713,7 +1713,7 @@ def createHomogenizedCopy(self, pinSpatialLocators=False): This method creates and returns a homogenized representation of itself in the form of a new Block. The homogenization occurs in the following manner. A single Hexagon Component is created - add added to the new Block. This Hexagon Component is given the + and added to the new Block. This Hexagon Component is given the :py:class:`armi.materials.mixture._Mixture` material and a volume averaged temperature (``getAverageTempInC``). The number densities of the original Block are also stored on this new Component (:need:`I_ARMI_CMP_GET_NDENS`). Several parameters from the original block diff --git a/armi/reactor/components/component.py b/armi/reactor/components/component.py index 033a948d2..4cebed503 100644 --- a/armi/reactor/components/component.py +++ b/armi/reactor/components/component.py @@ -129,7 +129,7 @@ class ComponentType(composites.CompositeModelType): system. """ - TYPES = dict() + TYPES = dict() #: :meta hide-value: NON_DIMENSION_NAMES = ( "Tinput", diff --git a/armi/reactor/composites.py b/armi/reactor/composites.py index 147d323d4..e086777cb 100644 --- a/armi/reactor/composites.py +++ b/armi/reactor/composites.py @@ -249,8 +249,12 @@ class CompositeModelType(resolveCollections.ResolveParametersMeta): collection of all defined subclasses, called TYPES. """ - # Dictionary mapping class name -> class object for all subclasses TYPES: Dict[str, Type] = dict() + """ + Dictionary mapping class name to class object for all subclasses. + + :meta hide-value: + """ def __new__(cls, name, bases, attrs): newType = resolveCollections.ResolveParametersMeta.__new__( diff --git a/armi/reactor/converters/blockConverters.py b/armi/reactor/converters/blockConverters.py index 046aa6fb0..62f7873f4 100644 --- a/armi/reactor/converters/blockConverters.py +++ b/armi/reactor/converters/blockConverters.py @@ -758,7 +758,7 @@ def radiiFromHexSides(sideLengths): def radiiFromRingOfRods(distToRodCenter, numRods, rodRadii, layout="hexagon"): - """ + r""" Return list of radii from ring of rods. Parameters @@ -778,17 +778,24 @@ def radiiFromRingOfRods(distToRodCenter, numRods, rodRadii, layout="hexagon"): Notes ----- There are two assumptions when making circles: - 1) the rings are concentric about the radToRodCenter; - 2) the ring area of the fuel rods are distributed to the inside and outside rings with the same thickness. - thicknessOnEachSide (t) is calculated as follows: - r1 = inner rad that thickness is added to on inside - r2 = outer rad that thickness is added to on outside - radToRodCenter = (r1 + r2) / 2.0 due to being concentric; - Total Area = Area of annulus 1 + Area of annulus 2 - Area of annulus 1 = pi * r1 ** 2 - pi * (r1 - t) ** 2 - Area of annulus 2 = pi * (r2 + t) ** 2 - pi * r2 ** 2 - Solving for thicknessOnEachSide(t): - t = Total Area / (4 * pi * radToRodCenter) + + #. The rings are concentric about the ``radToRodCenter``. + #. The ring area of the fuel rods are distributed to the inside and outside + rings with the same thickness. ``thicknessOnEachSide`` (:math:`t`) is calculated + as follows: + + .. math:: + :nowrap: + + \begin{aligned} + r_1 &\equiv \text{inner rad that thickness is added to on inside} \\ + r_2 &\equiv \text{outer rad that thickness is added to on outside} \\ + \texttt{radToRodCenter} &= \frac{r_1 + r_2}{2} \text{(due to being concentric)} \\ + \text{Total Area} &= \text{Area of annulus 1} + \text{Area of annulus 2} \\ + \text{Area of annulus 1} &= \pi r_1^2 - \pi (r_1 - t)^2 \\ + \text{Area of annulus 2} &= \pi (r_2 + t)^2 - \pi r_2^2 \\ + t &= \frac{\text{Total Area}}{4\pi\times\texttt{radToRodCenter}} + \end{aligned} """ if layout == "polygon": alpha = 2.0 * math.pi / float(numRods) diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index 394d82211..cb85e4553 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -261,10 +261,10 @@ class Core(composites.Composite): A :py:class:`Core <armi.reactor.reactors.Core>` object is typically a child of a :py:class:`Reactor <armi.reactor.reactors.Reactor>` object. A Reactor can contain multiple objects of the Core type. The instance - attribute name ``r.core`` is reserved for the object representating the + attribute name ``r.core`` is reserved for the object representing the active core. A reactor may also have a spent fuel pool instance attribute, ``r.sfp``, which is also of type - :py:class:`core <armi.reactor.reactors.Core>`. + :py:class:`Core <armi.reactor.reactors.Core>`. Most of the operations to retrieve information from the ARMI reactor data model are mediated through Core objects. For example, @@ -832,7 +832,7 @@ def getNumRings(self, indexBased=False): This method determines the number of rings in the reactor. If the setting ``circularRingMode`` is enabled (by default it is false), the assemblies will be grouped into roughly circular rings based on - their positions and the number of circular rings is reteurned. + their positions and the number of circular rings is returned. Otherwise, the number of hex rings is returned. This parameter is mostly used to facilitate certain fuel management strategies where the fuel is categorized and moved based on ring indexing. diff --git a/doc/user/inputs.rst b/doc/user/inputs.rst index 5a49e303f..32a7b43e6 100644 --- a/doc/user/inputs.rst +++ b/doc/user/inputs.rst @@ -1548,9 +1548,7 @@ through ``self.cs``. def looks_like_path(s): """Super quick, not robust, check if a string looks like a file path.""" - if s.startswith("\\\\") or s.startswith("//"): - return True - elif s[1:].startswith(":\\") + if s.startswith("\\\\") or s.startswith("//") or s[1:].startswith(":\\"): return True return False @@ -1568,7 +1566,7 @@ through ``self.cs``. content += ' - {}\n'.format(' '.join(wrapper.wrap(str(setting.description) or ''))) default = str(getattr(setting, 'default', None)).split("/")[-1] options = str(getattr(setting,'options','') or '') - if looks_like_path(defaults): + if looks_like_path(default): # We don't want to display default file paths in this table. default = "" options = "" From 0668e8ca1f52c67dc7074c25a0723e1073ddce4d Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:15:29 -0800 Subject: [PATCH 169/176] Fixing docstring example formatting (#1638) --- armi/apps.py | 15 ++++++++------- armi/interfaces.py | 4 +--- armi/nuclearDataIO/cccc/compxs.py | 1 - armi/nuclearDataIO/cccc/nhflux.py | 2 +- armi/nuclearDataIO/xsCollections.py | 16 ++++++++-------- armi/nuclearDataIO/xsNuclides.py | 8 ++++---- armi/physics/neutronics/crossSectionSettings.py | 4 ++-- armi/reactor/assemblies.py | 10 +++++----- armi/reactor/blocks.py | 2 +- armi/reactor/converters/geometryConverters.py | 4 ++-- armi/reactor/converters/uniformMesh.py | 10 +++++----- armi/reactor/reactors.py | 8 ++++---- armi/reactor/zones.py | 8 ++++---- .../settings/fwSettings/tightCouplingSettings.py | 2 +- armi/utils/mathematics.py | 2 +- 15 files changed, 47 insertions(+), 49 deletions(-) diff --git a/armi/apps.py b/armi/apps.py index 3020cbef0..22b620239 100644 --- a/armi/apps.py +++ b/armi/apps.py @@ -19,13 +19,14 @@ Framework for a specific application. An ``App`` implements a simple interface for customizing much of the Framework's behavior. -.. admonition:: Historical Fun Fact - - This pattern is used by many frameworks as a way of encapsulating what would - otherwise be global state. The ARMI Framework has historically made heavy use of - global state (e.g., :py:mod:`armi.nucDirectory.nuclideBases`), and it will take - quite a bit of effort to refactor the code to access such things through an App - object. +Notes +----- +Historical Fun Fact + +This pattern is used by many frameworks as a way of encapsulating what would otherwise be global +state. The ARMI Framework has historically made heavy use of global state (e.g., +:py:mod:`armi.nucDirectory.nuclideBases`), and it will take quite a bit of effort to refactor the +code to access such things through an App object. """ # ruff: noqa: E402 from typing import Dict, Optional, Tuple, List diff --git a/armi/interfaces.py b/armi/interfaces.py index 65a3bacf1..873621872 100644 --- a/armi/interfaces.py +++ b/armi/interfaces.py @@ -21,9 +21,7 @@ See Also -------- armi.operators : Schedule calls to various interfaces - armi.plugins : Register various interfaces - """ import copy from typing import Union @@ -362,7 +360,7 @@ def preDistributeState(self): Examples -------- - return {'neutronsPerFission',self.neutronsPerFission} + >>> return {'neutronsPerFission',self.neutronsPerFission} """ return {} diff --git a/armi/nuclearDataIO/cccc/compxs.py b/armi/nuclearDataIO/cccc/compxs.py index 0ddc6d24e..6256347eb 100644 --- a/armi/nuclearDataIO/cccc/compxs.py +++ b/armi/nuclearDataIO/cccc/compxs.py @@ -46,7 +46,6 @@ Examples -------- -:: >>> from armi.nuclearDataIO import compxs >>> lib = compxs.readBinary('COMPXS') >>> r0 = lib.regions[0] diff --git a/armi/nuclearDataIO/cccc/nhflux.py b/armi/nuclearDataIO/cccc/nhflux.py index ac6e7d66b..87a5ae64d 100644 --- a/armi/nuclearDataIO/cccc/nhflux.py +++ b/armi/nuclearDataIO/cccc/nhflux.py @@ -413,7 +413,7 @@ def _rwGeodstCoordMap2D(self): Examples -------- - geodstCoordMap[NodalIndex] = geodstIndex + geodstCoordMap[NodalIndex] = geodstIndex See Also -------- diff --git a/armi/nuclearDataIO/xsCollections.py b/armi/nuclearDataIO/xsCollections.py index c8116ed92..ed1a70f6f 100644 --- a/armi/nuclearDataIO/xsCollections.py +++ b/armi/nuclearDataIO/xsCollections.py @@ -27,14 +27,14 @@ Examples -------- -# creating a MicroscopicXSCollection by loading one from ISOTXS. -microLib = armi.nuclearDataIO.ISOTXS('ISOTXS') -micros = myLib.nuclides['U235AA'].micros - -# creating macroscopic XS: -mc = MacroscopicCrossSectionCreator() -macroCollection = mc.createMacrosFromMicros(microLib, block) -blocksWithMacros = mc.createMacrosOnBlocklist(microLib, blocks) + # creating a MicroscopicXSCollection by loading one from ISOTXS. + microLib = armi.nuclearDataIO.ISOTXS('ISOTXS') + micros = myLib.nuclides['U235AA'].micros + + # creating macroscopic XS: + mc = MacroscopicCrossSectionCreator() + macroCollection = mc.createMacrosFromMicros(microLib, block) + blocksWithMacros = mc.createMacrosOnBlocklist(microLib, blocks) """ import numpy diff --git a/armi/nuclearDataIO/xsNuclides.py b/armi/nuclearDataIO/xsNuclides.py index 9ed04267a..e11296a7e 100644 --- a/armi/nuclearDataIO/xsNuclides.py +++ b/armi/nuclearDataIO/xsNuclides.py @@ -14,11 +14,11 @@ r""" This module contains cross section nuclides, which are a wrapper around the -:py:class:`~armi.nucDirectory.nuclideBases.INuclide` objects. The cross section nuclide objects contain -cross section information from a specific calculation (e.g. neutron, or gamma cross sections). +:py:class:`~armi.nucDirectory.nuclideBases.INuclide` objects. The cross section nuclide objects +contain cross section information from a specific calculation (e.g. neutron, or gamma cross sections). -:py:class:`XSNuclide` objects also contain meta data from the original file, so that another file can be -reconstructed. +:py:class:`XSNuclide` objects also contain meta data from the original file, so that another file +can be reconstructed. .. warning:: :py:class:`XSNuclide` objects should only be created by reading data into diff --git a/armi/physics/neutronics/crossSectionSettings.py b/armi/physics/neutronics/crossSectionSettings.py index 5cdbdf55b..439cf7abf 100644 --- a/armi/physics/neutronics/crossSectionSettings.py +++ b/armi/physics/neutronics/crossSectionSettings.py @@ -87,8 +87,8 @@ def getStr(cls, typeSpec: Enum): Examples -------- - XSGeometryTypes.getStr(XSGeometryTypes.ZERO_DIMENSIONAL) == "0D" - XSGeometryTypes.getStr(XSGeometryTypes.TWO_DIMENSIONAL_HEX) == "2D hex" + XSGeometryTypes.getStr(XSGeometryTypes.ZERO_DIMENSIONAL) == "0D" + XSGeometryTypes.getStr(XSGeometryTypes.TWO_DIMENSIONAL_HEX) == "2D hex" """ geometryTypes = list(cls) if typeSpec not in geometryTypes: diff --git a/armi/reactor/assemblies.py b/armi/reactor/assemblies.py index bd27ae65b..8051a82f7 100644 --- a/armi/reactor/assemblies.py +++ b/armi/reactor/assemblies.py @@ -864,8 +864,8 @@ def getBlocksAndZ(self, typeSpec=None, returnBottomZ=False, returnTopZ=False): Examples -------- - for block, bottomZ in a.getBlocksAndZ(returnBottomZ=True): - print({0}'s bottom mesh point is {1}'.format(block, bottomZ)) + for block, bottomZ in a.getBlocksAndZ(returnBottomZ=True): + print({0}'s bottom mesh point is {1}'.format(block, bottomZ)) """ if returnBottomZ and returnTopZ: raise ValueError("Both returnTopZ and returnBottomZ are set to `True`") @@ -982,9 +982,9 @@ def getBlocksBetweenElevations(self, zLower, zUpper): Examples -------- If the block structure looks like: - 50.0 to 100.0 Block3 - 25.0 to 50.0 Block2 - 0.0 to 25.0 Block1 + 50.0 to 100.0 Block3 + 25.0 to 50.0 Block2 + 0.0 to 25.0 Block1 Then, diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py index 2a36161cd..97296c974 100644 --- a/armi/reactor/blocks.py +++ b/armi/reactor/blocks.py @@ -2049,7 +2049,7 @@ def rotatePins(self, rotNum, justCompute=False): Examples -------- - rotateIndexLookup[i_after_rotation-1] = i_before_rotation-1 + rotateIndexLookup[i_after_rotation-1] = i_before_rotation-1 """ if not 0 <= rotNum <= 5: raise ValueError( diff --git a/armi/reactor/converters/geometryConverters.py b/armi/reactor/converters/geometryConverters.py index 3b5d0e40d..a2a24f7f5 100644 --- a/armi/reactor/converters/geometryConverters.py +++ b/armi/reactor/converters/geometryConverters.py @@ -1399,8 +1399,8 @@ class EdgeAssemblyChanger(GeometryChanger): Examples -------- - edgeChanger = EdgeAssemblyChanger() - edgeChanger.removeEdgeAssemblies(reactor.core) + edgeChanger = EdgeAssemblyChanger() + edgeChanger.removeEdgeAssemblies(reactor.core) """ def addEdgeAssemblies(self, core): diff --git a/armi/reactor/converters/uniformMesh.py b/armi/reactor/converters/uniformMesh.py index 260414239..8b8e3c129 100644 --- a/armi/reactor/converters/uniformMesh.py +++ b/armi/reactor/converters/uniformMesh.py @@ -41,11 +41,11 @@ Examples -------- -converter = uniformMesh.NeutronicsUniformMeshConverter() -converter.convert(reactor) -uniformReactor = converter.convReactor -# do calcs, then: -converter.applyStateToOriginal() + converter = uniformMesh.NeutronicsUniformMeshConverter() + converter.convert(reactor) + uniformReactor = converter.convReactor + # do calcs, then: + converter.applyStateToOriginal() The mesh mapping happens as described in the figure: diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index cb85e4553..653d96b92 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -2585,10 +2585,10 @@ def buildManualZones(self, cs): -------- Manual zones will be defined in a special string format, e.g.: - zoneDefinitions: - - ring-1: 001-001 - - ring-2: 002-001, 002-002 - - ring-3: 003-001, 003-002, 003-003 + >>> zoneDefinitions: + >>> - ring-1: 001-001 + >>> - ring-2: 002-001, 002-002 + >>> - ring-3: 003-001, 003-002, 003-003 Notes ----- diff --git a/armi/reactor/zones.py b/armi/reactor/zones.py index 2d27b5fb0..163ddce27 100644 --- a/armi/reactor/zones.py +++ b/armi/reactor/zones.py @@ -446,10 +446,10 @@ def summary(self) -> None: Examples -------- - zoneDefinitions: - - ring-1: 001-001 - - ring-2: 002-001, 002-002 - - ring-3: 003-001, 003-002, 003-003 + zoneDefinitions: + - ring-1: 001-001 + - ring-2: 002-001, 002-002 + - ring-3: 003-001, 003-002, 003-003 """ # log a quick header runLog.info("zoneDefinitions:") diff --git a/armi/settings/fwSettings/tightCouplingSettings.py b/armi/settings/fwSettings/tightCouplingSettings.py index 983c40c5d..77be098b0 100644 --- a/armi/settings/fwSettings/tightCouplingSettings.py +++ b/armi/settings/fwSettings/tightCouplingSettings.py @@ -49,7 +49,7 @@ class TightCouplingSettings(dict): Examples -------- - couplingSettings = TightCouplingSettings({'globalFlux': {'parameter': 'keff', 'convergence': 1e-05}}) + couplingSettings = TightCouplingSettings({'globalFlux': {'parameter': 'keff', 'convergence': 1e-05}}) """ def __repr__(self): diff --git a/armi/utils/mathematics.py b/armi/utils/mathematics.py index 7b4b3d0dc..afff43f4d 100644 --- a/armi/utils/mathematics.py +++ b/armi/utils/mathematics.py @@ -84,7 +84,7 @@ def convertToSlice(x, increment=False): Examples -------- - a = np.array([10, 11, 12, 13]) + >>> a = np.array([10, 11, 12, 13]) >>> convertToSlice(2) slice(2, 3, None) From b23fc745af3dcd498388a6116219c64c9c1ffdb6 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Wed, 31 Jan 2024 09:14:42 -0800 Subject: [PATCH 170/176] Improving the formatting of some broken See Also's (#1642) --- armi/nuclearDataIO/cccc/nhflux.py | 148 +++++++++--------- armi/reactor/composites.py | 30 ++-- .../converters/axialExpansionChanger.py | 90 ++++++----- 3 files changed, 140 insertions(+), 128 deletions(-) diff --git a/armi/nuclearDataIO/cccc/nhflux.py b/armi/nuclearDataIO/cccc/nhflux.py index 87a5ae64d..7d740cf95 100644 --- a/armi/nuclearDataIO/cccc/nhflux.py +++ b/armi/nuclearDataIO/cccc/nhflux.py @@ -58,82 +58,81 @@ class NHFLUX(cccc.DataContainer): """ - An abstraction of a NHFLUX file. This format is defined in the DIF3D manual. Note - that the format for DIF3D-Nodal and DIF3D-VARIANT are not the same. The VARIANT - NHFLUX format has recently changed, so this reader is only compatible with files - produced by v11.0 of the solver. - - .. warning:: - DIF3D outputs NHFLUX at every time node, but REBUS outputs NHFLUX only at every cycle. - - See also [VARIANT-95]_ and [VARIANT-2014]_. - - .. [VARIANT-95] G. Palmiotti, E. E. Lewis, and C. B. Carrico, VARIANT: VARIational - Anisotropic Nodal Transport for Multidimensional Cartesian and Hexagonal Geometry - Calculation, ANL-95/40, Argonne National Laboratory, Argonne, IL (October 1995). - - .. [VARIANT-2014] Smith, M. A., Lewis, E. E., and Shemon, E. R. DIF3D-VARIANT 11.0: A - Decade of Updates. United States: N. p., 2014. Web. doi:10.2172/1127298. - https://publications.anl.gov/anlpubs/2014/04/78313.pdf + An abstraction of a NHFLUX file. This format is defined in the DIF3D manual. Note that the + format for DIF3D-Nodal and DIF3D-VARIANT are not the same. The VARIANT NHFLUX format has + recently changed, so this reader is only compatible with files produced by v11.0 of the solver. Attributes ---------- metadata : file control - The NHFLUX file control info (sort of global for this library). This is the contents - of the 1D data block on the file. + The NHFLUX file control info (sort of global for this library). This is the contents of the + 1D data block on the file. incomingPointersToAllAssemblies: 2-D list of floats - This is an index map for the "internal surfaces" between DIF3D nodal - indexing and DIF3D GEODST indexing. It can be used to process incoming partial - currents. This uses the same ordering as the geodstCoordMap attribute. + This is an index map for the "internal surfaces" between DIF3D nodal indexing and DIF3D + GEODST indexing. It can be used to process incoming partial currents. This uses the same + ordering as the geodstCoordMap attribute. externalCurrentPointers : list of ints - This is an index map for the "external surfaces" between DIF3D nodal - indexing and DIF3D GEODST indexing. "External surfaces" are important because they - contain the INCOMING partial currents from the outer reactor boundary. This uses - the same ordering as geodstCoordMap, except that each assembly now has multiple - subsequent indices. For example, for a hexagonal core, if hex of index n (0 to N-1) - has a surface of index k (0 to 5) that lies on the vacuum boundary, then the index - of that surface is N*6 + k + 1. + This is an index map for the "external surfaces" between DIF3D nodal indexing and DIF3D + GEODST indexing. "External surfaces" are important because they contain the INCOMING partial + currents from the outer reactor boundary. This uses the same ordering as geodstCoordMap, + except that each assembly now has multiple subsequent indices. For example, for a hexagonal + core, if hex of index n (0 to N-1) has a surface of index k (0 to 5) that lies on the vacuum + boundary, then the index of that surface is N*6 + k + 1. geodstCoordMap : list of ints - This is an index map between DIF3D nodal and DIF3D GEODST. It is - necessary for interpreting the ordering of flux and partial current data in the - NHFLUX file. Note that this mapping between DIF3D-Nodal and DIF3D-VARIANT is not - the same. + This is an index map between DIF3D nodal and DIF3D GEODST. It is necessary for interpreting + the ordering of flux and partial current data in the NHFLUX file. Note that this mapping + between DIF3D-Nodal and DIF3D-VARIANT is not the same. outgoingPCSymSeCPointers: list of ints - This is an index map for the outpgoing partial currents on the symmetric and sector - lateral boundary. It is only present for DIF3D-VARIANT for hexagonal cores. + This is an index map for the outpgoing partial currents on the symmetric and sector lateral + boundary. It is only present for DIF3D-VARIANT for hexagonal cores. ingoingPCSymSeCPointers: list of ints - This is an index map for the ingoing (or incoming) partial currents on the symmetric - and sector lateral boundary. It is only present for DIF3D-VARIANT for hexagonal cores. + This is an index map for the ingoing (or incoming) partial currents on the symmetric and + sector lateral boundary. It is only present for DIF3D-VARIANT for hexagonal cores. fluxMomentsAll : 4-D list of floats - This contains all the flux moments for all core assemblies. The jth planar flux moment - of assembly i in group g in axial node k is fluxMoments[i][k][j][g]. The - assemblies are ordered according to the geodstCoordMap attribute. For DIF3D-VARIANT, - this includes both even and odd parity moments. + This contains all the flux moments for all core assemblies. The jth planar flux moment of + assembly i in group g in axial node k is fluxMoments[i][k][j][g]. The assemblies are ordered + according to the geodstCoordMap attribute. For DIF3D-VARIANT, this includes both even and + odd parity moments. partialCurrentsHexAll : 5-D list of floats This contains all the OUTGOING partial currents for all core assemblies. The OUTGOING partial current on surface j in assembly i in axial node k in group g is partialCurrentsHex[i][k][j][g][m], where m=0. The assemblies are ordered according to the - geodstCoordMap attribute. For DIF3D-VARIANT, higher-order data is available for the - m axis. + geodstCoordMap attribute. For DIF3D-VARIANT, higher-order data is available for the m axis. partialCurrentsHex_extAll : 4-D list of floats - This contains all the INCOMING partial currents on "external surfaces", which are - adjacent to the reactor outer boundary (usually vacuum). Internal reflective surfaces - are NOT included in this! These "external surfaces" are ordered according to - externalCurrentPointers. For DIF3D-VARIANT, higher-order data is available for the - last axis. + This contains all the INCOMING partial currents on "external surfaces", which are adjacent + to the reactor outer boundary (usually vacuum). Internal reflective surfaces are NOT + included in this! These "external surfaces" are ordered according to + externalCurrentPointers. For DIF3D-VARIANT, higher-order data is available for the last + axis. partialCurrentsZAll : 5-D list of floats - This contains all the upward and downward partial currents for all core assemblies - The assemblies are ordered according to the geodstCoordMap attribute. For DIF3D-VARIANT, - higher-order data is available for the last axis. + This contains all the upward and downward partial currents for all core assemblies. The + assemblies are ordered according to the geodstCoordMap attribute. For DIF3D-VARIANT, higher- + order data is available for the last axis. + + Warning + ------- + DIF3D outputs NHFLUX at every time node, but REBUS outputs NHFLUX only at every cycle. + + See Also + -------- + [VARIANT-95]_ and [VARIANT-2014]_. + + .. [VARIANT-95] G. Palmiotti, E. E. Lewis, and C. B. Carrico, VARIANT: VARIational Anisotropic + Nodal Transport for Multidimensional Cartesian and Hexagonal Geometry Calculation, ANL-95/40, + Argonne National Laboratory, Argonne, IL (October 1995). + + .. [VARIANT-2014] Smith, M. A., Lewis, E. E., and Shemon, E. R. DIF3D-VARIANT 11.0: A Decade of + Updates. United States: N. p., 2014. Web. doi:10.2172/1127298. + https://publications.anl.gov/anlpubs/2014/04/78313.pdf """ def __init__(self, fName="NHFLUX", variant=False, numDataSetsToRead=1): @@ -146,8 +145,8 @@ def __init__(self, fName="NHFLUX", variant=False, numDataSetsToRead=1): Filename of the NHFLUX binary file to be read. variant : bool, optional - Whether or not this NHFLUX/NAFLUX file has the DIF3D-VARIANT output format, which - is different than the DIF3D-Nodal format. + Whether or not this NHFLUX/NAFLUX file has the DIF3D-VARIANT output format, which is + different than the DIF3D-Nodal format. """ cccc.DataContainer.__init__(self) @@ -181,8 +180,8 @@ def fluxMoments(self): def partialCurrentsHex(self): """ For DIF3D-Nodal, this property is almost always equivalent to the attribute - `partialCurrentsHex`. For DIF3D-VARIANT, this property returns the zeroth-order - moment of the outgoing radial currents. + ``partialCurrentsHex``. For DIF3D-VARIANT, this property returns the zeroth-order moment of + the outgoing radial currents. Read-only property (there is no setter). """ @@ -217,11 +216,11 @@ def _getDataContainer() -> NHFLUX: return NHFLUX() def readWrite(self): - r""" + """ Read everything from the DIF3D binary file NHFLUX. - Read all surface-averaged partial currents, all planar moments, and the DIF3D - nodal coordinate mapping system. + Read all surface-averaged partial currents, all planar moments, and the DIF3D nodal + coordinate mapping system. Notes ----- @@ -231,18 +230,17 @@ def readWrite(self): Parameters ---------- numDataSetsToRead : int, optional - The number of whole-core flux data sets included in this NHFLUX/NAFLUX file - that one wishes to be read. Some NHFLUX/NAFLUX files, such as NAFLUX files - written by SASSYS/DIF3D-K, contain more than one flux data set. Each data set - overwrites the previous one on the NHFLUX class object, which will contain only the - numDataSetsToRead-th data set. The first numDataSetsToRead-1 data sets are essentially - skipped over. + The number of whole-core flux data sets included in this NHFLUX/NAFLUX file that one + wishes to be read. Some NHFLUX/NAFLUX files, such as NAFLUX files written by + SASSYS/DIF3D-K, contain more than one flux data set. Each data set overwrites the + previous one on the NHFLUX class object, which will contain only the + ``numDataSetsToRead-th`` data set. The first numDataSetsToRead-1 data sets are + essentially skipped over. """ self._rwFileID() self._rwBasicFileData1D() - # This control info only exists for VARIANT. We can only process entries with 0 - # or 1. + # This control info only exists for VARIANT. We can only process entries with 0 or 1. if self._metadata["variantFlag"] and self._metadata["iwnhfl"] == 2: msg = ( "This reader can only read VARIANT NHFLUX files where 'iwnhfl'=0 (both " @@ -619,9 +617,9 @@ def _rwZPartialCurrents5D(self, surfCurrents): return surfCurrents def _getEnergyGroupIndex(self, g): - r""" - Real fluxes stored in NHFLUX have "normal" (or "forward") energy groups. - Also see the subclass method NAFLUX.getEnergyGroupIndex(). + """ + Real fluxes stored in NHFLUX have "normal" (or "forward") energy groups. Also see the + subclass method NAFLUX.getEnergyGroupIndex(). """ return g @@ -634,7 +632,7 @@ class NafluxStream(NhfluxStream): """ def _getEnergyGroupIndex(self, g): - r"""Adjoint fluxes stored in NAFLUX have "reversed" (or "backward") energy groups.""" + """Adjoint fluxes stored in NAFLUX have "reversed" (or "backward") energy groups.""" ng = self._metadata["ngroup"] return ng - g - 1 @@ -645,7 +643,7 @@ class NhfluxStreamVariant(NhfluxStream): Notes ----- - Can be deleted after have the NHFLUX data container be the public interface + Can be deleted after have the NHFLUX data container be the public interface. """ @staticmethod @@ -659,7 +657,7 @@ class NafluxStreamVariant(NafluxStream): Notes ----- - Can be deleted after have the NHFLUX data container be the public interface + Can be deleted after have the NHFLUX data container be the public interface. """ @staticmethod @@ -668,9 +666,9 @@ def _getDataContainer() -> NHFLUX: def getNhfluxReader(adjointFlag, variantFlag): - r""" - Returns the appropriate DIF3D nodal flux binary file reader class, - either NHFLUX (real) or NAFLUX (adjoint). + """ + Returns the appropriate DIF3D nodal flux binary file reader class, either NHFLUX (real) or + NAFLUX (adjoint). """ if adjointFlag: reader = NafluxStreamVariant if variantFlag else NafluxStream diff --git a/armi/reactor/composites.py b/armi/reactor/composites.py index e086777cb..1b34d39df 100644 --- a/armi/reactor/composites.py +++ b/armi/reactor/composites.py @@ -16,18 +16,21 @@ This module contains the basic composite pattern underlying the reactor package. This follows the principles of the `Composite Design Pattern -<https://en.wikipedia.org/wiki/Composite_pattern>`_ to allow the construction of a -part/whole hierarchy representing a physical nuclear reactor. The composite objects act -somewhat like lists: they can be indexed, iterated over, appended, extended, inserted, -etc. Each member of the hierarchy knows its children and its parent, so full access to -the hierarchy is available from everywhere. This design was chosen because of the close -analogy of the model to the physical nature of nuclear reactors. - -.. warning:: Because each member of the hierarchy is linked to the entire tree, - it is often unsafe to save references to individual members; it can cause - large and unexpected memory inefficiencies. - -See Also: :doc:`/developer/index`. +<https://en.wikipedia.org/wiki/Composite_pattern>`_ to allow the construction of a part/whole +hierarchy representing a physical nuclear reactor. The composite objects act somewhat like lists: +they can be indexed, iterated over, appended, extended, inserted, etc. Each member of the hierarchy +knows its children and its parent, so full access to the hierarchy is available from everywhere. +This design was chosen because of the close analogy of the model to the physical nature of nuclear +reactors. + +Warning +------- +Because each member of the hierarchy is linked to the entire tree, it is often unsafe to save +references to individual members; it can cause large and unexpected memory inefficiencies. + +See Also +-------- +:doc:`/developer/index`. """ import collections import itertools @@ -566,7 +569,6 @@ def doChildrenHaveFlags(self, typeSpec: TypeSpec, deep=False): ---------- typeSpec : TypeSpec Requested type of the child - """ for c in self.getChildren(deep): if c.hasFlags(typeSpec, exact=False): @@ -587,7 +589,6 @@ def containsAtLeastOneChildWithFlags(self, typeSpec: TypeSpec): -------- self.doChildrenHaveFlags self.containsOnlyChildrenWithFlags - """ return any(self.doChildrenHaveFlags(typeSpec)) @@ -604,7 +605,6 @@ def containsOnlyChildrenWithFlags(self, typeSpec: TypeSpec): -------- self.doChildrenHaveFlags self.containsAtLeastOneChildWithFlags - """ return all(self.doChildrenHaveFlags(typeSpec)) diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py index 33b52b220..a9eda0989 100644 --- a/armi/reactor/converters/axialExpansionChanger.py +++ b/armi/reactor/converters/axialExpansionChanger.py @@ -197,19 +197,22 @@ def performThermalAxialExpansion( setFuel: bool = True, expandFromTinputToThot: bool = False, ): - """Perform thermal expansion/contraction for an assembly given an axial temperature grid and field. + """Perform thermal expansion/contraction for an assembly given an axial temperature grid and + field. - .. impl:: Perform thermal expansion/contraction, given an axial temperature distribution over an assembly. + .. impl:: Perform thermal expansion/contraction, given an axial temperature distribution + over an assembly. :id: I_ARMI_AXIAL_EXP_THERM :implements: R_ARMI_AXIAL_EXP_THERM - This method performs component-wise thermal expansion for an assembly given a discrete temperature - distribution over the axial length of the Assembly. In ``setAssembly``, the Assembly is prepared - for axial expansion by determining Component-wise axial linkage and checking to see if a dummy Block - is in place (necessary for ensuring conservation properties). The discrete temperature distribution - is then leveraged to update Component temperatures and compute thermal expansion factors - (via ``updateComponentTempsBy1DTempField`` and ``computeThermalExpansionFactors``, respectively). - Finally, the axial expansion is performed in ``axiallyExpandAssembly``. + This method performs component-wise thermal expansion for an assembly given a discrete + temperature distribution over the axial length of the Assembly. In ``setAssembly``, the + Assembly is prepared for axial expansion by determining Component-wise axial linkage and + checking to see if a dummy Block is in place (necessary for ensuring conservation + properties). The discrete temperature distribution is then leveraged to update Component + temperatures and compute thermal expansion factors (via + ``updateComponentTempsBy1DTempField`` and ``computeThermalExpansionFactors``, + respectively). Finally, the axial expansion is performed in ``axiallyExpandAssembly``. Parameters ---------- @@ -251,10 +254,11 @@ def setAssembly(self, a, setFuel=True, expandFromTinputToThot=False): Notes ----- - When considering thermal expansion, if there is an axial temperature distribution on the assembly, - the axial expansion methodology will NOT perfectly preseve mass. The magnitude of the gradient of - the temperature distribution is the primary factor in determining the cumulative loss of mass conservation. - Additional details will be documented in :ref:`axialExpansion` of the documentation. + When considering thermal expansion, if there is an axial temperature distribution on the + assembly, the axial expansion methodology will NOT perfectly preseve mass. The magnitude of + the gradient of the temperature distribution is the primary factor in determining the + cumulative loss of mass conservation. Additional details will be documented in + :ref:`axialExpansion` of the documentation. """ self.linked = AssemblyAxialLinkage(a) self.expansionData = ExpansionData( @@ -360,7 +364,8 @@ def axiallyExpandAssembly(self): b.p.z = b.p.zbottom + b.getHeight() / 2.0 _checkBlockHeight(b) - # call component.clearCache to update the component volume, and therefore the masses, of all solid components. + # Call Component.clearCache to update the Component volume, and therefore the masses, + # of all solid components. for c in getSolidComponents(b): c.clearCache() # redo mesh -- functionality based on assembly.calculateZCoords() @@ -443,18 +448,19 @@ class AssemblyAxialLinkage: reference to original assembly; is directly modified/changed during expansion. linkedBlocks : dict - keys --> :py:class:`Block <armi.reactor.blocks.Block>` - - values --> list of axially linked blocks; index 0 = lower linked block; index 1: upper linked block. - - see also: self._getLinkedBlocks() + - keys = :py:class:`Block <armi.reactor.blocks.Block>` + - values = list of axially linked blocks; index 0 = lower linked block; index 1: upper + linked block. linkedComponents : dict - keys --> :py:class:`Component <armi.reactor.components.component.Component>` - - values --> list of axially linked components; index 0 = lower linked component; index 1: upper linked component. + - keys = :py:class:`Component <armi.reactor.components.component.Component>` + - values = list of axially linked components; index 0 = lower linked component; + index 1: upper linked component. - see also: self._getLinkedComponents + See Also + -------- + - self._getLinkedComponents + - self._getLinkedBlocks() """ def __init__(self, StdAssem): @@ -550,8 +556,8 @@ def _getLinkedComponents(self, b, c): 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}" + " This is indicative of an error in the blueprints! Linked " + f"components found are {lstLinkedC[ib]} and {otherC}" ) runLog.error(msg=errMsg) raise RuntimeError(errMsg) @@ -583,9 +589,10 @@ def _determineLinked(componentA, componentB): 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. + - 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. @@ -603,8 +610,8 @@ def _determineLinked(componentA, componentB): if isinstance(componentA, UnshapedComponent): 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, " + "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, ) @@ -681,12 +688,18 @@ def setExpansionFactors(self, componentLst: List, expFrac: List): ) raise RuntimeError if 0.0 in expFrac: - msg = "An expansion fraction, L1/L0, equal to 0.0, is not physical. Expansion fractions should be greater than 0.0." + msg = ( + "An expansion fraction, L1/L0, equal to 0.0, is not physical. Expansion fractions " + "should be greater than 0.0." + ) runLog.error(msg) raise RuntimeError(msg) for exp in expFrac: if exp < 0.0: - msg = "A negative expansion fraction, L1/L0, is not physical. Expansion fractions should be greater than 0.0." + msg = ( + "A negative expansion fraction, L1/L0, is not physical. Expansion fractions " + "should be greater than 0.0." + ) runLog.error(msg) raise RuntimeError(msg) for c, p in zip(componentLst, expFrac): @@ -762,7 +775,7 @@ def computeThermalExpansionFactors(self): for b in self._a: for c in getSolidComponents(b): if self.expandFromTinputToThot: - # get thermal expansion factor between c.inputTemperatureInC and c.temperatureInC + # get thermal expansion factor between c.inputTemperatureInC & c.temperatureInC self._expansionFactors[c] = c.getThermalExpansionFactor() elif c in self.componentReferenceTemperature: growFrac = c.getThermalExpansionFactor( @@ -770,9 +783,9 @@ def computeThermalExpansionFactors(self): ) self._expansionFactors[c] = growFrac else: - # we want expansion factors relative to componentReferenceTemperature not Tinput. - # But for this component there isn't a componentReferenceTemperature, - # so we'll assume that the expansion factor is 1.0. + # We want expansion factors relative to componentReferenceTemperature not + # Tinput. But for this component there isn't a componentReferenceTemperature, so + # we'll assume that the expansion factor is 1.0. self._expansionFactors[c] = 1.0 def getExpansionFactor(self, c): @@ -810,7 +823,8 @@ def _setTargetComponents(self, setFuel): self.determineTargetComponent(b) def determineTargetComponent(self, b, flagOfInterest=None): - """Determines target component, stores it on the block, and appends it to self._componentDeterminesBlockHeight. + """Determines target component, stores it on the block, and appends it to + self._componentDeterminesBlockHeight. Parameters ---------- @@ -833,7 +847,7 @@ def determineTargetComponent(self, b, flagOfInterest=None): multiple target components found """ if flagOfInterest is None: - # Follow expansion of most neutronically important component, fuel first then control/poison + # Follow expansion of most neutronically important component, fuel then control/poison for targetFlag in TARGET_FLAGS_IN_PREFERRED_ORDER: componentWFlag = [c for c in b.getChildren() if c.hasFlags(targetFlag)] if componentWFlag != []: From d40204592fd0541d29da86821df6f9d8c690a040 Mon Sep 17 00:00:00 2001 From: Tony Alberti <aalberti@terrapower.com> Date: Wed, 31 Jan 2024 14:49:13 -0800 Subject: [PATCH 171/176] Fixing pural/singular grammar issue in docstring (#1643) --- armi/reactor/assemblies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armi/reactor/assemblies.py b/armi/reactor/assemblies.py index 8051a82f7..c59295819 100644 --- a/armi/reactor/assemblies.py +++ b/armi/reactor/assemblies.py @@ -271,7 +271,7 @@ def getArea(self): :implements: R_ARMI_ASSEM_DIMS Returns the area of the first block in the Assembly. If there are no - block in the Assembly, a warning is issued and a default area of 1.0 + blocks in the Assembly, a warning is issued and a default area of 1.0 is returned. """ try: From f6e1c78d1ed4b3a061ca8f633d47be8cb11c58ca Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 1 Feb 2024 09:39:57 -0800 Subject: [PATCH 172/176] Fixing broken impl tag (#1644) --- armi/physics/fuelCycle/fuelHandlers.py | 40 ++++++++++++-------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/armi/physics/fuelCycle/fuelHandlers.py b/armi/physics/fuelCycle/fuelHandlers.py index 3479b8fbf..9e1daadc8 100644 --- a/armi/physics/fuelCycle/fuelHandlers.py +++ b/armi/physics/fuelCycle/fuelHandlers.py @@ -841,33 +841,31 @@ def _transferStationaryBlocks(self, assembly1, assembly2): def dischargeSwap(self, incoming, outgoing): """Removes one assembly from the core and replace it with another assembly. - Parameters - ---------- - incoming : :py:class:`Assembly <armi.reactor.assemblies.Assembly>` - The assembly getting swapped into the core. - outgoing : :py:class:`Assembly <armi.reactor.assemblies.Assembly>` - The assembly getting discharged out the core. - .. impl:: User-specified blocks can be left in place for the discharge swap. :id: I_ARMI_SHUFFLE_STATIONARY1 :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 from the outgoing - assembly remain in the original core position, while the stationary - blocks from the incoming assembly are discharged with the outgoing assembly. + Once this process is complete, the actual assembly movement can take place. Through this + process, the stationary blocks from the outgoing assembly remain in the original core + position, while the stationary blocks from the incoming assembly are discharged with the + outgoing assembly. + + Parameters + ---------- + incoming : :py:class:`Assembly <armi.reactor.assemblies.Assembly>` + The assembly getting swapped into the core. + outgoing : :py:class:`Assembly <armi.reactor.assemblies.Assembly>` + The assembly getting discharged out the core. See Also -------- From 571e4f58f364229b754f11b56d10ef2c153b92c9 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 6 Feb 2024 10:21:43 -0800 Subject: [PATCH 173/176] Enforcing a line length of 140 characters (#1637) --- armi/bookkeeping/report/newReportUtils.py | 7 +- armi/bookkeeping/report/reportingUtils.py | 5 +- armi/interfaces.py | 5 +- armi/materials/uraniumOxide.py | 9 +- armi/materials/zr.py | 6 +- armi/nucDirectory/nuclideBases.py | 6 +- armi/operators/settingsValidation.py | 19 ++- armi/physics/fuelCycle/fuelHandlers.py | 5 +- .../neutronics/crossSectionGroupManager.py | 64 ++++++---- .../lumpedFissionProduct.py | 4 +- .../isotopicDepletion/crossSectionTable.py | 89 +++++++------ armi/physics/neutronics/parameters.py | 5 +- armi/reactor/__init__.py | 23 +++- armi/reactor/assemblies.py | 7 +- armi/reactor/blueprints/isotopicOptions.py | 106 ++++++++-------- armi/reactor/composites.py | 105 ++++++++-------- armi/reactor/converters/geometryConverters.py | 113 +++++++++-------- armi/reactor/reactors.py | 119 ++++++++---------- pyproject.toml | 7 +- 19 files changed, 377 insertions(+), 327 deletions(-) diff --git a/armi/bookkeeping/report/newReportUtils.py b/armi/bookkeeping/report/newReportUtils.py index 8fcd43faa..9b8bf0554 100644 --- a/armi/bookkeeping/report/newReportUtils.py +++ b/armi/bookkeeping/report/newReportUtils.py @@ -638,7 +638,12 @@ def insertCoreAndAssemblyMaps( } core = r.core - imageCaption = "The axial block and enrichment distributions of assemblies in the core at beginning of life. The percentage represents the block enrichment (U-235 or B-10), where as the additional character represents the cross section id of the block. The number of fine-mesh subdivisions are provided on the secondary y-axis." + imageCaption = ( + "The axial block and enrichment distributions of assemblies in the core at beginning of " + + "life. The percentage represents the block enrichment (U-235 or B-10), where as the " + + "additional character represents the cross section id of the block. The number of fine-" + + "mesh subdivisions are provided on the secondary y-axis." + ) report[DESIGN]["Assembly Designs"] = newReports.Section("Assembly Designs") currentSection = report[DESIGN]["Assembly Designs"] diff --git a/armi/bookkeeping/report/reportingUtils.py b/armi/bookkeeping/report/reportingUtils.py index 864519282..3a0cfcf3b 100644 --- a/armi/bookkeeping/report/reportingUtils.py +++ b/armi/bookkeeping/report/reportingUtils.py @@ -193,7 +193,10 @@ def _writeMachineInformation(): ) # If this is on Windows: run sys info on each unique node too if "win" in sys.platform: - sysInfoCmd = 'systeminfo | findstr /B /C:"OS Name" /B /C:"OS Version" /B /C:"Processor" && systeminfo | findstr /E /C:"Mhz"' + sysInfoCmd = ( + 'systeminfo | findstr /B /C:"OS Name" /B /C:"OS Version" /B ' + '/C:"Processor" && systeminfo | findstr /E /C:"Mhz"' + ) out = subprocess.run( sysInfoCmd, capture_output=True, text=True, shell=True ) diff --git a/armi/interfaces.py b/armi/interfaces.py index 873621872..2855274b4 100644 --- a/armi/interfaces.py +++ b/armi/interfaces.py @@ -129,7 +129,10 @@ def __init__(self, param, tolerance, maxIters): self.eps = numpy.inf def __repr__(self): - return f"<{self.__class__.__name__}, Parameter: {self.parameter}, Convergence Criteria: {self.tolerance}, Maximum Coupled Iterations: {self.maxIters}>" + return ( + f"<{self.__class__.__name__}, Parameter: {self.parameter}, Convergence Criteria: " + + f"{self.tolerance}, Maximum Coupled Iterations: {self.maxIters}>" + ) def storePreviousIterationValue(self, val: _SUPPORTED_TYPES): """ diff --git a/armi/materials/uraniumOxide.py b/armi/materials/uraniumOxide.py index 6d6a689b2..64ce7f1b0 100644 --- a/armi/materials/uraniumOxide.py +++ b/armi/materials/uraniumOxide.py @@ -62,8 +62,10 @@ class UraniumOxide(material.FuelMaterial, material.SimpleSolid): } references = { - "thermal conductivity": "Thermal conductivity of uranium dioxide by nonequilibrium molecular dynamics simulation. S. Motoyama. Physical Review B, Volume 60, Number 1, July 1999", - "linear expansion": "Thermophysical Properties of MOX and UO2 Fuels Including the Effects of Irradiation. S.G. Popov, et.al. Oak Ridge National Laboratory. ORNL/TM-2000/351", + "thermal conductivity": "Thermal conductivity of uranium dioxide by nonequilibrium molecular dynamics " + + "simulation. S. Motoyama. Physical Review B, Volume 60, Number 1, July 1999", + "linear expansion": "Thermophysical Properties of MOX and UO2 Fuels Including the Effects of Irradiation. " + + "S.G. Popov, et.al. Oak Ridge National Laboratory. ORNL/TM-2000/351", "heat capacity": "ORNL/TM-2000/351", } @@ -73,7 +75,8 @@ class UraniumOxide(material.FuelMaterial, material.SimpleSolid): ) # Thermal conductivity values taken from: - # Thermal conductivity of uranium dioxide by nonequilibrium molecular dynamics simulation. S. Motoyama. Physical Review B, Volume 60, Number 1, July 1999 + # Thermal conductivity of uranium dioxide by nonequilibrium molecular dynamics simulation. S. Motoyama. + # Physical Review B, Volume 60, Number 1, July 1999 thermalConductivityTableK = [ 300, 600, diff --git a/armi/materials/zr.py b/armi/materials/zr.py index f5616e841..eca912d61 100644 --- a/armi/materials/zr.py +++ b/armi/materials/zr.py @@ -33,8 +33,10 @@ class Zr(Material): references = { "density": "AAA Materials Handbook 45803", "thermal conductivity": "AAA Fuels handbook. ANL", - "linear expansion": "Y.S. Touloukian, R.K. Kirby, R.E. Taylor and P.D. Desai, Thermal Expansion, Thermophysical Properties of Matter, Vol. 12, IFI/Plenum, New York-Washington (1975)", - "linear expansion percent": "Y.S. Touloukian, R.K. Kirby, R.E. Taylor and P.D. Desai, Thermal Expansion, Thermophysical Properties of Matter, Vol. 12, IFI/Plenum, New York-Washington (1975)", + "linear expansion": "Y.S. Touloukian, R.K. Kirby, R.E. Taylor and P.D. Desai, Thermal Expansion, " + + "Thermophysical Properties of Matter, Vol. 12, IFI/Plenum, New York-Washington (1975)", + "linear expansion percent": "Y.S. Touloukian, R.K. Kirby, R.E. Taylor and P.D. Desai, Thermal Expansion, " + + "Thermophysical Properties of Matter, Vol. 12, IFI/Plenum, New York-Washington (1975)", } linearExpansionTableK = [ diff --git a/armi/nucDirectory/nuclideBases.py b/armi/nucDirectory/nuclideBases.py index 1ccdaf290..172342dc2 100644 --- a/armi/nucDirectory/nuclideBases.py +++ b/armi/nucDirectory/nuclideBases.py @@ -564,7 +564,11 @@ def __init__(self, element, a, weight, abundance, state, halflife): ) def __repr__(self): - return f"<{self.__class__.__name__} {self.name}: Z:{self.z}, A:{self.a}, S:{self.state}, W:{self.weight:<12.6e}, Label:{self.label}>, HL:{self.halflife:<15.11e}, Abund:{self.abundance:<8.6e}>" + return ( + f"<{self.__class__.__name__} {self.name}: Z:{self.z}, A:{self.a}, S:{self.state}, " + + f"W:{self.weight:<12.6e}, Label:{self.label}>, HL:{self.halflife:<15.11e}, " + + f"Abund:{self.abundance:<8.6e}>" + ) @staticmethod def _createName(element, a, state): diff --git a/armi/operators/settingsValidation.py b/armi/operators/settingsValidation.py index 3defb5f24..6680b01b0 100644 --- a/armi/operators/settingsValidation.py +++ b/armi/operators/settingsValidation.py @@ -468,16 +468,16 @@ def _inspectSettings(self): ) def _willBeCopiedFrom(fName): - for copyFile in self.cs["copyFilesFrom"]: - if fName == os.path.split(copyFile)[1]: - return True - return False + return any( + fName == os.path.split(copyFile)[1] + for copyFile in self.cs["copyFilesFrom"] + ) self.addQuery( lambda: self.cs["explicitRepeatShuffles"] and not self._csRelativePathExists(self.cs["explicitRepeatShuffles"]) and not _willBeCopiedFrom(self.cs["explicitRepeatShuffles"]), - "The specified repeat shuffle file `{0}` does not exist, and won't be copied from elsewhere. " + "The specified repeat shuffle file `{0}` does not exist, and won't be copied. " "Run will crash.".format(self.cs["explicitRepeatShuffles"]), "", self.NO_ACTION, @@ -640,11 +640,10 @@ def decayCyclesHaveInputThatWillBeIgnored(): except: # noqa: bare-except return True - for pf, af in zip(powerFracs, availabilities): - if pf > 0.0 and af == 0.0: - # this will be a full decay step and any power fraction will be ignored. May be ok, but warn. - return True - return False + # This will be a full decay step and any power fraction will be ignored. May be ok. + return any( + pf > 0.0 and af == 0.0 for pf, af in zip(powerFracs, availabilities) + ) self.addQuery( lambda: ( diff --git a/armi/physics/fuelCycle/fuelHandlers.py b/armi/physics/fuelCycle/fuelHandlers.py index 9e1daadc8..e4f8f2986 100644 --- a/armi/physics/fuelCycle/fuelHandlers.py +++ b/armi/physics/fuelCycle/fuelHandlers.py @@ -1035,7 +1035,10 @@ def readMoves(fname): elif "assembly" in line: # this is the new load style where an actual assembly type is written to the shuffle logic # due to legacy reasons, the assembly type will be put into group 4 - pat = r"([A-Za-z0-9!\-]+) moved to ([A-Za-z0-9!\-]+) with assembly type ([A-Za-z0-9!\s]+)\s*(ANAME=\S+)?\s*with enrich list: (.+)" + pat = ( + r"([A-Za-z0-9!\-]+) moved to ([A-Za-z0-9!\-]+) with assembly type " + + r"([A-Za-z0-9!\s]+)\s*(ANAME=\S+)?\s*with enrich list: (.+)" + ) m = re.search(pat, line) if not m: raise InputError( diff --git a/armi/physics/neutronics/crossSectionGroupManager.py b/armi/physics/neutronics/crossSectionGroupManager.py index 54028511a..f93d24cd7 100644 --- a/armi/physics/neutronics/crossSectionGroupManager.py +++ b/armi/physics/neutronics/crossSectionGroupManager.py @@ -855,14 +855,18 @@ def __init__(self, r, cs): def interactBOL(self): """Called at the Beginning-of-Life of a run, before any cycles start. - .. impl:: The lattice physics interface and cross-section group manager are connected at BOL. + .. impl:: The lattice physics interface and cross-section group manager are connected at + BOL. :id: I_ARMI_XSGM_FREQ0 :implements: R_ARMI_XSGM_FREQ - This method sets the cross-section block averaging method and and logic for whether all blocks in a cross section group should be used when generating - a representative block. Furthermore, if the control logic for lattice physics frequency updates is set at beginning-of-life (`BOL`) through the :py:class:`LatticePhysicsInterface <armi.physics.neutronics.latticePhysics>`, - the cross-section group manager will construct representative blocks for each cross-section IDs at the beginning of the reactor state. - + This method sets the cross-section block averaging method and and logic for whether all + blocks in a cross section group should be used when generating a representative block. + Furthermore, if the control logic for lattice physics frequency updates is set at + beginning-of-life (`BOL`) through the :py:class:`LatticePhysicsInterface + <armi.physics.neutronics.latticePhysics>`, the cross-section group manager will + construct representative blocks for each cross-section IDs at the beginning of the + reactor state. """ # now that all cs settings are loaded, apply defaults to compound XS settings from armi.physics.neutronics.settings import CONF_XS_BLOCK_REPRESENTATION @@ -885,13 +889,17 @@ def interactBOC(self, cycle=None): """ Update representative blocks and block burnup groups. - .. impl:: The lattice physics interface and cross-section group manager are connected at BOC. + .. impl:: The lattice physics interface and cross-section group manager are connected at + BOC. :id: I_ARMI_XSGM_FREQ1 :implements: R_ARMI_XSGM_FREQ - This method updates representative blocks and block burnups at the beginning-of-cycle for each cross-section ID if the control logic - for lattice physics frequency updates is set at beginning-of-cycle (`BOC`) through the :py:class:`LatticePhysicsInterface <armi.physics.neutronics.latticePhysics>`. - At the beginning-of-cycle, the cross-section group manager will construct representative blocks for each cross-section IDs for the current reactor state. + This method updates representative blocks and block burnups at the beginning-of-cycle + for each cross-section ID if the control logic for lattice physics frequency updates is + set at beginning-of-cycle (`BOC`) through the :py:class:`LatticePhysicsInterface + <armi.physics.neutronics.latticePhysics>`. At the beginning-of-cycle, the cross-section + group manager will construct representative blocks for each cross-section IDs for the + current reactor state. Notes ----- @@ -910,35 +918,45 @@ def interactEOC(self, cycle=None): def interactEveryNode(self, cycle=None, tn=None): """Interaction at every time node. - .. impl:: The lattice physics interface and cross-section group manager are connected at every time node. + .. impl:: The lattice physics interface and cross-section group manager are connected at + every time node. :id: I_ARMI_XSGM_FREQ2 :implements: R_ARMI_XSGM_FREQ - This method updates representative blocks and block burnups at every node for each cross-section ID if the control logic - for lattices physics frequency updates is set for every node (`everyNode`) through the :py:class:`LatticePhysicsInterface <armi.physics.neutronics.latticePhysics>`. - At every node, the cross-section group manager will construct representative blocks for each cross-section ID in the current reactor state. - + This method updates representative blocks and block burnups at every node for each + cross-section ID if the control logic for lattices physics frequency updates is set for + every node (`everyNode`) through the :py:class:`LatticePhysicsInterface + <armi.physics.neutronics.latticePhysics>`. At every node, the cross-section group + manager will construct representative blocks for each cross-section ID in the current + reactor state. """ if self._latticePhysicsFrequency >= LatticePhysicsFrequency.everyNode: self.createRepresentativeBlocks() def interactCoupled(self, iteration): - """Update cross-section groups on each physics coupling iteration to get latest temperatures. + """Update cross-section groups on each physics coupling iteration to get latest + temperatures. - .. impl:: The lattice physics interface and cross-section group manager are connected during coupling. + .. impl:: The lattice physics interface and cross-section group manager are connected + during coupling. :id: I_ARMI_XSGM_FREQ3 :implements: R_ARMI_XSGM_FREQ - This method updates representative blocks and block burnups at every node and the first coupled iteration for each cross-section ID - if the control logic for lattices physics frequency updates is set for the first coupled iteration (``firstCoupledIteration``) through the :py:class:`LatticePhysicsInterface <armi.physics.neutronics.latticePhysics>`. - The cross-section group manager will construct representative blocks for each cross-section ID at the first iteration of every time node. + This method updates representative blocks and block burnups at every node and the first + coupled iteration for each cross-section ID if the control logic for lattices physics + frequency updates is set for the first coupled iteration (``firstCoupledIteration``) + through the + :py:class:`LatticePhysicsInterface <armi.physics.neutronics.latticePhysics>`. + The cross-section group manager will construct representative blocks for each + cross-section ID at the first iteration of every time node. Notes ----- - Updating the cross-section on only the first (i.e., iteration == 0) timenode can be a reasonable approximation to - get new cross sections with some temperature updates but not have to run lattice physics on each - coupled iteration. If the user desires to have the cross sections updated with every coupling iteration, - the ``latticePhysicsFrequency: all`` option. + Updating the cross-section on only the first (i.e., iteration == 0) timenode can be a + reasonable approximation to get new cross sections with some temperature updates but not + have to run lattice physics on each coupled iteration. If the user desires to have the + cross sections updated with every coupling iteration, the ``latticePhysicsFrequency: all`` + option. See Also -------- diff --git a/armi/physics/neutronics/fissionProductModel/lumpedFissionProduct.py b/armi/physics/neutronics/fissionProductModel/lumpedFissionProduct.py index d937b81b2..0a3f849ad 100644 --- a/armi/physics/neutronics/fissionProductModel/lumpedFissionProduct.py +++ b/armi/physics/neutronics/fissionProductModel/lumpedFissionProduct.py @@ -413,7 +413,8 @@ def _buildMo99LumpedFissionProduct(): for lfp in nuclideBases.where( lambda nb: isinstance(nb, nuclideBases.LumpNuclideBase) ): - # Not all lump nuclides bases defined are fission products, so ensure that only fission products are considered. + # Not all lump nuclides bases defined are fission products, so ensure that only fission + # products are considered. if not ("FP" in lfp.name or "REGN" in lfp.name): continue mo99FP = LumpedFissionProduct(lfp.name) @@ -424,6 +425,7 @@ def _buildMo99LumpedFissionProduct(): def isGas(nuc): """True if nuclide is considered a gas.""" + # ruff: noqa: SIM110 for element in elements.getElementsByChemicalPhase(elements.ChemicalPhase.GAS): if element == nuc.element: return True diff --git a/armi/physics/neutronics/isotopicDepletion/crossSectionTable.py b/armi/physics/neutronics/isotopicDepletion/crossSectionTable.py index efec30290..4120de81f 100644 --- a/armi/physics/neutronics/isotopicDepletion/crossSectionTable.py +++ b/armi/physics/neutronics/isotopicDepletion/crossSectionTable.py @@ -15,12 +15,11 @@ """ Module containing the CrossSectionTable class. -The CrossSectionTable is useful for performing isotopic depletion analysis by storing -one-group cross sections of interest to such an analysis. This used to live alongside -the isotopicDepletionInterface, but that proved to be an unpleasant coupling between the -ARMI composite model and the physics code contained therein. Separating it out at least -means that the composite model doesn't need to import the isotopicDepletionInterface to -function. +The CrossSectionTable is useful for performing isotopic depletion analysis by storing one-group +cross sections of interest to such an analysis. This used to live alongside the +isotopicDepletionInterface, but that proved to be an unpleasant coupling between the ARMI composite +model and the physics code contained therein. Separating it out at least means that the composite +model doesn't need to import the isotopicDepletionInterface to function. """ import collections from typing import List @@ -116,10 +115,10 @@ def addMultiGroupXS(self, nucName, microMultiGroupXS, mgFlux, totalFlux=None): def hasValues(self): """Determines if there are non-zero values in this cross section table.""" - for nuclideCrossSectionSet in self.values(): - if any(nuclideCrossSectionSet.values()): - return True - return False + return any( + any(nuclideCrossSectionSet.values()) + for nuclideCrossSectionSet in self.values() + ) def getXsecTable( self, @@ -133,26 +132,25 @@ def getXsecTable( :id: I_ARMI_DEPL_TABLES1 :implements: R_ARMI_DEPL_TABLES - Loops over the reaction rates stored as ``self`` to produce a string with - the cross sections for each nuclide in the block. Cross sections may be - populated by :py:meth:`~armi.physics.neutronics.isotopicDepletion.crossSectionTable.makeReactionRateTable`. + Loops over the reaction rates stored as ``self`` to produce a string with the cross + sections for each nuclide in the block. Cross sections may be populated by + :py:meth:`~armi.physics.neutronics.isotopicDepletion.crossSectionTable.makeReactionRateTable` - The string will have a header with the table's name formatted according - to ``headerFormat`` followed by rows for each unique nuclide/reaction - combination, where each line is formatted according to ``tableFormat``. + The string will have a header with the table's name formatted according to + ``headerFormat`` followed by rows for each unique nuclide/reaction combination, where + each line is formatted according to ``tableFormat``. Parameters ---------- headerFormat: string (optional) - this is the format in which the elements of the header with be returned - -- i.e. if you use a .format() call with the case name you'll return a - formatted list of string elements + This is the format in which the elements of the header with be returned -- i.e. if you + use a .format() call with the case name you'll return a formatted list of strings. tableFormat: string (optional) - this is the format in which the elements of the table with be returned - -- i.e. if you use a .format() call with mcnpId, nG, nF, n2n, n3n, nA, - and nP you'll get the format you want. If you use a .format() call with the case name you'll return a - formatted list of string elements + This is the format in which the elements of the table with be returned -- i.e. if you + use a .format() call with mcnpId, nG, nF, n2n, n3n, nA, and nP you'll get the format you + want. If you use a .format() call with the case name you'll return a formatted list of + string elements Results ------- @@ -175,33 +173,30 @@ def makeReactionRateTable(obj, nuclides: List = None): Often useful in support of depletion. - .. impl:: Generate a reaction rate table with entries for (nG), (nF), (n2n), (nA), and (nP) reactions. + .. impl:: Generate a reaction rate table with entries for (nG), (nF), (n2n), (nA), and (nP) + reactions. :id: I_ARMI_DEPL_TABLES0 :implements: R_ARMI_DEPL_TABLES - For a given composite object ``obj`` and a list of nuclides ``nuclides`` - in that object, call ``obj.getReactionRates()`` for each nuclide with a ``nDensity`` parameter of 1.0. - If ``nuclides`` is not specified, use a list of all nuclides in ``obj``. - This will reach upwards through the parents of ``obj`` to the associated - :py:class:`~armi.reactor.reactors.Core` object and pull the ISOTXS library - that is stored there. If ``obj`` does not belong to a ``Core``, a warning - is printed. - - For each child of ``obj``, use the ISOTXS library and the cross-section ID for the associated block - to produce a reaction rate dictionary in units of inverse seconds for - the nuclide specified in the original call to ``obj.getReactionRates()``. - Because ``nDensity`` was originally specified as - 1.0, this dictionary actually represents the reaction rates per unit volume. - If the nuclide is not in the ISOTXS library, a warning is printed. - - Combine the reaction rates for all nuclides into a combined dictionary by - summing together reaction rates of the same type on the same isotope from - each of the children of ``obj``. - - If ``obj`` has a non-zero multi-group flux, sum the group-wise flux into - the total flux and normalize the reaction rates by the total flux, producing - a one-group macroscopic cross section for each reaction type on each - nuclide. Store these values in a + For a given composite object ``obj`` and a list of nuclides ``nuclides`` in that object, + call ``obj.getReactionRates()`` for each nuclide with a ``nDensity`` parameter of 1.0. If + ``nuclides`` is not specified, use a list of all nuclides in ``obj``. This will reach + upwards through the parents of ``obj`` to the associated + :py:class:`~armi.reactor.reactors.Core` object and pull the ISOTXS library that is stored + there. If ``obj`` does not belong to a ``Core``, a warning is printed. + + For each child of ``obj``, use the ISOTXS library and the cross-section ID for the + associated block to produce a reaction rate dictionary in units of inverse seconds for the + nuclide specified in the original call to ``obj.getReactionRates()``. Because ``nDensity`` + was originally specified as 1.0, this dictionary actually represents the reaction rates per + unit volume. If the nuclide is not in the ISOTXS library, a warning is printed. + + Combine the reaction rates for all nuclides into a combined dictionary by summing together + reaction rates of the same type on the same isotope from each of the children of ``obj``. + + If ``obj`` has a non-zero multi-group flux, sum the group-wise flux into the total flux and + normalize the reaction rates by the total flux, producing a one-group macroscopic cross + section for each reaction type on each nuclide. Store these values in a :py:class:`~armi.physics.neutronics.isotopicDepletion.crossSectionTable.CrossSectionTable`. Parameters diff --git a/armi/physics/neutronics/parameters.py b/armi/physics/neutronics/parameters.py index 07b4bb552..269598980 100644 --- a/armi/physics/neutronics/parameters.py +++ b/armi/physics/neutronics/parameters.py @@ -542,7 +542,10 @@ def _getNeutronicsBlockParams(): pb.defParam( "kInf", units=units.UNITLESS, - description="Neutron production rate in this block/neutron absorption rate in this block. Not truly kinf but a reasonable approximation of reactivity.", + description=( + "Neutron production rate in this block/neutron absorption rate in this " + "block. Not truly kinf but a reasonable approximation of reactivity." + ), ) pb.defParam( diff --git a/armi/reactor/__init__.py b/armi/reactor/__init__.py index dc3b1fdd8..a25977afc 100644 --- a/armi/reactor/__init__.py +++ b/armi/reactor/__init__.py @@ -20,7 +20,28 @@ .. _reactor-class-diagram: -.. pyreverse:: armi.reactor -A -k --ignore=complexShapes.py,grids.py,componentParameters.py,dodecaShapes.py,volumetricShapes.py,tests,converters,blockParameters.py,assemblyParameters.py,reactorParameters.py,batchParameters.py,basicShapes.py,shapes.py,zones.py,parameters,flags.py,geometry.py,blueprints,batch.py,assemblyLists.py,plugins.py +.. pyreverse:: armi.reactor -A -k --ignore= + assemblyLists.py, + assemblyParameters.py, + basicShapes.py, + batch.py, + batchParameters.py, + blockParameters.py, + blueprints, + complexShapes.py, + componentParameters.py, + converters, + dodecaShapes.py, + flags.py, + geometry.py, + grids.py, + parameters, + plugins.py, + reactorParameters.py, + shapes.py, + tests, + volumetricShapes.py, + zones.py :align: center :alt: Reactor class diagram :width: 90% diff --git a/armi/reactor/assemblies.py b/armi/reactor/assemblies.py index c59295819..77eadca41 100644 --- a/armi/reactor/assemblies.py +++ b/armi/reactor/assemblies.py @@ -889,10 +889,9 @@ def getBlocksAndZ(self, typeSpec=None, returnBottomZ=False, returnTopZ=False): return zip(blocks, zCoords) def hasContinuousCoolantChannel(self): - for b in self.getBlocks(): - if not b.containsAtLeastOneChildWithFlags(Flags.COOLANT): - return False - return True + return all( + b.containsAtLeastOneChildWithFlags(Flags.COOLANT) for b in self.getBlocks() + ) def getFirstBlock(self, typeSpec=None, exact=False): bs = self.getBlocks(typeSpec, exact=exact) diff --git a/armi/reactor/blueprints/isotopicOptions.py b/armi/reactor/blueprints/isotopicOptions.py index c4784fed2..077db9f21 100644 --- a/armi/reactor/blueprints/isotopicOptions.py +++ b/armi/reactor/blueprints/isotopicOptions.py @@ -44,41 +44,38 @@ class NuclideFlag(yamlize.Object): """ Defines whether or not each nuclide is included in the burn chain and cross sections. - Also controls which nuclides get expanded from elementals to isotopics - and which natural isotopics to exclude (if any). Oftentimes, cross section - library creators include some natural isotopes but not all. For example, - it is common to include O16 but not O17 or O18. Each code has slightly - different interpretations of this so we give the user full control here. + Also controls which nuclides get expanded from elementals to isotopics and which natural + isotopics to exclude (if any). Oftentimes, cross section library creators include some natural + isotopes but not all. For example, it is common to include O16 but not O17 or O18. Each code has + slightly different interpretations of this so we give the user full control here. We also try to provide useful defaults. - There are lots of complications that can arise in these choices. - It makes reasonable sense to use elemental compositions - for things that are typically used without isotopic modifications - (Fe, O, Zr, Cr, Na). If we choose to expand some or all of these - to isotopics at initialization based on cross section library - requirements, a single case will work fine with a given lattice - physics option. However, restarting from that case with different - cross section needs is challenging. + There are lots of complications that can arise in these choices. It makes reasonable sense to + use elemental compositions for things that are typically used without isotopic modifications + (Fe, O, Zr, Cr, Na). If we choose to expand some or all of these to isotopics at initialization + based on cross section library requirements, a single case will work fine with a given lattice + physics option. However, restarting from that case with different cross section needs is + challenging. .. impl:: The blueprint object that represents a nuclide flag. :id: I_ARMI_BP_NUC_FLAGS1 :implements: R_ARMI_BP_NUC_FLAGS - This class creates a yaml interface for the user to specify in their blueprints - which isotopes should be depleted. It is incorporated into the "nuclide flags" - section of a blueprints file by being included as key-value pairs within - the :py:class:`~armi.reactor.blueprints.isotopicOptions.NuclideFlags` class, - which is in turn included into the overall blueprints within - :py:class:`~armi.reactor.blueprints.Blueprints`. + This class creates a yaml interface for the user to specify in their blueprints which + isotopes should be depleted. It is incorporated into the "nuclide flags" section of a + blueprints file by being included as key-value pairs within the + :py:class:`~armi.reactor.blueprints.isotopicOptions.NuclideFlags` class, which is in turn + included into the overall blueprints within :py:class:`~armi.reactor.blueprints.Blueprints`. - This class includes a boolean ``burn`` attribute which can be specified - for any nuclide. This attribute is examined by the :py:meth:`~armi.reactor.blueprints.isotopicOptions.NuclideFlag.fileAsActiveOrInert` - method to sort the nuclides into sets of depletable or not, which is typically - called during construction of assemblies in :py:meth:`~armi.reactor.blueprints.Blueprints.constructAssem`. + This class includes a boolean ``burn`` attribute which can be specified for any nuclide. + This attribute is examined by the + :py:meth:`~armi.reactor.blueprints.isotopicOptions.NuclideFlag.fileAsActiveOrInert` method + to sort the nuclides into sets of depletable or not, which is typically called during + construction of assemblies in :py:meth:`~armi.reactor.blueprints.Blueprints.constructAssem`. - Note that while the ``burn`` attribute can be set by the user in the blueprints, - other methods may also set it based on case settings (see, for instance, + Note that while the ``burn`` attribute can be set by the user in the blueprints, other + methods may also set it based on case settings (see, for instance, :py:func:`~armi.reactor.blueprints.isotopicOptions.genDefaultNucFlags`, :py:func:`~armi.reactor.blueprints.isotopicOptions.autoUpdateNuclideFlags`, and :py:func:`~armi.reactor.blueprints.isotopicOptions.getAllNuclideBasesByLibrary`). @@ -88,18 +85,15 @@ class NuclideFlag(yamlize.Object): nuclideName : str The name of the nuclide burn : bool - True if this nuclide should be added to the burn chain. - If True, all reachable nuclides via transmutation - and decay must be included as well. + True if this nuclide should be added to the burn chain. If True, all reachable nuclides via + transmutation and decay must be included as well. xs : bool - True if this nuclide should be included in the cross - section libraries. Effectively, if this nuclide is in the problem - at all, this should be true. + True if this nuclide should be included in the cross section libraries. Effectively, if this + nuclide is in the problem at all, this should be true. expandTo : list of str, optional - isotope nuclideNames to expand to. For example, if nuclideName is - ``O`` then this could be ``["O16", "O17"]`` to expand it into - those two isotopes (but not ``O18``). The nuclides will be scaled - up uniformly to account for any missing natural nuclides. + isotope nuclideNames to expand to. For example, if nuclideName is ``O`` then this could be + ``["O16", "O17"]`` to expand it into those two isotopes (but not ``O18``). The nuclides will + be scaled up uniformly to account for any missing natural nuclides. """ nuclideName = yamlize.Attribute(type=str) @@ -166,32 +160,32 @@ class NuclideFlags(yamlize.KeyedList): class CustomIsotopic(yamlize.Map): """ - User specified, custom isotopics input defined by a name (such as MOX), and key/pairs of nuclide names and numeric - values consistent with the ``input format``. + User specified, custom isotopics input defined by a name (such as MOX), and key/pairs of nuclide + names and numeric values consistent with the ``input format``. .. impl:: Certain material modifications will be applied using this code. :id: I_ARMI_MAT_USER_INPUT2 :implements: R_ARMI_MAT_USER_INPUT - Defines a yaml construct that allows the user to define a custom isotopic vector - from within their blueprints file, including a name and key-value pairs - corresponding to nuclide names and their concentrations. - - Relies on the underlying infrastructure from the ``yamlize`` package for - reading from text files, serialization, and internal storage of the data. - - Is implemented as part of a blueprints file by being used in key-value pairs - within the :py:class:`~armi.reactor.blueprints.isotopicOptions.CustomIsotopics` class, - which is imported and used as an attribute within the larger :py:class:`~armi.reactor.blueprints.Blueprints` - class. - - These isotopics are linked to a component during calls to :py:meth:`~armi.reactor.blueprints.componentBlueprint.ComponentBlueprint.construct`, - where the name specified in the ``isotopics`` attribute of the component blueprint - is searched against the available ``CustomIsotopics`` defined in the - "custom isotopics" section of the blueprints. - Once linked, the :py:meth:`~armi.reactor.blueprints.isotopicOptions.CustomIsotopic.apply` - method is called, which adjusts the ``massFrac`` attribute of the component's - material class. + Defines a yaml construct that allows the user to define a custom isotopic vector from within + their blueprints file, including a name and key-value pairs corresponding to nuclide names + and their concentrations. + + Relies on the underlying infrastructure from the ``yamlize`` package for reading from text + files, serialization, and internal storage of the data. + + Is implemented as part of a blueprints file by being used in key-value pairs within the + :py:class:`~armi.reactor.blueprints.isotopicOptions.CustomIsotopics` class, which is + imported and used as an attribute within the larger + :py:class:`~armi.reactor.blueprints.Blueprints` class. + + These isotopics are linked to a component during calls to + :py:meth:`~armi.reactor.blueprints.componentBlueprint.ComponentBlueprint.construct`, where + the name specified in the ``isotopics`` attribute of the component blueprint is searched + against the available ``CustomIsotopics`` defined in the "custom isotopics" section of the + blueprints. Once linked, the + :py:meth:`~armi.reactor.blueprints.isotopicOptions.CustomIsotopic.apply` method is called, + which adjusts the ``massFrac`` attribute of the component's material class. """ key_type = yamlize.Typed(str) diff --git a/armi/reactor/composites.py b/armi/reactor/composites.py index 1b34d39df..15c8618c6 100644 --- a/armi/reactor/composites.py +++ b/armi/reactor/composites.py @@ -247,9 +247,9 @@ class CompositeModelType(resolveCollections.ResolveParametersMeta): """ Metaclass for tracking subclasses of ArmiObject subclasses. - It is often useful to have an easily-accessible collection of all classes that - participate in the ARMI composite reactor model. This metaclass maintains a - collection of all defined subclasses, called TYPES. + It is often useful to have an easily-accessible collection of all classes that participate in + the ARMI composite reactor model. This metaclass maintains a collection of all defined + subclasses, called TYPES. """ TYPES: Dict[str, Type] = dict() @@ -276,32 +276,29 @@ class ArmiObject(metaclass=CompositeModelType): This: * declares the interface for objects in the composition - * implements default behavior for the interface common to all - classes - * Declares an interface for accessing and managing - child objects + * implements default behavior for the interface common to all classes + * Declares an interface for accessing and managing child objects * Defines an interface for accessing parents. - Called "component" in gang of four, this is an ArmiObject here because the word - component was already taken in ARMI. - - The :py:class:`armi.reactor.parameters.ResolveParametersMeta` metaclass is used to - automatically create ``ParameterCollection`` subclasses for storing parameters - associated with any particular subclass of ArmiObject. Defining a ``pDefs`` class - attribute in the definition of a subclass of ArmiObject will lead to the creation of - a new subclass of py:class:`armi.reactor.parameters.ParameterCollection`, which will - contain the definitions from that class's ``pDefs`` as well as the definitions for - all of its parents. A new ``paramCollectionType`` class attribute will be added to - the ArmiObject subclass to reflect which type of parameter collection should be - used. - - .. warning:: - This class has far too many public methods. We are in the midst of a composite - tree cleanup that will likely break these out onto a number of separate functional - classes grouping things like composition, location, shape/dimensions, and - various physics queries. Methods are being collected here from the various - specialized subclasses (Block, Assembly) in preparation for this next step. - As a result, the public API on this method should be considered unstable. + Called "component" in gang of four, this is an ArmiObject here because the word component was + already taken in ARMI. + + The :py:class:`armi.reactor.parameters.ResolveParametersMeta` metaclass is used to automatically + create ``ParameterCollection`` subclasses for storing parameters associated with any particular + subclass of ArmiObject. Defining a ``pDefs`` class attribute in the definition of a subclass of + ArmiObject will lead to the creation of a new subclass of + py:class:`armi.reactor.parameters.ParameterCollection`, which will contain the definitions from + that class's ``pDefs`` as well as the definitions for all of its parents. A new + ``paramCollectionType`` class attribute will be added to the ArmiObject subclass to reflect + which type of parameter collection should be used. + + Warning + ------- + This class has far too many public methods. We are in the midst of a composite tree cleanup that + will likely break these out onto a number of separate functional classes grouping things like + composition, location, shape/dimensions, and various physics queries. Methods are being + collected here from the various specialized subclasses (Block, Assembly) in preparation for this + next step. As a result, the public API on this method should be considered unstable. .. impl:: Parameters are accessible throughout the armi tree. :id: I_ARMI_PARAM_PART @@ -3196,6 +3193,7 @@ def requiresLumpedFissionProducts(self, nuclides=None): if nuclides is None: nuclides = self.getNuclides() + # ruff: noqa: SIM110 for nucName in nuclides: if isinstance(nuclideBases.byName[nucName], nuclideBases.LumpNuclideBase): return True @@ -3338,19 +3336,18 @@ class StateRetainer: """ Retains state during some operations. - This can be used to temporarily cache state, perform an operation, extract some info, and - then revert back to the original state. + This can be used to temporarily cache state, perform an operation, extract some info, and then + revert back to the original state. - * A state retainer is faster than restoring state from a database as it reduces - the number of IO reads; however, it does use more memory. + * A state retainer is faster than restoring state from a database as it reduces the number of IO + reads; however, it does use more memory. * This can be used on any object within the composite pattern via with ``[rabc].retainState([list], [of], [parameters], [to], [retain]):``. Use on an object up in the hierarchy applies to all objects below as well. - * This is intended to work across MPI, so that if you were to broadcast the - reactor the state would be correct; however the exact implication on - ``parameters`` may be unclear. + * This is intended to work across MPI, so that if you were to broadcast the reactor the state + would be correct; however the exact implication on ``parameters`` may be unclear. """ @@ -3364,9 +3361,8 @@ def __init__(self, composite, paramsToApply=None): composite object to retain state (recursively) paramsToApply: iterable of parameters.Parameter - Iterable of parameters.Parameter to retain updated values after `__exit__`. - All other parameters are reverted to the original state, i.e. retained at - the original value. + Iterable of parameters.Parameter to retain updated values after `__exit__`. All other + parameters are reverted to the original state, i.e. retained at the original value. """ self.composite = composite self.paramsToApply = set(paramsToApply or []) @@ -3379,7 +3375,9 @@ def __exit__(self, *args): self._enterExitHelper(lambda obj: obj.restoreBackup(self.paramsToApply)) def _enterExitHelper(self, func): - """Helper method for ``__enter__`` and ``__exit__``. ``func`` is a lambda to either ``backUp()`` or ``restoreBackup()``.""" + """Helper method for ``__enter__`` and ``__exit__``. ``func`` is a lambda to either + ``backUp()`` or ``restoreBackup()``. + """ paramDefs = set() for child in [self.composite] + self.composite.getChildren( deep=True, includeMaterials=True @@ -3401,8 +3399,8 @@ def gatherMaterialsByVolume( Parameters ---------- objects : list of ArmiObject - Objects to look within. This argument allows clients to search though some subset - of the three (e.g. when you're looking for all CLADDING components within FUEL blocks) + Objects to look within. This argument allows clients to search though some subset of the + three (e.g. when you're looking for all CLADDING components within FUEL blocks) typeSpec : TypeSpec Flags for the components to look at @@ -3412,9 +3410,9 @@ def gatherMaterialsByVolume( Notes ----- - This helper method is outside the main ArmiObject tree for the special clients that need - to filter both by container type (e.g. Block type) with one set of flags, and Components - with another set of flags. + This helper method is outside the main ArmiObject tree for the special clients that need to + filter both by container type (e.g. Block type) with one set of flags, and Components with + another set of flags. .. warning:: This is a **composition** related helper method that will likely be filed into classes/modules that deal specifically with the composition of things in the data model. @@ -3439,18 +3437,19 @@ def getDominantMaterial( """ Return the first sample of the most dominant material (by volume) in a set of objects. - .. warning:: This is a **composition** related helper method that will likely be filed into - classes/modules that deal specifically with the composition of things in the data model. - Thus clients that use it from here should expect to need updates soon. + Warning + ------- + This is a **composition** related helper method that will likely be filed into classes/modules + that deal specifically with the composition of things in the data model. Thus clients that use + it from here should expect to need updates soon. """ volumes, samples = gatherMaterialsByVolume(objects, typeSpec, exact) if volumes: # find matName with max volume maxMatName = list(sorted(volumes.items(), key=lambda item: item[1])).pop()[0] - # return this material. Note that if this material - # has properties like Zr-frac, enrichment, etc. then this will - # just return one in the batch, not an average. + # return this material. Note that if this material has properties like Zr-frac, enrichment, + # etc. then this will just return one in the batch, not an average. return samples[maxMatName] return None @@ -3461,13 +3460,13 @@ def getReactionRateDict(nucName, lib, xsSuffix, mgFlux, nDens): Parameters ---------- nucName : str - nuclide name -- e.g. 'U235', 'PU239', etc. Not to be confused with the nuclide - _label_, see the nucDirectory module for a description of the difference. + nuclide name -- e.g. 'U235', 'PU239', etc. Not to be confused with the nuclide _label_, see + the nucDirectory module for a description of the difference. lib : isotxs cross section library xsSuffix : str - cross section suffix, consisting of the type followed by the burnup group, - e.g. 'AB' for the second burnup group of type A + cross section suffix, consisting of the type followed by the burnup group, e.g. 'AB' for the + second burnup group of type A mgFlux : numpy.nArray integrated mgFlux (n-cm/s) nDens : float diff --git a/armi/reactor/converters/geometryConverters.py b/armi/reactor/converters/geometryConverters.py index a2a24f7f5..ed1ba1011 100644 --- a/armi/reactor/converters/geometryConverters.py +++ b/armi/reactor/converters/geometryConverters.py @@ -15,14 +15,15 @@ """ Change a reactor from one geometry to another. -Examples may include going from Hex to R-Z or from Third-core to full core. This module -contains **converters** (which create new reactor objects with different geometry), and -**changers** (which modify a given reactor in place) in this module. +Examples may include going from Hex to R-Z or from Third-core to full core. This module contains +**converters** (which create new reactor objects with different geometry), and **changers** (which +modify a given reactor in place) in this module. Generally, mass is conserved in geometry conversions. -.. warning:: These are mostly designed for hex geometry. - +Warning +------- +These are mostly designed for hex geometry. """ import collections import copy @@ -107,7 +108,8 @@ class GeometryConverter(GeometryChanger): -------- To convert a hex case to a R-Z case, do this: - >>> geomConv = armi.reactorConverters.HexToRZConverter(useMostCommonXsId=False, expandReactor=False) + >>> from armi.reactorConverters import HexToRZConverter + >>> HexToRZConverter(useMostCommonXsId=False, expandReactor=False) >>> geomConv.convert(r) >>> newR = geomConv.convReactor >>> dif3d = dif3dInterface.Dif3dInterface('dif3dRZ', newR) @@ -126,7 +128,8 @@ class FuelAssemNumModifier(GeometryChanger): Notes ----- - - The number of fuel assemblies should ALWAYS be set for the third-core regardless of the reactor geometry model. + - The number of fuel assemblies should ALWAYS be set for the third-core regardless of the + reactor geometry model. - The modification is only valid for third-core and full-core geometry models. """ @@ -144,8 +147,8 @@ def convert(self, r): Notes ----- - - While adding fuel, does not modify existing fuel/control positions, but does overwrite assemblies in the - overwriteList (e.g. reflectors, shields) + - While adding fuel, does not modify existing fuel/control positions, but does overwrite + assemblies in the overwriteList (e.g. reflectors, shields) - Once specified amount of fuel is in place, removes all assemblies past the outer fuel boundary - To re-add reflector/shield assemblies around the new core, use the ringsToAdd attribute """ @@ -323,8 +326,8 @@ class HexToRZThetaConverter(GeometryConverter): Parameters ---------- converterSettings: dict - Settings that specify how the mesh of the RZTheta reactor should be generated. - Controls the number of theta regions, how to group regions, etc. + Settings that specify how the mesh of the RZTheta reactor should be generated. Controls the + number of theta regions, how to group regions, etc. uniformThetaMesh bool flag that determines if the theta mesh should be uniform or not @@ -336,19 +339,21 @@ class HexToRZThetaConverter(GeometryConverter): * ``Ring Compositions`` -- to convert by composition axialConversionType - * ``Axial Coordinates`` -- use :py:class:`armi.reactor.converters.meshConverters._RZThetaReactorMeshConverterByAxialCoordinates` - * ``Axial Bins`` -- use :py:class:`armi.reactor.converters.meshConverters._RZThetaReactorMeshConverterByAxialBins` + * ``Axial Coordinates`` -- use + :py:class:`armi.reactor.converters.meshConverters._RZThetaReactorMeshConverterByAxialCoordinates` + * ``Axial Bins`` -- use + :py:class:`armi.reactor.converters.meshConverters._RZThetaReactorMeshConverterByAxialBins` homogenizeAxiallyByFlags - Boolean that if set to True will ignore the `axialConversionType` input and determine a mesh based on the - material boundaries for each RZ region axially. + Boolean that if set to True will ignore the `axialConversionType` input and determine a + mesh based on the material boundaries for each RZ region axially. expandReactor : bool - If True, the HEX-Z reactor will be expanded to full core geometry prior to converting to the RZT reactor. - Either way the converted RZTheta core will be full core. + If True, the HEX-Z reactor will be expanded to full core geometry prior to converting to the + RZT reactor. Either way the converted RZTheta core will be full core. strictHomogenization : bool - If True, the converter will restrict HEX-Z blocks with dissimilar XS types from being homogenized into an - RZT block. + If True, the converter will restrict HEX-Z blocks with dissimilar XS types from being + homogenized into an RZT block. """ _GEOMETRY_TYPE = geometry.GeomType.RZT @@ -448,22 +453,22 @@ def convert(self, r): Notes ----- - The linked requirement technically points to a child class of this class, - HexToRZConverter. However, this is the method where the conversion actually happens - and thus the implementation tag is noted here. + The linked requirement technically points to a child class of this class, HexToRZConverter. + However, this is the method where the conversion actually happens and thus the + implementation tag is noted here. - As a part of the RZT mesh converters it is possible to obtain a radial mesh that - has repeated ring numbers. For instance, if there are fuel assemblies and control - assemblies within the same radial hex ring then it's possible that a radial mesh - output from the byRingComposition mesh converter method will look something like: + As a part of the RZT mesh converters it is possible to obtain a radial mesh that has + repeated ring numbers. For instance, if there are fuel assemblies and control assemblies + within the same radial hex ring then it's possible that a radial mesh output from the + byRingComposition mesh converter method will look something like: self.meshConverter.radialMesh = [2, 3, 4, 4, 5, 5, 6, 6, 6, 7, 8, 8, 9, 10] - In this instance the hex ring will remain the same for multiple iterations over - radial direction when homogenizing the hex core into the RZT geometry. In this - case, the converter needs to keep track of the compositions within this ring so - that it can separate this repeated ring into multiple RZT rings. Each of the RZT - rings should have a single composition (fuel1, fuel2, control, etc.) + In this instance the hex ring will remain the same for multiple iterations over radial + direction when homogenizing the hex core into the RZT geometry. In this case, the converter + needs to keep track of the compositions within this ring so that it can separate this + repeated ring into multiple RZT rings. Each of the RZT rings should have a single + composition (fuel1, fuel2, control, etc.) See Also -------- @@ -556,13 +561,15 @@ def _setNextAssemblyTypeInRadialZone(self, lowerRing, upperRing): def _getSortedAssemblyTypesInRadialZone(self, lowerRing, upperRing): """ - Retrieve assembly types in a radial zone between (lowerRing, upperRing), sort from highest occurrence to lowest. + Retrieve assembly types in a radial zone between (lowerRing, upperRing), sort from highest + occurrence to lowest. Notes ----- - - Assembly types are based on the assembly names and not the direct composition within each assembly. For - instance, if two assemblies are named `fuel 1` and `fuel 2` but they have the same composition at some reactor - state then they will still be separated as two different assembly types. + - Assembly types are based on the assembly names and not the direct composition within each + assembly. For instance, if two assemblies are named `fuel 1` and `fuel 2` but they have + the same composition at some reactor state then they will still be separated as two + different assembly types. """ aCountByTypes = collections.Counter() for a in self._getAssembliesInCurrentRadialZone(lowerRing, upperRing): @@ -609,8 +616,8 @@ def _setAssemsInRadialZone(self, radialIndex, lowerRing, upperRing): Notes ----- - self._assemsInRadialZone keeps track of the unique assemblies that are in each radial ring. This - ensures that no assemblies are duplicated when using self._getAssemsInRadialThetaZone() + self._assemsInRadialZone keeps track of the unique assemblies that are in each radial ring. + This ensures that no assemblies are duplicated when using self._getAssemsInRadialThetaZone() """ lowerTheta = 0.0 for _thetaIndex, upperTheta in enumerate(self.meshConverter.thetaMesh): @@ -677,7 +684,9 @@ def _getAssembliesInSector(core, theta1, theta2): return aList def _getAssemsInRadialThetaZone(self, lowerRing, upperRing, lowerTheta, upperTheta): - """Retrieve list of assemblies in the reactor between (lowerRing, upperRing) and (lowerTheta, upperTheta).""" + """Retrieve list of assemblies in the reactor between (lowerRing, upperRing) and + (lowerTheta, upperTheta). + """ thetaAssems = self._getAssembliesInSector( self._sourceReactor.core, math.degrees(lowerTheta), math.degrees(upperTheta) ) @@ -856,8 +865,10 @@ def _checkVolumeConservation(self, newBlock): if abs(newBlockVolumeFraction - 1.0) > 0.00001: raise ValueError( - "The volume fraction of block {} is {} and not 1.0. An error occurred when converting the reactor" - " geometry.".format(newBlock, newBlockVolumeFraction) + "The volume fraction of block {} is {} and not 1.0. An error occurred when " + "converting the reactor geometry.".format( + newBlock, newBlockVolumeFraction + ) ) def createHomogenizedRZTBlock( @@ -866,8 +877,9 @@ def createHomogenizedRZTBlock( """ Create the homogenized RZT block by computing the average atoms in the zone. - Additional calculations are performed to determine the homogenized block type, the block average temperature, - and the volume fraction of each hex block that is in the new homogenized block. + Additional calculations are performed to determine the homogenized block type, the block + average temperature, and the volume fraction of each hex block that is in the new + homogenized block. """ homBlockXsTypes = set() numHexBlockByType = collections.Counter() @@ -896,8 +908,8 @@ def createHomogenizedRZTBlock( # Notify if blocks with different xs types are being homogenized. May be undesired behavior. if len(homBlockXsTypes) > 1: msg = ( - "Blocks {} with dissimilar XS IDs are being homogenized in {} between axial heights {} " - "cm and {} cm. ".format( + "Blocks {} with dissimilar XS IDs are being homogenized in {} between axial heights" + " {} cm and {} cm. ".format( self.blockMap[homBlock], self.convReactor.core, lowerAxialZ, @@ -922,16 +934,17 @@ def createHomogenizedRZTBlock( def _getHomogenizedBlockType(self, numHexBlockByType): """ - Generate the homogenized block mixture type based on the frequency of hex block types that were merged - together. + Generate the homogenized block mixture type based on the frequency of hex block types that + were merged together. Notes ----- self._BLOCK_MIXTURE_TYPE_EXCLUSIONS: - The normal function of this method is to assign the mixture name based on the number of occurrences of the - block type. This list stops that and assigns the mixture based on the first occurrence. - (i.e. if the mixture has a set of blocks but it comes across one with the name of 'control' the process will - stop and the new mixture type will be set to 'mixture control' + The normal function of this method is to assign the mixture name based on the number of + occurrences of the block type. This list stops that and assigns the mixture based on the + first occurrence. (i.e. if the mixture has a set of blocks but it comes across one with + the name of 'control' the process will stop and the new mixture type will be set to + 'mixture control'. self._BLOCK_MIXTURE_TYPE_MAP: A dictionary that provides the name of blocks that are condensed together diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index 653d96b92..e18ac04e3 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -70,31 +70,27 @@ class Reactor(composites.Composite): Top level of the composite structure, potentially representing all components in a reactor. - This class contains the core and any ex-core structures that are to be - represented in the ARMI model. Historically, the `Reactor` contained only - the core. To support better representation of ex-core structures, the old - `Reactor` functionality was moved to the newer `Core` class, which has a - `Reactor` parent. + This class contains the core and any ex-core structures that are to be represented in the ARMI + model. Historically, the ``Reactor`` contained only the core. To support better representation + of ex-core structures, the old ``Reactor`` functionality was moved to the newer `Core` class, + which has a ``Reactor`` parent. .. impl:: The user-specified reactor. :id: I_ARMI_R :implements: R_ARMI_R - The :py:class:`Reactor <armi.reactor.reactors.Reactor>` is the top - level of the composite structure, which can represent all components - within a reactor core. The reactor contains a :py:class:`Core - <armi.reactor.reactors.Core>`, which contains a collection of - :py:class:`Assembly <armi.reactor.assemblies.Assembly>` objects - arranged in a hexagonal or Cartesian grid. Each Assembly consists of a - stack of :py:class:`Block <armi.reactor.blocks.Block>` objects, which - are each composed of one or more :py:class:`Component - <armi.reactor.components.component.Component>` objects. Each - :py:class:`Interface <armi.interfaces.Interface>` is able to interact - with the reactor and its child :py:class:`Composites - <armi.reactor.composites.Composite>` by retrieving data from it or - writing new data to it. This is the main medium through which input - information and the output of physics calculations is exchanged between - interfaces and written to an ARMI database. + The :py:class:`Reactor <armi.reactor.reactors.Reactor>` is the top level of the composite + structure, which can represent all components within a reactor core. The reactor contains a + :py:class:`Core <armi.reactor.reactors.Core>`, which contains a collection of + :py:class:`Assembly <armi.reactor.assemblies.Assembly>` objects arranged in a hexagonal or + Cartesian grid. Each Assembly consists of a stack of + :py:class:`Block <armi.reactor.blocks.Block>` objects, which are each composed of one or + more :py:class:`Component <armi.reactor.components.component.Component>` objects. Each + :py:class:`Interface <armi.interfaces.Interface>` is able to interact with the reactor and + its child :py:class:`Composites <armi.reactor.composites.Composite>` by retrieving data from + it or writing new data to it. This is the main medium through which input information and + the output of physics calculations is exchanged between interfaces and written to an ARMI + database. """ pDefs = reactorParameters.defineReactorParameters() @@ -149,8 +145,8 @@ def incrementAssemNum(self): Notes ----- - The "max assembly number" is not currently used in the Reactor. So the idea - is that we return the current number, then iterate it for the next assembly. + The "max assembly number" is not currently used in the Reactor. So the idea is that we + return the current number, then iterate it for the next assembly. Obviously, this method will be unused for non-assembly-based reactors. @@ -258,18 +254,16 @@ class Core(composites.Composite): :id: I_ARMI_R_CORE :implements: R_ARMI_R_CORE - A :py:class:`Core <armi.reactor.reactors.Core>` object is typically a - child of a :py:class:`Reactor <armi.reactor.reactors.Reactor>` object. - A Reactor can contain multiple objects of the Core type. The instance - attribute name ``r.core`` is reserved for the object representing the - active core. A reactor may also have a spent fuel pool instance - attribute, ``r.sfp``, which is also of type - :py:class:`Core <armi.reactor.reactors.Core>`. + A :py:class:`Core <armi.reactor.reactors.Core>` object is typically a child of a + :py:class:`Reactor <armi.reactor.reactors.Reactor>` object. A Reactor can contain multiple + objects of the Core type. The instance attribute name ``r.core`` is reserved for the object + representating the active core. A reactor may also have a spent fuel pool instance + attribute, ``r.sfp``, which is also of type :py:class:`core <armi.reactor.reactors.Core>`. - Most of the operations to retrieve information from the ARMI reactor - data model are mediated through Core objects. For example, - :py:meth:`getAssemblies() <armi.reactor.reactors.Core.getAssemblies>` is - used to get a list of all assemblies in the Core. + Most of the operations to retrieve information from the ARMI reactor data model are mediated + through Core objects. For example, + :py:meth:`getAssemblies() <armi.reactor.reactors.Core.getAssemblies>` is used to get a list + of all assemblies in the Core. Attributes ---------- @@ -380,24 +374,19 @@ def symmetry(self) -> geometry.SymmetryType: :id: I_ARMI_R_SYMM :implements: R_ARMI_R_SYMM - This property getter returns the symmetry attribute of the - spatialGrid instance attribute. The spatialGrid is an instance of a - child of the abstract base class :py:class:`Grid - <armi.reactor.grids.grid.Grid>` type. The symmetry attribute is an - instance of the :py:class:`SymmetryType - <armi.reactor.geometry.SymmetryType>` class, which is a wrapper - around the :py:class:`DomainType <armi.reactor.geometry.DomainType>` - and :py:class:`BoundaryType <armi.reactor.geometry.BoundaryType>` - enumerations used to classify the domain (e.g., 1/3 core, quarter - core, full core) and symmetry boundary conditions (e.g., periodic, - reflective, none) of a reactor, respectively. - - Only specific combinations of :py:class:`Grid - <armi.reactor.grids.grid.Grid>` type, :py:class:`DomainType - <armi.reactor.geometry.DomainType>`, and :py:class:`BoundaryType - <armi.reactor.geometry.BoundaryType>` are valid. The validity of a - user-specified geometry and symmetry is verified by a settings - :py:class:`Inspector + This property getter returns the symmetry attribute of the spatialGrid instance + attribute. The spatialGrid is an instance of a child of the abstract base class + :py:class:`Grid <armi.reactor.grids.grid.Grid>` type. The symmetry attribute is an + instance of the :py:class:`SymmetryType <armi.reactor.geometry.SymmetryType>` class, + which is a wrapper around the :py:class:`DomainType <armi.reactor.geometry.DomainType>` + and :py:class:`BoundaryType <armi.reactor.geometry.BoundaryType>` enumerations used to + classify the domain (e.g., 1/3 core, quarter core, full core) and symmetry boundary + conditions (e.g., periodic, reflective, none) of a reactor, respectively. + + Only specific combinations of :py:class:`Grid <armi.reactor.grids.grid.Grid>` type, + :py:class:`DomainType <armi.reactor.geometry.DomainType>`, and :py:class:`BoundaryType + <armi.reactor.geometry.BoundaryType>` are valid. The validity of a user-specified + geometry and symmetry is verified by a settings :py:class:`Inspector <armi.operators.settingsValidation.Inspector`. """ if not self.spatialGrid: @@ -433,12 +422,10 @@ def lib(self) -> Optional[xsLibraries.IsotxsLibrary]: """ Return the microscopic cross section library if one exists. - - If there is a library currently associated with the core, - it will be returned - - Otherwise, an ``ISOTXS`` file will be searched for in the working directory, - opened as ``ISOTXS`` object and returned. - - Finally, if no ``ISOTXS`` file exists in the working directory, - a None will be returned. + - If there is a library currently associated with the core, it will be returned + - Otherwise, an ``ISOTXS`` file will be searched for in the working directory, opened as + ``ISOTXS`` object and returned. + - Finally, if no ``ISOTXS`` file exists in the working directory, a None will be returned. """ isotxsFileName = nuclearDataIO.getExpectedISOTXSFileName() if self._lib is None and os.path.exists(isotxsFileName): @@ -468,14 +455,14 @@ def refAssem(self): """ Return the "reference" assembly for this Core. - The reference assembly is defined as the center-most assembly with a FUEL flag, - if any are present, or the center-most of any assembly otherwise. + The reference assembly is defined as the center-most assembly with a FUEL flag, if any are + present, or the center-most of any assembly otherwise. Warning ------- - The convenience of this property should be weighed against it's somewhat - arbitrary nature for any particular client. The center-most fueled assembly is - not particularly representative of the state of the core as a whole. + The convenience of this property should be weighed against it's somewhat arbitrary nature + for any particular client. The center-most fueled assembly is not particularly + representative of the state of the core as a whole. """ key = lambda a: a.spatialLocator.getRingPos() assems = self.getAssemblies(Flags.FUEL, sortKey=key) @@ -526,9 +513,8 @@ def setPowerFromDensity(self): def setPowerIfNecessary(self): """Set the core power, from the power density. - If the power density is set, but the power isn't, we set the calculate the - total heavy metal mass of the reactor, and set the total power. Which will - then be the real source of truth again. + If the power density is set, but the power isn't, calculate the total heavy metal mass of + the reactor, and set the total power. Which will then be the real source of truth again. """ if self.p.power == 0 and self.p.powerDensity > 0: self.setPowerFromDensity() @@ -549,8 +535,7 @@ def locateAllAssemblies(self): """ Store the current location of all assemblies. - This is required for shuffle printouts, repeat shuffling, and - MCNP shuffling. + This is required for shuffle printouts, repeat shuffling, and MCNP shuffling. """ for a in self.getAssemblies(includeAll=True): a.lastLocationLabel = a.getLocation() diff --git a/pyproject.toml b/pyproject.toml index 6b68f14b3..bc15238c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -135,8 +135,8 @@ required-version = "0.0.272" # Assume Python 3.9 target-version = "py39" -# Setting line-length to 88 to match Black -line-length = 88 +# Setting line-length to 140 (though blacks default is 88) +line-length = 140 # Enable pycodestyle (E) and Pyflakes (F) codes by default. # D - NumPy docstring rules @@ -166,12 +166,11 @@ select = ["E", "F", "D", "N801", "SIM", "TID"] # # D105 - we don't need to document well-known magic methods # D205 - 1 blank line required between summary line and description -# E501 - line length, because we use Black for that # E731 - we can use lambdas however we want # RUF100 - no unused noqa statements (not consistent enough yet) # SIM118 - this does not work where we overload the .keys() method # -ignore = ["D100", "D101", "D102", "D103", "D105", "D106", "D205", "D401", "D404", "E501", "E731", "RUF100", "SIM102", "SIM105", "SIM108", "SIM114", "SIM115", "SIM117", "SIM118"] +ignore = ["D100", "D101", "D102", "D103", "D105", "D106", "D205", "D401", "D404", "E731", "RUF100", "SIM102", "SIM105", "SIM108", "SIM114", "SIM115", "SIM117", "SIM118"] # Exclude a variety of commonly ignored directories. exclude = [ From f0fe692acdf9ec1eacb9ad680a9e6529dcd33361 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Wed, 21 Feb 2024 09:07:47 -0800 Subject: [PATCH 174/176] Renaming structuredgrid.py to camelCase (#1650) --- armi/reactor/blueprints/gridBlueprint.py | 2 +- armi/reactor/grids/__init__.py | 2 +- armi/reactor/grids/axial.py | 2 +- armi/reactor/grids/cartesian.py | 2 +- armi/reactor/grids/hexagonal.py | 2 +- armi/reactor/grids/{structuredgrid.py => structuredGrid.py} | 0 armi/reactor/grids/thetarz.py | 2 +- doc/release/0.3.rst | 5 +++++ 8 files changed, 11 insertions(+), 6 deletions(-) rename armi/reactor/grids/{structuredgrid.py => structuredGrid.py} (100%) diff --git a/armi/reactor/blueprints/gridBlueprint.py b/armi/reactor/blueprints/gridBlueprint.py index 0fe012454..ce133a673 100644 --- a/armi/reactor/blueprints/gridBlueprint.py +++ b/armi/reactor/blueprints/gridBlueprint.py @@ -161,7 +161,7 @@ class GridBlueprint(yamlize.Object): class. Includes a ``construct`` method, which instantiates an instance of one - of the subclasses of :py:class:`~armi.reactor.grids.structuredgrid.StructuredGrid`. + of the subclasses of :py:class:`~armi.reactor.grids.structuredGrid.StructuredGrid`. This is typically called from within :py:meth:`~armi.reactor.blueprints.blockBlueprint.BlockBlueprint.construct`, which then also associates the individual components in the block with locations specifed in the grid. diff --git a/armi/reactor/grids/__init__.py b/armi/reactor/grids/__init__.py index db5eb3492..5daf84c68 100644 --- a/armi/reactor/grids/__init__.py +++ b/armi/reactor/grids/__init__.py @@ -79,7 +79,7 @@ ) from armi.reactor.grids.grid import Grid -from armi.reactor.grids.structuredgrid import StructuredGrid, GridParameters, _tuplify +from armi.reactor.grids.structuredGrid import StructuredGrid, GridParameters, _tuplify from armi.reactor.grids.axial import AxialGrid, axialUnitGrid from armi.reactor.grids.cartesian import CartesianGrid from armi.reactor.grids.hexagonal import HexGrid, COS30, SIN30, TRIANGLES_IN_HEXAGON diff --git a/armi/reactor/grids/axial.py b/armi/reactor/grids/axial.py index d2a02a25a..b297c4596 100644 --- a/armi/reactor/grids/axial.py +++ b/armi/reactor/grids/axial.py @@ -17,7 +17,7 @@ import numpy from armi.reactor.grids.locations import IJType, LocationBase -from armi.reactor.grids.structuredgrid import StructuredGrid +from armi.reactor.grids.structuredGrid import StructuredGrid if TYPE_CHECKING: from armi.reactor.composites import ArmiObject diff --git a/armi/reactor/grids/cartesian.py b/armi/reactor/grids/cartesian.py index 73ff01f55..6de012184 100644 --- a/armi/reactor/grids/cartesian.py +++ b/armi/reactor/grids/cartesian.py @@ -19,7 +19,7 @@ from armi.reactor import geometry from armi.reactor.grids.locations import IJType -from armi.reactor.grids.structuredgrid import StructuredGrid +from armi.reactor.grids.structuredGrid import StructuredGrid class CartesianGrid(StructuredGrid): diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py index 2365b1001..a98b88b82 100644 --- a/armi/reactor/grids/hexagonal.py +++ b/armi/reactor/grids/hexagonal.py @@ -26,7 +26,7 @@ BOUNDARY_CENTER, ) from armi.reactor.grids.locations import IndexLocation, IJKType, IJType -from armi.reactor.grids.structuredgrid import StructuredGrid +from armi.reactor.grids.structuredGrid import StructuredGrid COS30 = sqrt(3) / 2.0 SIN30 = 1.0 / 2.0 diff --git a/armi/reactor/grids/structuredgrid.py b/armi/reactor/grids/structuredGrid.py similarity index 100% rename from armi/reactor/grids/structuredgrid.py rename to armi/reactor/grids/structuredGrid.py diff --git a/armi/reactor/grids/thetarz.py b/armi/reactor/grids/thetarz.py index ec6ed2774..27148ae98 100644 --- a/armi/reactor/grids/thetarz.py +++ b/armi/reactor/grids/thetarz.py @@ -17,7 +17,7 @@ import numpy from armi.reactor.grids.locations import IJType, IJKType -from armi.reactor.grids.structuredgrid import StructuredGrid +from armi.reactor.grids.structuredGrid import StructuredGrid if TYPE_CHECKING: # Avoid circular imports diff --git a/doc/release/0.3.rst b/doc/release/0.3.rst index e27d98c6b..a08068122 100644 --- a/doc/release/0.3.rst +++ b/doc/release/0.3.rst @@ -10,6 +10,11 @@ What's new in ARMI? ------------------- #. TBD +API Changes +----------- +#. Renaming ``structuredgrid.py`` to camelCase. (`PR#1650 <https://github.com/terrapower/armi/pull/1650>`_) +#. TBD + Bug Fixes --------- #. TBD From cfbc7f67ec4c76c6f28612416514934bcd5c4dd6 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 22 Feb 2024 14:02:56 -0800 Subject: [PATCH 175/176] Removing unused Block.coords argument rotationDegreesCCW (#1651) --- armi/reactor/blocks.py | 10 ++-------- doc/release/0.3.rst | 3 ++- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py index 97296c974..8fd59963c 100644 --- a/armi/reactor/blocks.py +++ b/armi/reactor/blocks.py @@ -605,7 +605,7 @@ def getLocation(self): else: return "ExCore" - def coords(self, rotationDegreesCCW=0.0): + def coords(self): """ Returns the coordinates of the block. @@ -617,11 +617,7 @@ def coords(self, rotationDegreesCCW=0.0): method of the block's ``spatialLocator`` attribute, which recursively calls itself on all parents of the block to get the coordinates of the block's centroid in 3D cartesian space. - - If ``rotationDegreesCCW`` is non-zero, an error is raised. """ - if rotationDegreesCCW: - raise NotImplementedError("Cannot get coordinates with rotation.") return self.spatialLocator.getGlobalCoordinates() def setBuLimitInfo(self): @@ -1677,7 +1673,7 @@ class HexBlock(Block): def __init__(self, name, height=1.0): Block.__init__(self, name, height) - def coords(self, rotationDegreesCCW=0.0): + def coords(self): """ Returns the coordinates of the block. @@ -1692,8 +1688,6 @@ def coords(self, rotationDegreesCCW=0.0): Will additionally adjust the x and y coordinates based on the block parameters ``displacementX`` and ``displacementY``. - - Note that the ``rotationDegreesCCW`` argument is unused. """ x, y, _z = self.spatialLocator.getGlobalCoordinates() x += self.p.displacementX * 100.0 diff --git a/doc/release/0.3.rst b/doc/release/0.3.rst index a08068122..03f867b0b 100644 --- a/doc/release/0.3.rst +++ b/doc/release/0.3.rst @@ -13,6 +13,7 @@ What's new in ARMI? API Changes ----------- #. Renaming ``structuredgrid.py`` to camelCase. (`PR#1650 <https://github.com/terrapower/armi/pull/1650>`_) +#. Removing unused argument from ``Block.coords()``. (`PR#1651 <https://github.com/terrapower/armi/pull/1651>`_) #. TBD Bug Fixes @@ -21,7 +22,7 @@ Bug Fixes Changes that Affect Requirements -------------------------------- -#. TBD +#. (`PR#1651 <https://github.com/terrapower/armi/pull/1651>`_) - Very minor change to ``Block.coords()``, removing unused argument. ARMI v0.3.0 From 6163e942abe873129533b84a6f2efa632f260b47 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Thu, 22 Feb 2024 14:29:14 -0800 Subject: [PATCH 176/176] Updating PR form to better explain release notes (#1652) --- .github/pull_request_template.md | 12 ++++++------ armi/bookkeeping/db/layout.py | 6 +++--- doc/developer/tooling.rst | 20 ++++++++++++++++++++ doc/release/0.3.rst | 5 +++-- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 10fd10092..6600a0c6b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -7,18 +7,19 @@ <!-- MANDATORY: Explain why the change is necessary --> <!-- Optional: Link to any related GitHub Issues --> + --- ## Checklist <!-- You (the pull requester) should put an `x` in the boxes below you have completed. - If you're unsure about any of them, don't hesitate to ask. We're here to help! - (If a checkbox requires no action for this PR, put an `x` in the box.) + + (If a checkbox doesn't apply to your PR, check it anyway.) --> - [ ] This PR has only [one purpose or idea](https://terrapower.github.io/armi/developer/tooling.html#one-idea-one-pr). -- [ ] [Tests](https://terrapower.github.io/armi/developer/tooling.html#test-it) have been added/updated to verify that the new/changed code works. +- [ ] [Tests](https://terrapower.github.io/armi/developer/tooling.html#test-it) have been added/updated to verify any new/changed code. <!-- Check the code quality --> @@ -27,7 +28,6 @@ <!-- Check the project-level cruft --> -- [ ] The [release notes](https://terrapower.github.io/armi/release/index.html) (location `doc/release/0.X.rst`) are up-to-date with any important changes. +- [ ] The [release notes](https://terrapower.github.io/armi/developer/tooling.html#add-release-notes) have been updated if necessary. - [ ] The [documentation](https://terrapower.github.io/armi/developer/tooling.html#document-it) is still up-to-date in the `doc` folder. -- [ ] If any [requirements](https://terrapower.github.io/armi/developer/tooling.html#watch-for-requirements) were affected, mention it in the [release notes](https://terrapower.github.io/armi/release/index.html). -- [ ] The dependencies are still up-to-date in `pyproject.toml`. +- [ ] The dependencies are still up-to-date in `pyproject.toml`. \ No newline at end of file diff --git a/armi/bookkeeping/db/layout.py b/armi/bookkeeping/db/layout.py index 2db3f5ea5..4dad92606 100644 --- a/armi/bookkeeping/db/layout.py +++ b/armi/bookkeeping/db/layout.py @@ -603,7 +603,7 @@ def _packLocationsV1( def _packLocationsV2( locations: List[grids.LocationBase], ) -> Tuple[List[str], List[Tuple[int, int, int]]]: - """Location packing implementation for minor version 3. See release notes above.""" + """Location packing implementation for minor version 3. See module docstring above.""" locTypes = [] locData: List[Tuple[int, int, int]] = [] for loc in locations: @@ -630,7 +630,7 @@ def _packLocationsV2( def _packLocationsV3( locations: List[grids.LocationBase], ) -> Tuple[List[str], List[Tuple[int, int, int]]]: - """Location packing implementation for minor version 4. See release notes above.""" + """Location packing implementation for minor version 4. See module docstring above.""" locTypes = [] locData: List[Tuple[int, int, int]] = [] @@ -689,7 +689,7 @@ def _unpackLocationsV1(locationTypes, locData): def _unpackLocationsV2(locationTypes, locData): - """Location unpacking implementation for minor version 3+. See release notes above.""" + """Location unpacking implementation for minor version 3+. See module docstring above.""" locsIter = iter(locData) unpackedLocs = [] for lt in locationTypes: diff --git a/doc/developer/tooling.rst b/doc/developer/tooling.rst index b9d8c1bd6..a423ea6fc 100644 --- a/doc/developer/tooling.rst +++ b/doc/developer/tooling.rst @@ -105,6 +105,26 @@ call out that you touch such a code in your PR. Your PR reviewer will take an extra look at any PR that touches a requirement test or implementation. And you will need to add a special release note under the "Changes that Affect Requirements" section header. +Add Release Notes +----------------- +For most PRs, you will need to add release notes to the +`Release Notes documentation <https://github.com/terrapower/armi/tree/main/doc/release>`_. The goal +here is to help track all the important changes that happened in ARMI, so ARMI can document what +has changed during the next `release <https://github.com/terrapower/armi/releases>`_. To that end, +minor PRs won't require a release note. + +In particular, in the release notes, you will find four sections in the releasee notes: + +1. **New Features** - A new feature (or major addition to a current feature) was added to the code. +2. **API Changes** - ANY change to the public-facing API of ARMI. +3. **Bug Fixes** - ANY bug fix in the code (not the documentation), no matter how minor. +4. **Changes that Affect Requirements** - If you touch the code (``impl``) or test (``test``) for + anything that currently has a requirement crumb. (This must be a non-trivial change.) + +If your PR fits more than one of these categories, great! Put a description of your change under +all the categories that apply. + + Packaging and dependency management =================================== The process of packaging Python projects and managing their dependencies is somewhat diff --git a/doc/release/0.3.rst b/doc/release/0.3.rst index 03f867b0b..4f708df3f 100644 --- a/doc/release/0.3.rst +++ b/doc/release/0.3.rst @@ -6,8 +6,8 @@ ARMI v0.3.1 =========== Release Date: TBD -What's new in ARMI? -------------------- +New Features +------------ #. TBD API Changes @@ -23,6 +23,7 @@ Bug Fixes Changes that Affect Requirements -------------------------------- #. (`PR#1651 <https://github.com/terrapower/armi/pull/1651>`_) - Very minor change to ``Block.coords()``, removing unused argument. +#. TBD ARMI v0.3.0