From 6dcedacdea505a508cf85119b4eb22953a1763e0 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Mon, 7 Mar 2022 11:35:15 -0800 Subject: [PATCH] Tracking ARMI Requirements (#590) --- armi/bookkeeping/report/reportingUtils.py | 5 +- armi/bookkeeping/report/tests/test_report.py | 5 +- armi/cases/tests/test_suiteBuilder.py | 16 ++- armi/cli/__init__.py | 10 +- armi/nuclearDataIO/cccc/tests/test_nhflux.py | 6 -- armi/operators/operator.py | 18 ++-- armi/operators/tests/test_operators.py | 1 + armi/physics/neutronics/settings.py | 2 +- armi/reactor/blueprints/__init__.py | 8 +- .../blueprints/tests/test_blueprints.py | 39 +++++--- armi/reactor/components/basicShapes.py | 8 +- armi/reactor/components/complexShapes.py | 7 ++ armi/reactor/components/component.py | 6 +- armi/reactor/components/volumetricShapes.py | 10 +- .../tests/test_geometryConverters.py | 5 + .../converters/tests/test_uniformMesh.py | 8 +- armi/reactor/grids.py | 15 +++ armi/reactor/reactors.py | 70 +++++++------ armi/reactor/tests/test_components.py | 77 ++++++++++++--- armi/reactor/tests/test_grids.py | 21 ++++ armi/reactor/tests/test_reactors.py | 13 ++- .../settings/tests/old_xml_settings_input.xml | 4 +- armi/utils/__init__.py | 1 + armi/utils/iterables.py | 2 +- armi/utils/tests/test_utils.py | 30 +++++- doc/conf.py | 99 +++---------------- doc/developer/tooling.rst | 2 + doc/index.rst | 1 + doc/requirements-docs.txt | 4 + doc/requirements/index.rst | 23 +++++ doc/requirements/srsd.rst | 90 +++++++++++++++++ 31 files changed, 420 insertions(+), 186 deletions(-) create mode 100644 doc/requirements/index.rst create mode 100644 doc/requirements/srsd.rst diff --git a/armi/bookkeeping/report/reportingUtils.py b/armi/bookkeeping/report/reportingUtils.py index aa4af6e53..0a22755e9 100644 --- a/armi/bookkeeping/report/reportingUtils.py +++ b/armi/bookkeeping/report/reportingUtils.py @@ -383,7 +383,7 @@ def writeCycleSummary(core): core: armi.reactor.reactors.Core cs: armi.settings.caseSettings.Settings """ - ## would io be worth considering for this? + # would io be worth considering for this? cycle = core.r.p.cycle str_ = [] runLog.important("Cycle {0} Summary:".format(cycle)) @@ -583,7 +583,7 @@ def summarizePower(core): # calculate total power tot = sum(sums.values()) or float("inf") - ## NOTE: if tot is 0.0, set to infinity to prevent ZeroDivisionError + # NOTE: if tot is 0.0, set to infinity to prevent ZeroDivisionError runLog.important("Power summary") for atype, val in sums.items(): @@ -870,7 +870,6 @@ def _setGeneralSimulationData(core, cs, coreDesignTable): ) -## Block Design Report def makeBlockDesignReport(r): r"""Summarize the block designs from the loading file diff --git a/armi/bookkeeping/report/tests/test_report.py b/armi/bookkeeping/report/tests/test_report.py index 8b7ae381a..83f44f391 100644 --- a/armi/bookkeeping/report/tests/test_report.py +++ b/armi/bookkeeping/report/tests/test_report.py @@ -61,10 +61,7 @@ def test_setData(self): self.assertEqual(filled_instance["banana_3"], ["sundae", "chocolate"]) def test_printReports(self): - """testing testing - - :ref:`REQ86d884bb-6133-4078-8804-5a334c935338` - """ + """testing printReports method""" repInt = reportInterface.ReportInterface(None, None) rep = repInt.printReports() diff --git a/armi/cases/tests/test_suiteBuilder.py b/armi/cases/tests/test_suiteBuilder.py index 242cbb82f..da07d05c1 100644 --- a/armi/cases/tests/test_suiteBuilder.py +++ b/armi/cases/tests/test_suiteBuilder.py @@ -15,13 +15,23 @@ """ Unit tests for the SuiteBuilder """ +import os import unittest -from armi.cases.inputModifiers.inputModifiers import SamplingInputModifier + from armi import cases, settings +from armi.cases.inputModifiers.inputModifiers import SamplingInputModifier from armi.cases.suiteBuilder import LatinHyperCubeSuiteBuilder - -cs = settings.Settings("armi/tests/tutorials/anl-afci-177.yaml") +cs = settings.Settings( + os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "..", + "..", + "tests", + "tutorials", + "anl-afci-177.yaml", + ) +) case = cases.Case(cs) diff --git a/armi/cli/__init__.py b/armi/cli/__init__.py index 7662c414e..9686f9e44 100644 --- a/armi/cli/__init__.py +++ b/armi/cli/__init__.py @@ -156,7 +156,7 @@ def listCommands(self): sub = re.compile(r"\s+").sub - ## given a string, condense white space into a single space + # given a string, condense white space into a single space condense = lambda s: sub(" ", s.strip()) commands = self._entryPoints.values() @@ -164,10 +164,10 @@ def listCommands(self): formatter = "{name:<{width}}{desc}".format print("\ncommands:") for cmd in sorted(commands, key=lambda cmd: cmd.name): - ## Each command can optionally define a class attribute `description` - ## as documentation. If description is not defined (default=None since - ## it should inherit from EntryPoint), then the docstring is used. - ## If the docstring is also None, then fall back to an empty string. + """Each command can optionally define a class attribute `description` + as documentation. If description is not defined (default=None since + it should inherit from EntryPoint), then the docstring is used. + If the docstring is also None, then fall back to an empty string.""" desc = condense(cmd.description or cmd.__doc__ or "") print(wrapper.fill(formatter(width=indent, name=cmd.name, desc=desc))) diff --git a/armi/nuclearDataIO/cccc/tests/test_nhflux.py b/armi/nuclearDataIO/cccc/tests/test_nhflux.py index b03fb7f12..b9f2bdf02 100644 --- a/armi/nuclearDataIO/cccc/tests/test_nhflux.py +++ b/armi/nuclearDataIO/cccc/tests/test_nhflux.py @@ -96,8 +96,6 @@ def test_fluxMoments(self): The 5 flux moments values are manually verified for two nodes. The indices are converted to zero based from the original by subtracting one. - - :req:`REQ77f06870-5923-429c-b3c7-d42f5a24f404` """ # node 1 (ring=1, position=1), axial=3, group=2 i = 0 # first one in node map (ring=1, position=1) @@ -131,8 +129,6 @@ def test_xyPartialCurrents(self): The surface partial currents can be used to reconstruct the surface flux and corner flux values. This test shows that the outgoing current in one hex is identical to the incoming current in the adjacent hex. - - :req:`REQ77f06870-5923-429c-b3c7-d42f5a24f404` """ # node 2 (ring=3, position=1), axial=4, group=2, surface=4, outgoing iNode, iSurf, iz, ig = 1, 3, 3, 1 # zero based @@ -154,8 +150,6 @@ def test_zPartialCurrents(self): The Z-directed partial currents are manually checked for one node surface. - - :req:`REQ77f06870-5923-429c-b3c7-d42f5a24f404` """ # node 15 (ring=2, position=3), axial=3, group=3, j=1 (z-plus) iNode, iz, ig, j = 14, 2, 2, 0 diff --git a/armi/operators/operator.py b/armi/operators/operator.py index 181434af7..ce9a64f11 100644 --- a/armi/operators/operator.py +++ b/armi/operators/operator.py @@ -21,6 +21,10 @@ 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: IMPL_EVOLVING_STATE_0 + :links: REQ_EVOLVING_STATE """ import time import shutil @@ -29,16 +33,16 @@ import armi from armi import context +from armi import interfaces from armi import runLog -from armi.bookkeeping import memoryProfiler -from armi.utils.mathematics import expandRepeatedFloats -from armi.utils import codeTiming -from armi.utils import pathTools from armi import settings +from armi.bookkeeping import memoryProfiler +from armi.bookkeeping.report import reportingUtils from armi.operators import settingsValidation from armi.operators.runTypes import RunTypes -from armi import interfaces -from armi.bookkeeping.report import reportingUtils +from armi.utils import codeTiming +from armi.utils import pathTools +from armi.utils.mathematics import expandRepeatedFloats class Operator: # pylint: disable=too-many-public-methods @@ -419,7 +423,7 @@ def _checkCsConsistency(self): def interactAllInit(self): """Call interactInit on all interfaces in the stack after they are initialized.""" - allInterfaces = self.interfaces[:] ## copy just in case + allInterfaces = self.interfaces[:] # copy just in case self._interactAll("Init", allInterfaces) def interactAllBOL(self, excludedInterfaceNames=()): diff --git a/armi/operators/tests/test_operators.py b/armi/operators/tests/test_operators.py index b49a62506..be3885b57 100644 --- a/armi/operators/tests/test_operators.py +++ b/armi/operators/tests/test_operators.py @@ -40,6 +40,7 @@ class InterfaceC(Interface): name = "Third" +# TODO: Add a test that shows time evolution of Reactor (REQ_EVOLVING_STATE) class OperatorTests(unittest.TestCase): def test_addInterfaceSubclassCollision(self): self.cs = settings.Settings() diff --git a/armi/physics/neutronics/settings.py b/armi/physics/neutronics/settings.py index cc73e6214..b4fc931c6 100644 --- a/armi/physics/neutronics/settings.py +++ b/armi/physics/neutronics/settings.py @@ -342,7 +342,7 @@ def defineSettings(): return settings -## OLD STYLE settings rules from settingsRules.py. Prefer validators moving forward. +# OLD STYLE settings rules from settingsRules.py. Prefer validators moving forward. @include_as_rule("genXS") diff --git a/armi/reactor/blueprints/__init__.py b/armi/reactor/blueprints/__init__.py index d4cdcaf98..8f9597a96 100644 --- a/armi/reactor/blueprints/__init__.py +++ b/armi/reactor/blueprints/__init__.py @@ -173,7 +173,13 @@ def __new__(mcs, name, bases, attrs): class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector): - """Base Blueprintsobject representing all the subsections in the input file.""" + """ + 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 + """ 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 8b21fc901..953743978 100644 --- a/armi/reactor/blueprints/tests/test_blueprints.py +++ b/armi/reactor/blueprints/tests/test_blueprints.py @@ -45,7 +45,6 @@ class TestBlueprints(unittest.TestCase): TODO: see the above note, and try to test blueprints on a wider range of input files, touching on each failure case. - """ @classmethod @@ -65,7 +64,12 @@ def tearDownClass(cls): cls.directoryChanger.close() def test_nuclides(self): - """Tests the available sets of nuclides work as expected""" + """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 + """ actives = set(self.blueprints.activeNuclides) inerts = set(self.blueprints.inertNuclides) self.assertEqual( @@ -87,6 +91,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. + + .. test:: Tests that the user can specify the dimensions of a component with arbitrary fidelity. + :id: TEST_REACTOR_1 + :links: REQ_REACTOR + """ fuelAssem = self.blueprints.constructAssem(self.cs, name="igniter fuel") fuel = fuelAssem.getComponents(Flags.FUEL)[0] self.assertAlmostEqual(fuel.getDimension("od", cold=True), 0.86602) @@ -109,7 +119,7 @@ def test_traceNuclides(self): class TestBlueprintsSchema(unittest.TestCase): """Test the blueprint schema checks""" - yamlString = r"""blocks: + _yamlString = r"""blocks: fuel: &block_fuel fuel: &component_fuel_fuel shape: Hexagon @@ -120,9 +130,9 @@ class TestBlueprintsSchema(unittest.TestCase): mult: 1.0 op: 10.0 fuel2: &block_fuel2 - group1: + group1: shape: Group - duct: + duct: shape: Hexagon material: UZr Tinput: 25.0 @@ -130,7 +140,7 @@ class TestBlueprintsSchema(unittest.TestCase): ip: 9.0 mult: 1.0 op: 10.0 - matrix: + matrix: shape: DerivedShape material: Graphite Tinput: 25.0 @@ -191,7 +201,7 @@ class TestBlueprintsSchema(unittest.TestCase): def test_assemblyParameters(self): cs = settings.Settings() - design = blueprints.Blueprints.load(self.yamlString) + design = blueprints.Blueprints.load(self._yamlString) fa = design.constructAssem(cs, name="fuel a") fb = design.constructAssem(cs, name="fuel b") for paramDef in fa.p.paramDefs.inCategory( @@ -211,12 +221,12 @@ def test_assemblyParameters(self): self.assertEqual(fb.p.hotChannelFactors, "Reactor") def test_nuclidesMc2v2(self): - """Tests that ZR is not expanded to its isotopics for this setting..""" + """Tests that ZR is not expanded to its isotopics for this setting.""" cs = settings.Settings() newSettings = {"xsKernel": "MC2v2"} cs = cs.modified(newSettings=newSettings) - design = blueprints.Blueprints.load(self.yamlString) + design = blueprints.Blueprints.load(self._yamlString) design._prepConstruction(cs) self.assertTrue( set({"U238", "U235", "ZR"}).issubset(set(design.allNuclidesInProblem)) @@ -233,7 +243,7 @@ def test_nuclidesMc2v3(self): newSettings = {"xsKernel": "MC2v3"} cs = cs.modified(newSettings=newSettings) - design = blueprints.Blueprints.load(self.yamlString) + design = blueprints.Blueprints.load(self._yamlString) design._prepConstruction(cs) # 93 and 95 are not naturally occurring. @@ -532,7 +542,7 @@ def test_topLevelComponentInput(self): without requiring a parent. """ cs = settings.Settings() - design = blueprints.Blueprints.load(self.yamlString) + design = blueprints.Blueprints.load(self._yamlString) # The following is needed to prep customisotopics # which is required during construction of a component design._resolveNuclides(cs) @@ -543,15 +553,12 @@ def test_topLevelComponentInput(self): self.assertGreater(topComponent.getMass("U235"), 0.0) def test_componentGroupInput(self): - """ - Make sure component groups can be input in blueprints. - """ - design = blueprints.Blueprints.load(self.yamlString) + """Make sure component groups can be input in blueprints.""" + design = blueprints.Blueprints.load(self._yamlString) componentGroup = design.componentGroups["group1"] self.assertEqual(componentGroup["freefuel"].name, "freefuel") self.assertEqual(componentGroup["freefuel"].mult, 1.0) if __name__ == "__main__": - # import sys;sys.argv = ['', 'TestBlueprints.test_nuclides']] unittest.main() diff --git a/armi/reactor/components/basicShapes.py b/armi/reactor/components/basicShapes.py index d1d3d2073..595efe3b0 100644 --- a/armi/reactor/components/basicShapes.py +++ b/armi/reactor/components/basicShapes.py @@ -17,8 +17,14 @@ 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: IMPL_REACTOR_SHAPES_0 + :links: REQ_REACTOR_SHAPES + + Here ARMI implements its support for: Circles, Hexagons, Rectangles, Solid Rectangles, + Squares, and Triangles. +""" import math from armi.reactor.components import ShapedComponent diff --git a/armi/reactor/components/complexShapes.py b/armi/reactor/components/complexShapes.py index 5cc4c8ef3..90839457c 100644 --- a/armi/reactor/components/complexShapes.py +++ b/armi/reactor/components/complexShapes.py @@ -14,6 +14,13 @@ """ 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 + + Here ARMI implements its support for: Holed Hexagons, Holed Rectangles, + Holed Squares, and Helixes. """ import math diff --git a/armi/reactor/components/component.py b/armi/reactor/components/component.py index 8b8ad9ee5..5a61c92f0 100644 --- a/armi/reactor/components/component.py +++ b/armi/reactor/components/component.py @@ -17,8 +17,8 @@ This module contains the abstract definition of a Component. """ -import re import copy +import re import numpy @@ -180,6 +180,10 @@ 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: IMPL_REACTOR_THERMAL_EXPANSION_0 + :links: REQ_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 6e8987b8e..597d09ec9 100644 --- a/armi/reactor/components/volumetricShapes.py +++ b/armi/reactor/components/volumetricShapes.py @@ -12,7 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""3-dimensional shapes.""" +"""3-dimensional shapes + + +.. impl:: ARMI supports a reasonable set of basic shapes. + :id: IMPL_REACTOR_SHAPES_2 + :links: REQ_REACTOR_SHAPES + + Here ARMI implements its support for: Spheres, Cubes, Toruses, and more. +""" import math diff --git a/armi/reactor/converters/tests/test_geometryConverters.py b/armi/reactor/converters/tests/test_geometryConverters.py index 681da8b56..3bdb4e68b 100644 --- a/armi/reactor/converters/tests/test_geometryConverters.py +++ b/armi/reactor/converters/tests/test_geometryConverters.py @@ -325,12 +325,16 @@ def test_growToFullCoreFromThirdCore(self): ), ) initialNumBlocks = len(self.r.core.getBlocks()) + # Perform reactor conversion changer = geometryConverters.ThirdCoreHexToFullCoreChanger(self.o.cs) changer.convert(self.r) + # Check the full core conversion is successful + 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) + # Check that the geometry can be restored to a third core changer.restorePreviousGeometry(self.o.cs, self.r) self.assertEqual(initialNumBlocks, len(self.r.core.getBlocks())) @@ -340,6 +344,7 @@ def test_growToFullCoreFromThirdCore(self): geometry.DomainType.THIRD_CORE, geometry.BoundaryType.PERIODIC ), ) + self.assertFalse(self.r.core.isFullCore) 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.""" diff --git a/armi/reactor/converters/tests/test_uniformMesh.py b/armi/reactor/converters/tests/test_uniformMesh.py index 1bce3f4dd..71e5e0ce0 100644 --- a/armi/reactor/converters/tests/test_uniformMesh.py +++ b/armi/reactor/converters/tests/test_uniformMesh.py @@ -15,8 +15,8 @@ """ Tests for the uniform mesh geometry converter """ -import unittest import random +import unittest import numpy @@ -37,7 +37,8 @@ class TestUniformMeshComponents(unittest.TestCase): @classmethod def setUpClass(cls): - random.seed(987324987234) # so it's always the same + # random seed to support random mesh in unit tests below + random.seed(987324987234) cls.o, cls.r = test_reactors.loadTestReactor( TEST_ROOT, customSettings={"xsKernel": "MC2v2"} ) @@ -51,9 +52,6 @@ def setUp(self): self.converter._sourceReactor = self.r def test_computeAverageAxialMesh(self): - """ - :req:`REQ5cabd0da-b92d-4bc1-b653-8c2139697582` - """ refMesh = self.r.core.findAllAxialMeshPoints( [self.r.core.getFirstAssembly(Flags.FUEL)] )[1:] diff --git a/armi/reactor/grids.py b/armi/reactor/grids.py index a10996615..84ac7cdd0 100644 --- a/armi/reactor/grids.py +++ b/armi/reactor/grids.py @@ -621,6 +621,10 @@ class 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: IMPL_REACTOR_MESH_0 + :links: REQ_REACTOR_MESH """ def __init__( @@ -1145,6 +1149,9 @@ class CartesianGrid(Grid): 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: IMPL_REACTOR_MESH_1 + :links: REQ_REACTOR_MESH """ @classmethod @@ -1380,6 +1387,10 @@ class HexGrid(Grid): (-1, 1) ( 0, 0) ( 1,-1) (-1, 0) ( 0,-1) + + .. impl:: ARMI supports a Hexagonal mesh. + :id: IMPL_REACTOR_MESH_2 + :links: REQ_REACTOR_MESH """ @staticmethod @@ -1747,6 +1758,10 @@ class ThetaRZGrid(Grid): meshes. See Figure 2.2 in Derstine 1984, ANL. [DIF3D]_. + + .. impl:: ARMI supports an RZTheta mesh. + :id: IMPL_REACTOR_MESH_3 + :links: REQ_REACTOR_MESH """ @staticmethod diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index 062d8cc09..a7acfccae 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -20,35 +20,41 @@ 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: IMPL_REACTOR_HIERARCHY_0 + :links: REQ_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 import copy import itertools import logging -from typing import Optional +import os import tabulate import time -import os import numpy from armi import getPluginManagerOrFail, materials, nuclearDataIO, settings +from armi.nuclearDataIO import xsLibraries from armi.reactor import assemblies from armi.reactor import assemblyLists from armi.reactor import composites from armi.reactor import geometry -from armi.reactor import systemLayoutInput from armi.reactor import grids from armi.reactor import parameters -from armi.reactor import zones from armi.reactor import reactorParameters +from armi.reactor import systemLayoutInput +from armi.reactor import zones +from armi.reactor.flags import Flags +from armi.settings.fwSettings.globalSettings import CONF_MATERIAL_NAMESPACE_ORDER from armi.utils import createFormattedStrWithDelimiter, units -from armi.utils.iterables import Sequence from armi.utils import directoryChangers +from armi.utils.iterables import Sequence from armi.utils.mathematics import average1DWithinTolerance -from armi.reactor.flags import Flags -from armi.settings.fwSettings.globalSettings import CONF_MATERIAL_NAMESPACE_ORDER -from armi.nuclearDataIO import xsLibraries # init logger runLog = logging.getLogger(__name__) @@ -694,7 +700,7 @@ def countBlocksWithFlags(self, blockTypeSpec, assemTypeSpec=None): try: return max(sum(b.hasFlags(blockTypeSpec) for b in a) for a in assems) except ValueError: - ## In case assems is empty + # In case assems is empty return 0 def countFuelAxialBlocks(self): @@ -711,7 +717,7 @@ def countFuelAxialBlocks(self): ) try: return max(len(fuel) for fuel in fuelblocks) - except ValueError: ## thrown when iterator is empty + except ValueError: # thrown when iterator is empty return 0 def getFirstFuelBlockAxialNode(self): @@ -730,13 +736,13 @@ def getFirstFuelBlockAxialNode(self): if b.hasFlags(Flags.FUEL) ) except ValueError: - ## ValueError is thrown if min is called on an empty sequence. - ## Since this is expected to be a rare case, try/except is more - ## efficient than an if/else condition that checks whether the - ## iterator is empty (the latter would require generating a list - ## or tuple, which further adds to the inefficiency). Hence Python's - ## mantra, "It's easier to ask forgiveness than permission." In fact - ## it's quicker to ask forgiveness than permission. + """ValueError is thrown if min is called on an empty sequence. + Since this is expected to be a rare case, try/except is more + efficient than an if/else condition that checks whether the + iterator is empty (the latter would require generating a list + or tuple, which further adds to the inefficiency). Hence Python's + mantra, "It's easier to ask forgiveness than permission." In fact + it's quicker to ask forgiveness than permission.""" return float("inf") def getAssembliesInRing( @@ -835,7 +841,7 @@ def getAssembliesInSquareOrHexRing( exclusions = set(exclusions) assems.drop(lambda a: a in exclusions) - ## filter based on geomType + # filter based on geomType if ( self.geomType == geometry.GeomType.CARTESIAN ): # a ring in cartesian is basically a square. @@ -845,7 +851,7 @@ def getAssembliesInSquareOrHexRing( else: assems.select(lambda a: (a.spatialLocator.getRingPos()[0] == ring)) - ## filter based on typeSpec + # filter based on typeSpec if typeSpec: assems.select(lambda a: a.hasFlags(typeSpec, exact=exactType)) @@ -895,12 +901,12 @@ def getAssembliesInCircularRing( assems = Sequence(self) - ## Remove exclusions + # Remove exclusions if exclusions: exclusions = set(exclusions) assems.drop(lambda a: a in exclusions) - ## get assemblies at locations + # get assemblies at locations locSet = self.circularRingList[ring] assems.select(lambda a: a.getLocation() in locSet) @@ -932,8 +938,8 @@ def buildCircularRingDictionary(self, ringPitch=1.0): for a in self: dist = a.spatialLocator.distanceTo(refLocation) - ## To reduce numerical sensitivity, round distance to 6 decimal places - ## before truncating. + # To reduce numerical sensitivity, round distance to 6 decimal places + # before truncating. index = int(round(dist * pitchFactor, 6)) or 1 # 1 is the smallest ring. circularRingDict[index].add(a.getLocation()) @@ -984,7 +990,7 @@ def _getAssembliesByName(self): """ runLog.extra("Generating assemblies-by-name map.") - ## NOTE: eliminated unnecessary repeated lookups in self for self.assembliesByName + # NOTE: eliminated unnecessary repeated lookups in self for self.assembliesByName self.assembliesByName = assymap = {} # don't includeAll b/c detailed ones are not ready yet for assem in self.getAssemblies(includeBolAssems=True, includeSFP=True): @@ -1130,9 +1136,9 @@ def _genBlocksByName(self): block.getName(): block for block in self.getBlocks(includeAll=True) } - ## An idea: wrap this in an "if not self.blocksByLocName:" - ## This will likely fail, but it will help diagnose why property approach - ## wasn't working correctly + # TODO: (Idea) wrap this in an "if not self.blocksByLocName:" + # This will likely fail, but it will help diagnose why property approach + # wasn't working correctly def genBlocksByLocName(self): """ If self.blocksByLocName is deleted, then this will regenerate it or update it if things change @@ -1219,7 +1225,7 @@ def getFirstAssembly(self, typeSpec=None, exact=False): runLog.warning("No assem of type {0} in reactor".format(typeSpec)) return None - ## Assumes at least one assembly in `self`. + # Assumes at least one assembly in `self` return next(iter(self)) def regenAssemblyLists(self): @@ -1355,8 +1361,8 @@ def getLocationContents(self, locs, assemblyLevel=False, locContents=None): makeLocationLookup : allows caching to speed this up if you call it a lot. """ - ## Why isn't locContents an attribute of reactor? It could be another - ## property that is generated on demand + # Why isn't locContents an attribute of reactor? It could be another + # property that is generated on demand if not locContents: locContents = self.makeLocationLookup(assemblyLevel) try: @@ -1382,7 +1388,7 @@ def makeLocationLookup(self, assemblyLevel=False): else: return {b.getLocation(): b for a in self for b in a} - ## Can be cleaned up, but need test case to guard agains breakage + # TODO: Can be cleaned up, but need test case to guard agains breakage def getFluxVector( self, energyOrder=0, adjoint=False, extSrc=False, volumeIntegrated=True ): @@ -1651,7 +1657,7 @@ def _getReflectiveDuplicateAssembly(self, neighborLoc): def setMoveList(self, cycle, oldLoc, newLoc, enrichList, assemblyType, assemName): """Tracks the movements in terms of locations and enrichments.""" data = (oldLoc, newLoc, enrichList, assemblyType, assemName) - ## NOTE: moveList is actually a moveDict (misnomer) + # NOTE: moveList is actually a moveDict (misnomer) if self.moveList.get(cycle) is None: self.moveList[cycle] = [] if data in self.moveList[cycle]: diff --git a/armi/reactor/tests/test_components.py b/armi/reactor/tests/test_components.py index ee1dae7ee..b0af6000f 100644 --- a/armi/reactor/tests/test_components.py +++ b/armi/reactor/tests/test_components.py @@ -205,6 +205,12 @@ class TestShapedComponent(TestGeneralComponents): """Abstract class for all shaped components""" def test_preserveMassDuringThermalExpansion(self): + """Test that when we thermally expand any arbirtray 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 + """ if not self.component.THERMAL_EXPANSION_DIMS: return temperatures = [25.0, 30.0, 40.0, 60.0, 80.0, 430.0] @@ -288,7 +294,13 @@ class TestCircle(TestShapedComponent): "mult": 1.5, } - def test_getThermalExpansionFactorConserveMassByLinearExpansionPercent(self): + 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 + """ hotTemp = 700.0 dLL = self.component.material.linearExpansionFactor( Tc=hotTemp, T0=self._coldTemp @@ -304,6 +316,12 @@ 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: TEST_REACTOR_THERMAL_EXPANSION_2 + :links: REQ_REACTOR_THERMAL_EXPANSION + """ self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) def test_getBoundingCircleOuterDiameter(self): @@ -384,20 +402,12 @@ def test_componentInteractionsLinkingBySubtraction(self): self.assertAlmostEqual(cur, ref) def test_getNumberDensities(self): - """ - Test that demonstrates that number densities can be retrieved on from component. - - :req:`REQ378c720f-987b-4fa8-8a2b-aba557aaa744` - """ + """Test that demonstrates that number densities can be retrieved on from component.""" self.component.p.numberDensities = {"NA23": 1.0} self.assertEqual(self.component.getNumberDensity("NA23"), 1.0) def test_changeNumberDensities(self): - """ - Test that demonstates that the number densities on a component can be modified. - - :req:`REQc263722f-3a59-45ef-903a-6276fc99cb40` - """ + """Test that demonstates that the number densities on a component can be modified.""" self.component.p.numberDensities = {"NA23": 1.0} self.assertEqual(self.component.getNumberDensity("NA23"), 1.0) self.component.changeNDensByFactor(3.0) @@ -423,6 +433,12 @@ 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: TEST_REACTOR_THERMAL_EXPANSION_3 + :links: REQ_REACTOR_THERMAL_EXPANSION + """ self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) def test_dimensionThermallyExpands(self): @@ -481,6 +497,12 @@ 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: TEST_REACTOR_THERMAL_EXPANSION_4 + :links: REQ_REACTOR_THERMAL_EXPANSION + """ self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) def test_dimensionThermallyExpands(self): @@ -521,6 +543,12 @@ 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: TEST_REACTOR_THERMAL_EXPANSION_5 + :links: REQ_REACTOR_THERMAL_EXPANSION + """ self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) def test_dimensionThermallyExpands(self): @@ -573,6 +601,12 @@ 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: TEST_REACTOR_THERMAL_EXPANSION_6 + :links: REQ_REACTOR_THERMAL_EXPANSION + """ self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) def test_dimensionThermallyExpands(self): @@ -632,6 +666,12 @@ 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: TEST_REACTOR_THERMAL_EXPANSION_7 + :links: REQ_REACTOR_THERMAL_EXPANSION + """ self.assertFalse(self.component.THERMAL_EXPANSION_DIMS) @@ -660,6 +700,12 @@ 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: TEST_REACTOR_THERMAL_EXPANSION_8 + :links: REQ_REACTOR_THERMAL_EXPANSION + """ self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) def test_dimensionThermallyExpands(self): @@ -698,6 +744,12 @@ 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: TEST_REACTOR_THERMAL_EXPANSION_9 + :links: REQ_REACTOR_THERMAL_EXPANSION + """ self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) def test_dimensionThermallyExpands(self): @@ -776,6 +828,9 @@ def setClassDims(self): # This enables subclassing testing for square self.width = self.length = self.component.getDimension("widthOuter") + def test_thermallyExpands(self): + self.assertTrue(self.component.THERMAL_EXPANSION_DIMS) + class TestHelix(TestShapedComponent): componentCls = Helix diff --git a/armi/reactor/tests/test_grids.py b/armi/reactor/tests/test_grids.py index 2639b9c16..0a89ba7c9 100644 --- a/armi/reactor/tests/test_grids.py +++ b/armi/reactor/tests/test_grids.py @@ -194,6 +194,13 @@ def test_getitem(self): 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 + """ + def testPositions(self): grid = grids.HexGrid.fromPitch(1.0) side = 1.0 / math.sqrt(3) @@ -401,6 +408,13 @@ def test_getIndexBounds(self): 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 + """ + def testPositions(self): grid = grids.ThetaRZGrid( bounds=(numpy.linspace(0, 2 * math.pi, 13), [0, 2, 2.5, 3], [0, 10, 20, 30]) @@ -417,6 +431,13 @@ def testPositions(self): 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 + """ + def testRingPosNoSplit(self): grid = grids.CartesianGrid.fromRectangle(1.0, 1.0, isOffset=True) diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 43d12aa33..1c630d081 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -269,6 +269,12 @@ def test_countBlocksOfType(self): self.assertEqual(numControlBlocks, 3) 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 + """ numFuelBlocks = self.r.core.countFuelAxialBlocks() self.assertEqual(numFuelBlocks, 3) @@ -398,7 +404,6 @@ def test_findAllRadMeshPoints(self): assert_allclose(expectedPoints, radPoints) def test_findNeighbors(self): - loc = self.r.core.spatialGrid.getLocatorFromRingAndPos(1, 1) a = self.r.core.childrenByLocator[loc] neighbs = self.r.core.findNeighbors( @@ -521,6 +526,12 @@ def test_getAssembly(self): self.assertEqual(a1, a3) def test_countAssemblies(self): + """Tests that the users definition of assemblies is preserved. + + .. test:: Tests that the users definition of assembilies is preserved. + :id: TEST_REACTOR_3 + :links: REQ_REACTOR + """ nFuel = self.r.core.countAssemblies(Flags.FUEL) self.assertEqual(2, nFuel) nFuel_r3 = self.r.core.countAssemblies(Flags.FUEL, ring=3) diff --git a/armi/settings/tests/old_xml_settings_input.xml b/armi/settings/tests/old_xml_settings_input.xml index 7ca6704d8..3e8a3f550 100644 --- a/armi/settings/tests/old_xml_settings_input.xml +++ b/armi/settings/tests/old_xml_settings_input.xml @@ -10,7 +10,7 @@ - + @@ -50,4 +50,4 @@ - \ No newline at end of file + diff --git a/armi/utils/__init__.py b/armi/utils/__init__.py index df0b728e9..a9713c773 100644 --- a/armi/utils/__init__.py +++ b/armi/utils/__init__.py @@ -228,6 +228,7 @@ def classesInHierarchy(obj, classCounts, visited=None): raise TypeError( "Need to pass in a default dict for classCounts (it's an out param)" ) + if visited is None: classCounts[type(obj)] += 1 visited = set() diff --git a/armi/utils/iterables.py b/armi/utils/iterables.py index d6a9e7905..a318ed3d4 100644 --- a/armi/utils/iterables.py +++ b/armi/utils/iterables.py @@ -193,7 +193,7 @@ class Sequence: ... i += 1 ... >>> s = Sequence(counter()).select(lambda i: i < 10) - >>> tuple(s) ## DON'T DO THIS! + >>> tuple(s) # DON'T DO THIS! Although the result should be (0,1,2,3,4,5,6,7,8,9), the select method is not smart enough to know that it's a terminal condition and will continue to diff --git a/armi/utils/tests/test_utils.py b/armi/utils/tests/test_utils.py index 77fc46d7f..2d4371aac 100644 --- a/armi/utils/tests/test_utils.py +++ b/armi/utils/tests/test_utils.py @@ -15,12 +15,14 @@ r""" Testing some utility functions """ # pylint: disable=missing-function-docstring,missing-class-docstring,abstract-method,protected-access,too-many-public-methods,invalid-name -import unittest +from collections import defaultdict import math +import unittest import numpy as np from armi import utils +from armi.reactor.tests.test_reactors import loadTestReactor from armi.utils import directoryChangers @@ -121,6 +123,32 @@ def test_plotMatrix(self): utils.plotMatrix(matrix, fname, minV=0, maxV=5, figsize=[3, 4]) 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: TEST_REACTOR_HIERARCHY_0 + :links: REQ_REACTOR_HIERARCHY + + This test shows that the Blocks and Assemblies are stored + heirarchically inside the Core, which is inside the Reactor object. + """ + # load the test reactor + o, r = loadTestReactor() + + # call the `classesInHierarchy` function + classCounts = defaultdict(lambda: 0) + utils.classesInHierarchy(r, classCounts, None) + + # validate the `classesInHierarchy` function + self.assertGreater(len(classCounts), 30) + self.assertEqual(classCounts[type(r)], 1) + self.assertEqual(classCounts[type(r.core)], 1) + + # further validate the Reactor heirarchy is in place + self.assertGreater(len(r.core.getAssemblies()), 50) + self.assertGreater(len(r.core.getBlocks()), 200) + if __name__ == "__main__": unittest.main() diff --git a/doc/conf.py b/doc/conf.py index 578aabf3c..07e588219 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -77,8 +77,17 @@ def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): ) +def autodoc_skip_member_handler(app, what, name, obj, skip, options): + """Manually exclude certain methods/functions from docs""" + # exclude special methods from unittest + excludes = ["setUp", "setUpClass", "tearDown", "tearDownClass"] + return name.startswith("_") or name in excludes + + def setup(app): """Method to make `python setup.py build_sphinx` generate api documentation""" + app.connect("autodoc-skip-member", autodoc_skip_member_handler) + app.add_domain(PatchedPythonDomain, override=True) app.add_directive("exec", ExecDirective) @@ -99,7 +108,7 @@ def setup(app): # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -# needs_sphinx = '1.0' +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. @@ -121,6 +130,8 @@ def setup(app): "sphinxext.opengraph", "sphinx_gallery.gen_gallery", "sphinx.ext.imgconverter", # to convert GH Actions badge SVGs to PNG for LaTeX + "sphinxcontrib.plantuml", + "sphinxcontrib.needs", ] # Our API should make sense without documenting private/special members. @@ -134,7 +145,7 @@ def setup(app): apidoc_module_dir = SOURCE_DIR apidoc_output_dir = APIDOC_REL -apidoc_excluded_paths = ["tests", "*/test*"] +apidoc_excluded_paths = ["*/test_gridGui.py"] apidoc_separate_modules = True apidoc_module_first = True @@ -163,9 +174,6 @@ def setup(app): # The suffix of source filenames. source_suffix = ".rst" -# The encoding of source files. -# source_encoding = 'utf-8-sig' - # The master toctree document. master_doc = "index" @@ -173,25 +181,10 @@ def setup(app): project = "ARMI" copyright = "2009-{}, TerraPower, LLC".format(datetime.datetime.now().year) -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = armi.__version__ #'.'.join(armi.__version__.split('.')[:2]) -# The full version, including alpha/beta/rc tags. +# Use the pre-existing version definition. +version = armi.__version__ release = armi.__version__ -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# today = '' -# Else, today_fmt is used as the format for a strftime call. -# today_fmt = '%B %d, %Y' - # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. """ @@ -207,12 +200,6 @@ def setup(app): "_build", ] # , '**/tests*'] -# The reST default role (used for this markup: `text`) to use for all documents. -# default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True - rst_epilog = r""" .. |keff| replace:: k\ :sub:`eff`\ """ @@ -224,14 +211,6 @@ def setup(app): ) } -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# show_authors = False - # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" @@ -259,17 +238,6 @@ def setup(app): # Add any paths that contain custom themes here, relative to this directory. html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -# html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# html_logo = None - # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. @@ -284,43 +252,6 @@ def setup(app): # using the given strftime format. html_last_updated_fmt = "%Y-%m-%d" -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -# html_sidebars = {'**': ['localtoc.html']} # enable this is you want a sidebar nav, it doesn't work super super well though, more options for functionality include - 'sourcelink.html', 'searchbox.html' - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# html_additional_pages = {} - -# If false, no module index is generated. -# html_domain_indices = True - -# If false, no index is generated. -# html_use_index = True - -# If true, the index is split into individual pages for each letter. -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - # Output file base name for HTML help builder. htmlhelp_basename = "ARMIdoc" diff --git a/doc/developer/tooling.rst b/doc/developer/tooling.rst index cd4a6b3e8..011ca3bc6 100644 --- a/doc/developer/tooling.rst +++ b/doc/developer/tooling.rst @@ -69,6 +69,8 @@ might look like ``0.1.7``, ``1.0.0``, or ``1.2.123``. Each number has a specific this might happen once a year. * ``bump`` - Revved every time we modify the API, or at will; any time we want. +NOTE: Changes to documenation or testing probably do not deserve a version bump. + **Any change to a major or minor version is considered a release.** Only a core member of the ARMI team may release a new version, or add a tag of any kind to diff --git a/doc/index.rst b/doc/index.rst index b8f039dc4..0b089c1a9 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -15,6 +15,7 @@ ARMI developer/index release/index glossary + requirements/index API Docs <.apidocs/modules> diff --git a/doc/requirements-docs.txt b/doc/requirements-docs.txt index d3d969e41..7c237a904 100644 --- a/doc/requirements-docs.txt +++ b/doc/requirements-docs.txt @@ -19,6 +19,10 @@ sphinxcontrib-apidoc==0.3.0 # generates OpenGraph metadata? sphinxext-opengraph==0.5.1 +# supporting requirement documentation +sphinxcontrib-needs==0.7.6 +sphinxcontrib-plantuml==0.23 + # This is not strictly necessary via PIP, but MUST be in the path. # Used to convert between file formats. pandoc diff --git a/doc/requirements/index.rst b/doc/requirements/index.rst new file mode 100644 index 000000000..e1d3d5ac2 --- /dev/null +++ b/doc/requirements/index.rst @@ -0,0 +1,23 @@ +############ +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 + * diff --git a/doc/requirements/srsd.rst b/doc/requirements/srsd.rst new file mode 100644 index 000000000..10db2958f --- /dev/null +++ b/doc/requirements/srsd.rst @@ -0,0 +1,90 @@ +************************************************** +Software Requirement Specification Document (SRSD) +************************************************** + +----------------------- +Functional Requirements +----------------------- + + +.. req:: ARMI shall be able to represent a user-specified reactor. + :id: REQ_REACTOR + :status: implemented, needs more tests + + Given user input describing a reactor system, 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 + :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 + :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 + :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 + :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 + :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 + :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 + :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 + :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 + :status: implemented, needs test + + The state shale be made available to users and modules, which may in turn modify the + state (e.g. for analysis or based on the results of a physical calculation). ARMI + shall fully define how all aspects of state may be accessed and modified and shall + reflect any new state after it is applied. + + State shall be represented as evolving eitehr through time (i.e. in a typical cycle- + by-cycle analysis) or through a series of control configurations.