Repository: terrapower/armi
Pull request #1928 (2024-10-04 18:19:59)

Removing unusable buildEqRingSchedule
## What is the change?

Removing unusable `buildEqRingSchedule()` function, and its helpers.

## Why is the change being made?

These functions reference external settings and code, and are not useable on their own in ARMI.

---

## Checklist

- [x] This PR has only [one purpose or idea](https://terrapower.github.io/armi/developer/tooling.html#one-idea-one-pr).
- [x] [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 -->

- [x] The code style follows [good practices](https://terrapower.github.io/armi/developer/standards_and_practices.html).
- [x] The commit message(s) follow [good practices](https://terrapower.github.io/armi/developer/tooling.html).

<!-- Check the project-level cruft -->

- [x] The [release notes](https://terrapower.github.io/armi/developer/tooling.html#add-release-notes) have been updated if necessary.
- [x] The [documentation](https://terrapower.github.io/armi/developer/tooling.html#document-it) is still up-to-date in the `doc` folder.
- [x] The dependencies are still up-to-date in `pyproject.toml`.

john-science: @john-science Test this downstream before merging.

Patch:
diff --git a/armi/bookkeeping/db/database3.py b/armi/bookkeeping/db/database3.py
index 4f85a2b9..a5056d88 100644
--- a/armi/bookkeeping/db/database3.py
+++ b/armi/bookkeeping/db/database3.py
@@ -223,6 +223,9 @@ class Database3:
         self.h5db.attrs["pluginPaths"] = ps
         self.h5db.attrs["localCommitHash"] = Database3.grabLocalCommitHash()
 
+    def isOpen(self):
+        return self.h5db is not None
+
     @staticmethod
     def writeSystemAttributes(h5db):
         """Write system attributes to the database.
@@ -1065,9 +1068,24 @@ class Database3:
             if "linkedDims" in attrs:
                 linkedDims = np.char.decode(attrs["linkedDims"])
 
+            unpackedData = data.tolist()
+            if len(comps) != len(unpackedData):
+                msg = (
+                    "While unpacking special data for {}, encountered "
+                    "composites and parameter data with unmatched sizes.\n"
+                    "Length of composites list = {}\n"
+                    "Length of data list = {}\n"
+                    "This could indicate an error in data unpacking, which could "
+                    "result in faulty data on the resulting reactor model.".format(
+                        paramName, len(comps), len(unpackedData)
+                    )
+                )
+                runLog.error(msg)
+                raise ValueError(msg)
+
             # iterating of np is not fast...
             for c, val, linkedDim in itertools.zip_longest(
-                comps, data.tolist(), linkedDims, fillvalue=""
+                comps, unpackedData, linkedDims, fillvalue=""
             ):
                 try:
                     if linkedDim != "":
diff --git a/armi/physics/fuelCycle/hexAssemblyFuelMgmtUtils.py b/armi/physics/fuelCycle/hexAssemblyFuelMgmtUtils.py
index e1c73608..9afe2d4c 100644
--- a/armi/physics/fuelCycle/hexAssemblyFuelMgmtUtils.py
+++ b/armi/physics/fuelCycle/hexAssemblyFuelMgmtUtils.py
@@ -19,13 +19,11 @@ Notes
 -----
 We are keeping these in ARMI even if they appear unused internally.
 """
-import math
-
 import numpy as np
 
 from armi import runLog
-from armi.utils.hexagon import getIndexOfRotatedCell
 from armi.reactor.flags import Flags
+from armi.utils.hexagon import getIndexOfRotatedCell
 from armi.utils.mathematics import findClosest
 
 
@@ -341,162 +339,3 @@ def buildConvergentRingSchedule(chargeRing, dischargeRing=1, coarseFactor=0.0):
 
     # step 4. assemble and return
     return convergent, conWidths
-
-
-def _buildEqRingScheduleHelper(ringSchedule, numRings):
-    r"""
-    turns ``ringScheduler`` into explicit list of rings.
-
-    Pulled out of buildEqRingSchedule for testing.
-
-    Parameters
-    ----------
-    ringSchedule : list
-        List of ring bounds that is required to be an even number of entries.  These
-        entries then are used in a from - to approach to add the rings.  The from ring will
-        always be included.
-
-    numRings : int
-        The number of rings in the hex assembly reactor.
-
-    Returns
-    -------
-    ringList : list
-        List of all rings in the order they should be shuffled.
-
-    Examples
-    --------
-    >>> _buildEqRingScheduleHelper([1,5])
-    [1,2,3,4,5]
-
-    >>> _buildEqRingScheduleHelper([1,5,9,6])
-    [1,2,3,4,5,9,8,7,6]
-
-    >>> _buildEqRingScheduleHelper([9,5,3,4,1,2])
-    [9,8,7,6,5,3,4,1,2]
-
-    >>> _buildEqRingScheduleHelper([2,5,1,1])
-    [2,3,4,5,1]
-    """
-    if len(ringSchedule) % 2 != 0:
-        runLog.error("Ring schedule: {}".format(ringSchedule))
-        raise RuntimeError("Ring schedule does not have an even number of entries.")
-
-    ringList = []
-    for i in range(0, len(ringSchedule), 2):
-        fromRing = ringSchedule[i]
-        toRing = ringSchedule[i + 1]
-        numRings = abs(toRing - fromRing) + 1
-
-        ringList.extend([int(j) for j in np.linspace(fromRing, toRing, numRings)])
-
-    # eliminate doubles (but allow a ring to show up multiple times)
-    newList = []
-    lastRing = None
-    for ring in ringList:
-        if ring != lastRing:
-            newList.append(ring)
-        if ring > numRings:
-            # error checking
-            runLog.warning(
-                "Ring {0} in eqRingSchedule larger than largest ring in reactor {1}. "
-                "Adjust shuffling.".format(ring, numRings),
-                single=True,
-                label="too many rings",
-            )
-        lastRing = ring
-
-    return newList
-
-
-def _squaredDistanceFromOrigin(a):
-    """Get the squared distance from the origin of an assembly.
-
-    Notes
-    -----
-    Just a helper for ``buildEqRingSchedule()``
-
-    Parameters
-    ----------
-    a: Assembly
-        Fully initialize Assembly object; already part of a reactor core.
-
-    Returns
-    -------
-    float: Distance from reactor center
-    """
-    origin = np.array([0.0, 0.0, 0.0])
-    p = np.array(a.spatialLocator.getLocalCoordinates())
-    return ((p - origin) ** 2).sum()
-
-
-def _assemAngle(a):
-    """Get the angle of the Assembly, in the reactor core.
-
-    Notes
-    -----
-    Just a helper for ``buildEqRingSchedule()``
-
-    Parameters
-    ----------
-    a: Assembly
-        Fully initialize Assembly object; already part of a reactor core.
-
-    Returns
-    -------
-    float: Angle position of assembly around the reactor core
-    """
-    x, y, _ = a.spatialLocator.getLocalCoordinates()
-    return math.atan2(y, x)
-
-
-def buildEqRingSchedule(core, ringSchedule, circularRingOrder):
-    r"""
-    Expands simple ``ringSchedule`` input into full-on location schedule.
-
-    Parameters
-    ----------
-    core : Core object
-        Fully initialized Core object, for a hex assembly reactor.
-
-    ringSchedule : list
-        List of ring bounds that is required to be an even number of entries.  These
-        entries then are used in a from - to approach to add the rings.  The from ring will
-        always be included.
-
-    circularRingOrder : str
-        From the circularRingOrder setting. Valid values include angle and distanceSmart,
-        anything else will
-
-    Returns
-    -------
-    list: location schedule
-    """
-    # start by expanding the user-input eqRingSchedule list into a list containing
-    # all the rings as it goes.
-    ringList = _buildEqRingScheduleHelper(ringSchedule, core.getNumRings())
-
-    # now build the locationSchedule ring by ring using this ringSchedule
-    lastRing = 0
-    locationSchedule = []
-    for ring in ringList:
-        assemsInRing = core.getAssembliesInRing(ring, typeSpec=Flags.FUEL)
-        if circularRingOrder == "angle":
-            sorter = lambda a: _assemAngle(a)
-        elif circularRingOrder == "distanceSmart":
-            if lastRing == ring + 1:
-                # converging. Put things on the outside first.
-                sorter = lambda a: -_squaredDistanceFromOrigin(a)
-            else:
-                # diverging. Put things on the inside first.
-                sorter = _squaredDistanceFromOrigin
-        else:
-            # purely based on distance. Can mix things up in convergent-divergent cases. Prefer distanceSmart
-            sorter = _squaredDistanceFromOrigin
-
-        assemsInRing = sorted(assemsInRing, key=sorter)
-        for a in assemsInRing:
-            locationSchedule.append(a.getLocation())
-        lastRing = ring
-
-    return locationSchedule
diff --git a/armi/reactor/assemblies.py b/armi/reactor/assemblies.py
index 0f756a5d..c26d74d6 100644
--- a/armi/reactor/assemblies.py
+++ b/armi/reactor/assemblies.py
@@ -1237,15 +1237,6 @@ class Assembly(composites.Composite):
 
         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. To perform the ``Block`` rotation, the
-            :py:meth:`armi.reactor.blocks.Block.rotate` method is called.
-
         Parameters
         ----------
         rad: float
@@ -1266,6 +1257,15 @@ class HexAssembly(Assembly):
     def rotate(self, rad: float):
         """Rotate an assembly and its children.
 
+        .. impl:: A hexagonal assembly shall support rotating around the z-axis in 60 degree increments.
+            :id: I_ARMI_ROTATE_HEX
+            :implements: R_ARMI_ROTATE_HEX
+
+            This method loops through every ``Block`` in this ``HexAssembly`` and rotates
+            it by a given angle (in radians). The rotation angle is positive in the
+            counter-clockwise direction. To perform the ``Block`` rotation, the
+            :meth:`armi.reactor.blocks.HexBlock.rotate` method is called.
+
         Parameters
         ----------
         rad : float
diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py
index cc18826a..7d9f4cb5 100644
--- a/armi/reactor/blocks.py
+++ b/armi/reactor/blocks.py
@@ -205,9 +205,10 @@ class Block(composites.Composite):
         all the area is fuel, it has 100% smear density. Lower smear density allows more room for
         swelling.
 
-        .. warning:: This requires circular fuel and circular cladding. Designs that vary
-            from this will be wrong. It may make sense in the future to put this somewhere a
-            bit more design specific.
+        Warning
+        -------
+        This requires circular fuel and circular cladding. Designs that vary from this will be
+        wrong. It may make sense in the future to put this somewhere a bit more design specific.
 
         Notes
         -----
@@ -225,12 +226,16 @@ class Block(composites.Composite):
 
         Returns
         -------
-        smearDensity : float
-            The smear density as a fraction
+        float
+            The smear density as a fraction.
         """
         fuels = self.getComponents(Flags.FUEL)
         if not fuels:
-            return 0.0  # Smear density is not computed for non-fuel blocks
+            # smear density is not computed for non-fuel blocks
+            return 0.0
+        elif not self.getNumPins():
+            # smear density is only defined for pinned blocks
+            return 0.0
 
         circles = self.getComponentsOfShape(components.Circle)
         if not circles:
diff --git a/doc/release/0.4.rst b/doc/release/0.4.rst
index 392d0e0d..73dd3640 100644
--- a/doc/release/0.4.rst
+++ b/doc/release/0.4.rst
@@ -28,6 +28,8 @@ API Changes
 #. Moving ``settingsValidation`` from ``operators`` to ``settings``. (`PR#1895 <https://github.com/terrapower/armi/pull/1895>`_)
 #. Removing deprecated method ``prepSearch``. (`PR#1845 <https://github.com/terrapower/armi/pull/1845>`_)
 #. Removing unused function ``SkippingXsGen_BuChangedLessThanTolerance``. (`PR#1845 <https://github.com/terrapower/armi/pull/1845>`_)
+#. Renaming ``Reactor.moveList`` to ``Reactor.moves``. (`PR#1881 <https://github.com/terrapower/armi/pull/1881>`_)
+#. Removing ``buildEqRingSchedule``. (`PR#1928 <https://github.com/terrapower/armi/pull/1928>`_)
 #. Allowing for unknown Flags when opening a DB. (`PR#1844 <https://github.com/terrapower/armi/pull/1835>`_)
 #. Removing ``assemblyLists.py`` and the ``AssemblyList`` class. (`PR#1891 <https://github.com/terrapower/armi/pull/1891>`_)
 #. Removing ``Assembly.rotatePins`` and ``Block.rotatePins``. Prefer ``Assembly.rotate`` and ``Block.rotate``. (`PR#1846 <https://github.com/terrapower/armi/1846`_)
@@ -51,6 +53,7 @@ Quality Work
 ------------
 #. Removing deprecated code ``axialUnitGrid``. (`PR#1809 <https://github.com/terrapower/armi/pull/1809>`_)
 #. Refactoring ``axialExpansionChanger``. (`PR#1861 <https://github.com/terrapower/armi/pull/1861>`_)
+#. Raise a ValueError when database load fails. (`PR#1940 <https://github.com/terrapower/armi/pull/1940>`_)
 #. TBD
 
 Changes that Affect Requirements


Test patch:
diff --git a/armi/bookkeeping/db/tests/test_database3.py b/armi/bookkeeping/db/tests/test_database3.py
index e918ee7c..9a413da5 100644
--- a/armi/bookkeeping/db/tests/test_database3.py
+++ b/armi/bookkeeping/db/tests/test_database3.py
@@ -841,3 +841,20 @@ grids:
             self.assertEqual(len(evst.getChildren()), 1)
             b1 = evst.getChildren()[0].getChildren()[0]
             self.assertEqual(b1.p.power, 12345.6)
+
+    def test_badData(self):
+        # create a DB to be modified
+        self.db.writeToDB(self.r)
+        self.db.close()
+
+        # modify the HDF5 file to corrupt a dataset
+        with h5py.File(self.db.fileName, "r+") as hf:
+            circleGroup = hf["c00n00"]["Circle"]
+            circleMass = np.array(circleGroup["massHmBOL"][()])
+            badData = circleMass[:-1]
+            del circleGroup["massHmBOL"]
+            circleGroup.create_dataset("massHmBOL", data=badData)
+
+        with self.assertRaises(ValueError):
+            with database3.Database3(self.db.fileName, "r") as db:
+                _r = db.load(0, 0, allowMissing=True)
diff --git a/armi/physics/fuelCycle/tests/test_hexAssemblyFuelMgmtUtils.py b/armi/physics/fuelCycle/tests/test_hexAssemblyFuelMgmtUtils.py
index 2b237b50..1155ec3a 100644
--- a/armi/physics/fuelCycle/tests/test_hexAssemblyFuelMgmtUtils.py
+++ b/armi/physics/fuelCycle/tests/test_hexAssemblyFuelMgmtUtils.py
@@ -13,55 +13,18 @@
 # limitations under the License.
 """Tests some fuel handling tools, specific to hex-assembly reactors."""
 from armi.physics.fuelCycle import hexAssemblyFuelMgmtUtils as hexUtils
-from armi.reactor.tests import test_reactors
-from armi.tests import ArmiTestHelper, TEST_ROOT
+from armi.tests import ArmiTestHelper
 from armi.utils import directoryChangers
 
 
 class TestHexAssemMgmtTools(ArmiTestHelper):
     def setUp(self):
-        # prepare the input files. This is important so the unit tests run from wherever
-        # they need to run from.
         self.td = directoryChangers.TemporaryDirectoryChanger()
         self.td.__enter__()
 
     def tearDown(self):
         self.td.__exit__(None, None, None)
 
-    def test_buildEqRingScheduleHelper(self):
-        numRings = 9
-        ringList1 = [1, 5]
-        buildRing1 = hexUtils._buildEqRingScheduleHelper(ringList1, numRings)
-        self.assertEqual(buildRing1, [1, 2, 3, 4, 5])
-
-        ringList2 = [1, 5, 9, 6]
-        buildRing2 = hexUtils._buildEqRingScheduleHelper(ringList2, numRings)
-        self.assertEqual(buildRing2, [1, 2, 3, 4, 5, 9, 8, 7, 6])
-
-        ringList3 = [9, 5, 3, 4, 1, 2]
-        buildRing3 = hexUtils._buildEqRingScheduleHelper(ringList3, numRings)
-        self.assertEqual(buildRing3, [9, 8, 7, 6, 5, 3, 4, 1, 2])
-
-        ringList4 = [2, 5, 1, 1]
-        buildRing1 = hexUtils._buildEqRingScheduleHelper(ringList4, numRings)
-        self.assertEqual(buildRing1, [2, 3, 4, 5, 1])
-
-    def test_buildEqRingSchedule(self):
-        _o, r = test_reactors.loadTestReactor(TEST_ROOT)
-        core = r.core
-
-        circularRingOrder = "angle"
-        locSchedule = hexUtils.buildEqRingSchedule(core, [2, 1], circularRingOrder)
-        self.assertEqual(locSchedule, ["002-001", "002-002", "001-001"])
-
-        circularRingOrder = "distanceSmart"
-        locSchedule = hexUtils.buildEqRingSchedule(core, [2, 1], circularRingOrder)
-        self.assertEqual(locSchedule, ["002-001", "002-002", "001-001"])
-
-        circularRingOrder = "somethingCrazy"
-        locSchedule = hexUtils.buildEqRingSchedule(core, [2, 1], circularRingOrder)
-        self.assertEqual(locSchedule, ["002-001", "002-002", "001-001"])
-
     def test_buildConvergentRingSchedule(self):
         schedule, widths = hexUtils.buildConvergentRingSchedule(1, 17, 0)
         self.assertEqual(schedule, [1, 17])
diff --git a/armi/reactor/tests/test_assemblies.py b/armi/reactor/tests/test_assemblies.py
index 978e8b3e..ebc4cac8 100644
--- a/armi/reactor/tests/test_assemblies.py
+++ b/armi/reactor/tests/test_assemblies.py
@@ -1026,8 +1026,8 @@ class Assembly_TestCase(unittest.TestCase):
         """Test rotation of an assembly spatial objects.
 
         .. test:: An assembly can be rotated about its z-axis.
-            :id: T_ARMI_SHUFFLE_ROTATE
-            :tests: R_ARMI_SHUFFLE_ROTATE
+            :id: T_ARMI_ROTATE_HEX
+            :tests: R_ARMI_ROTATE_HEX
         """
         a = makeTestAssembly(1, 1)
         b = blocks.HexBlock("TestBlock")
diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py
index 08f729e9..d7b7f2a0 100644
--- a/armi/reactor/tests/test_blocks.py
+++ b/armi/reactor/tests/test_blocks.py
@@ -395,6 +395,21 @@ class Block_TestCase(unittest.TestCase):
         ) / self.block.getDim(Flags.INNER | Flags.LINER, "id") ** 2
         self.assertAlmostEqual(cur, ref, places=10)
 
+    def test_getSmearDensityEdgeCases(self):
+        # show smear density is not computed for non-fuel blocks
+        b0 = blocks.Block("DummyReflectorBlock")
+        self.assertEqual(b0.getSmearDensity(), 0.0)
+
+        # show smear density is only defined for pinned fuel blocks
+        b1 = blocks.HexBlock("TestFuelHexBlock")
+        b1.setType("fuel")
+        b1.p.nPins = 0
+        fuel = components.Circle(
+            "fuel", "UZr", Tinput=25.0, Thot=25.0, od=0.84, id=0.6, mult=0
+        )
+        b1.add(fuel)
+        self.assertEqual(b1.getSmearDensity(), 0.0)
+
     def test_timeNodeParams(self):
         self.block.p["buRate", 3] = 0.1
         self.assertEqual(0.1, self.block.p[("buRate", 3)])
diff --git a/armi/tests/test_lwrInputs.py b/armi/tests/test_lwrInputs.py
index 1207ea47..a7b4e631 100644
--- a/armi/tests/test_lwrInputs.py
+++ b/armi/tests/test_lwrInputs.py
@@ -16,32 +16,23 @@ from logging import WARNING
 import os
 import unittest
 
-import numpy as np
-
-from armi import Mode
 from armi import runLog
-from armi import init as armi_init
-from armi.bookkeeping import db
 from armi.reactor.flags import Flags
 from armi.tests import mockRunLogs
 from armi.tests import TEST_ROOT
 from armi.utils import directoryChangers
+from armi.reactor.tests import test_reactors
 
-TEST_INPUT_TITLE = "c5g7-settings"
+TEST_INPUT_TITLE = "c5g7-settings.yaml"
 
 
 class C5G7ReactorTests(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls):
-
-        cls.directoryChanger = directoryChangers.DirectoryChanger(
-            os.path.join(TEST_ROOT, "tutorials")
-        )
-        cls.directoryChanger.open()
+    def setUp(self):
+        self.td = directoryChangers.TemporaryDirectoryChanger()
+        self.td.__enter__()
 
-    @classmethod
-    def tearDownClass(cls):
-        cls.directoryChanger.close()
+    def tearDown(self):
+        self.td.__exit__(None, None, None)
 
     def test_loadC5G7(self):
         """
@@ -54,44 +45,22 @@ class C5G7ReactorTests(unittest.TestCase):
             runLog.LOG.startLog("test_loadC5G7")
             runLog.LOG.setVerbosity(WARNING)
 
-            # ingest the settings file
-            Mode.setMode(Mode.BATCH)
-            o = armi_init(fName=TEST_INPUT_TITLE + ".yaml")
-            b = o.r.core.getFirstBlock(Flags.MOX)
+            # load the reactor
+            _o, r = test_reactors.loadTestReactor(
+                os.path.join(TEST_ROOT, "tutorials"),
+                inputFileName=TEST_INPUT_TITLE,
+            )
 
             # test warnings are being logged for malformed isotopics info in the settings file
             streamVal = mock.getStdout()
-            self.assertIn("UraniumOxide", streamVal, msg=streamVal)
-            self.assertIn("SaturatedWater", streamVal, msg=streamVal)
+            self.assertIn("Case Information", streamVal, msg=streamVal)
+            self.assertIn("Input File", streamVal, msg=streamVal)
 
             # test that there are 100 of each high, medium, and low MOX pins
+            b = r.core.getFirstBlock(Flags.MOX)
             fuelPinsHigh = b.getComponent(Flags.HIGH | Flags.MOX)
             self.assertEqual(fuelPinsHigh.getDimension("mult"), 100)
 
             # test the Guide Tube dimensions
             gt = b.getComponent(Flags.GUIDE_TUBE)
             self.assertEqual(gt.getDimension("mult"), 24)
-
-    def test_runAndLoadC5G7(self):
-        """
-        Run C5G7 in basic no-op app and load from the result from DB.
-
-        This ensures that these kinds of cases can be read from DB.
-        """
-
-        def loadLocs(o, locs):
-            for b in o.r.core.getBlocks():
-                indices = b.spatialLocator.getCompleteIndices()
-                locs[indices] = b.spatialLocator.getGlobalCoordinates()
-
-        o = armi_init(fName=TEST_INPUT_TITLE + ".yaml")
-        locsInput, locsDB = {}, {}
-        loadLocs(o, locsInput)
-        with directoryChangers.TemporaryDirectoryChanger():
-            o.operate()
-            o2 = db.loadOperator(TEST_INPUT_TITLE + ".h5", 0, 0)
-        loadLocs(o2, locsDB)
-
-        for indices, coordsInput in sorted(locsInput.items()):
-            coordsDB = locsDB[indices]
-            self.assertTrue(np.allclose(coordsInput, coordsDB))
diff --git a/armi/tests/tutorials/c5g7-settings.yaml b/armi/tests/tutorials/c5g7-settings.yaml
index 815c246a..2025cb08 100644
--- a/armi/tests/tutorials/c5g7-settings.yaml
+++ b/armi/tests/tutorials/c5g7-settings.yaml
@@ -13,6 +13,9 @@ settings:
   versions:
     armi: uncontrolled
 
+# database
+  db: true
+
 # neutronics
   genXS: Neutron