Repository: terrapower/armi

Issue:
Set Delayed Neutron Fraction on Core from Neutronics Plugin
#93 introduced a change to the `Core` where the delayed neutron fraction and precursor decay constants could be added to the core with user settings during initialization. It would be better to move this to the neutronics plugin within the `onProcessCoreLoading` plugin hook. 

This is a trivial change, but will help minimize neutronic-specific operations directly on the core. 

Discussion:

Patch:
diff --git a/armi/physics/neutronics/__init__.py b/armi/physics/neutronics/__init__.py
index 59e4fc5d..4cec01f6 100644
--- a/armi/physics/neutronics/__init__.py
+++ b/armi/physics/neutronics/__init__.py
@@ -34,10 +34,13 @@ independent interfaces:
 import os
 
 import yamlize
+import numpy
+import tabulate
 
 from armi import plugins
 from armi.physics.neutronics.const import CONF_CROSS_SECTION
 from armi.utils import directoryChangers
+from armi import runLog
 
 
 class NeutronicsPlugin(plugins.ArmiPlugin):
@@ -135,6 +138,11 @@ class NeutronicsPlugin(plugins.ArmiPlugin):
         )
         return queries
 
+    @staticmethod
+    @plugins.HOOKIMPL
+    def onProcessCoreLoading(core, cs):
+        applyEffectiveDelayedNeutronFractionToCore(core, cs)
+
 
 # Particle types constants
 GAMMA = "Gamma"
@@ -227,3 +235,52 @@ def adjointCalculationRequested(cs):
 def realCalculationRequested(cs):
     """Return true if a real calculation is requested based on the ``neutronicsType`` type setting."""
     return cs["neutronicsType"] in ["real", "both"]
+
+
+def applyEffectiveDelayedNeutronFractionToCore(core, cs):
+    """Process the settings for the delayed neutron fraction and precursor decay constants."""
+    # Verify and set the core beta parameters based on the user-supplied settings
+    beta = cs["beta"]
+    decayConstants = cs["decayConstants"]
+
+    # If beta is interpreted as a float, then assign it to
+    # the total delayed neutron fraction parameter. Otherwise, setup the
+    # group-wise delayed neutron fractions and precursor decay constants.
+    reportTableData = []
+    if isinstance(beta, float):
+        core.p.beta = beta
+        reportTableData.append(("Total Delayed Neutron Fraction", core.p.beta))
+
+    elif isinstance(beta, list) and isinstance(decayConstants, list):
+        if len(beta) != len(decayConstants):
+            raise ValueError(
+                f"The values for `beta` ({beta}) and `decayConstants` "
+                f"({decayConstants}) are not consistent lengths."
+            )
+
+        core.p.beta = sum(beta)
+        core.p.betaComponents = numpy.array(beta)
+        core.p.betaDecayConstants = numpy.array(decayConstants)
+
+        reportTableData.append(("Total Delayed Neutron Fraction", core.p.beta))
+        reportTableData.append(
+            ("Group-wise Delayed Neutron Fractions", core.p.betaComponents)
+        )
+        reportTableData.append(
+            ("Group-wise Precursor Decay Constants", core.p.betaDecayConstants)
+        )
+
+    # Report to the user the values were not applied.
+    if not reportTableData and (beta is not None or decayConstants is not None):
+        runLog.warning(
+            f"Delayed neutron fraction(s) - {beta} and decay constants"
+            " - {decayConstants} have not been applied."
+        )
+    else:
+        runLog.extra(
+            tabulate.tabulate(
+                tabular_data=reportTableData,
+                headers=["Component", "Value"],
+                tablefmt="armi",
+            )
+        )
diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py
index b78b75fc..cb41d21e 100644
--- a/armi/reactor/reactors.py
+++ b/armi/reactor/reactors.py
@@ -220,58 +220,6 @@ class Core(composites.Composite):
         self._circularRingPitch = cs["circularRingPitch"]
         self._automaticVariableMesh = cs["automaticVariableMesh"]
         self._minMeshSizeRatio = cs["minMeshSizeRatio"]
-        self._setupEffectiveDelayedNeutronFraction(cs)
-
-    def _setupEffectiveDelayedNeutronFraction(self, cs):
-        """Process the settings for the delayed neutron fraction and precursor decay constants."""
-        # Verify and set the core beta parameters based on the user-supplied settings
-        runLog.info(
-            "Checking for user-supplied `beta` and `decayConstants` settings and attempting to "
-            f"apply them to state of {self}."
-        )
-        beta = cs["beta"]
-        decayConstants = cs["decayConstants"]
-
-        # If beta is interpreted as a float, then assign it to
-        # the total delayed neutron fraction parameter. Otherwise, setup the
-        # group-wise delayed neutron fractions and precursor decay constants.
-        reportTableData = []
-        if isinstance(beta, float):
-            self.p.beta = beta
-            reportTableData.append(("Total Delayed Neutron Fraction", self.p.beta))
-
-        elif isinstance(beta, list) and isinstance(decayConstants, list):
-            if len(beta) != len(decayConstants):
-                raise ValueError(
-                    f"The values for `beta` ({beta}) and `decayConstants` "
-                    f"({decayConstants}) are not consistent lengths."
-                )
-
-            self.p.beta = sum(beta)
-            self.p.betaComponents = numpy.array(beta)
-            self.p.betaDecayConstants = numpy.array(decayConstants)
-
-            reportTableData.append(("Total Delayed Neutron Fraction", self.p.beta))
-            reportTableData.append(
-                ("Group-wise Delayed Neutron Fractions", self.p.betaComponents)
-            )
-            reportTableData.append(
-                ("Group-wise Precursor Decay Constants", self.p.betaDecayConstants)
-            )
-
-        # Report to the user the values were not applied.
-        if not reportTableData and (beta is not None or decayConstants is not None):
-            runLog.warning(
-                f"Delayed neutron fraction(s) - {beta} and decay constants - {decayConstants} have not been applied."
-            )
-        else:
-            runLog.info(
-                tabulate.tabulate(
-                    tabular_data=reportTableData,
-                    headers=["Component", "Value"],
-                    tablefmt="armi",
-                )
-            )
 
     def __getstate__(self):
         """Applies a settings and parent to the core and components. """


Test patch:
diff --git a/armi/physics/neutronics/tests/test_neutronicsPlugin.py b/armi/physics/neutronics/tests/test_neutronicsPlugin.py
index 1a4085a1..bb1cf5a3 100644
--- a/armi/physics/neutronics/tests/test_neutronicsPlugin.py
+++ b/armi/physics/neutronics/tests/test_neutronicsPlugin.py
@@ -17,11 +17,17 @@ import io
 import os
 
 from ruamel.yaml import YAML
+import voluptuous as vol
 
 from armi.tests.test_plugins import TestPlugin
 from armi.physics import neutronics
 from armi.settings import caseSettings
 from armi.physics.neutronics.const import CONF_CROSS_SECTION
+from armi.utils import directoryChangers
+from armi import tests
+from armi.tests import TEST_ROOT
+from armi import settings
+import armi
 
 XS_EXAMPLE = """AA:
     geometry: 0D
@@ -60,5 +66,146 @@ class Test_NeutronicsPlugin(TestPlugin):
         os.remove(fname)
 
 
+class NeutronicsReactorTests(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        # prepare the input files. This is important so the unit tests run from wherever
+        # they need to run from.
+        cls.directoryChanger = directoryChangers.DirectoryChanger(TEST_ROOT)
+        cls.directoryChanger.open()
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.directoryChanger.close()
+
+    def test_kineticsParameterAssignment(self):
+        """Test that the delayed neutron fraction and precursor decay constants are applied from settings."""
+
+        def _getModifiedSettings(customSettings):
+            cs = settings.Settings()
+            for key, val in customSettings.items():
+                cs[key] = val
+            return cs
+
+        r = tests.getEmptyHexReactor()
+        self.assertIsNone(r.core.p.beta)
+        self.assertIsNone(r.core.p.betaComponents)
+        self.assertIsNone(r.core.p.betaDecayConstants)
+
+        # Test that the group-wise beta and decay constants are assigned
+        # together given that they are the same length.
+        r = tests.getEmptyHexReactor()
+        cs = _getModifiedSettings(
+            customSettings={"beta": [0.0] * 6, "decayConstants": [0.0] * 6,}
+        )
+        armi.getPluginManagerOrFail().hook.onProcessCoreLoading(core=r.core, cs=cs)
+        r.core.setOptionsFromCs(cs)
+        self.assertEqual(r.core.p.beta, sum(cs["beta"]))
+        self.assertListEqual(list(r.core.p.betaComponents), cs["beta"])
+        self.assertListEqual(list(r.core.p.betaDecayConstants), cs["decayConstants"])
+
+        # Test the assignment of total beta as a float
+        r = tests.getEmptyHexReactor()
+        cs = _getModifiedSettings(customSettings={"beta": 0.00670},)
+        armi.getPluginManagerOrFail().hook.onProcessCoreLoading(core=r.core, cs=cs)
+        self.assertEqual(r.core.p.beta, cs["beta"])
+        self.assertIsNone(r.core.p.betaComponents)
+        self.assertIsNone(r.core.p.betaDecayConstants)
+
+        # Test that nothing is assigned if the beta is specified as a list
+        # without a corresponding decay constants list.
+        r = tests.getEmptyHexReactor()
+        cs = _getModifiedSettings(customSettings={"beta": [0.0] * 6,},)
+        armi.getPluginManagerOrFail().hook.onProcessCoreLoading(core=r.core, cs=cs)
+        self.assertIsNone(r.core.p.beta)
+        self.assertIsNone(r.core.p.betaComponents)
+        self.assertIsNone(r.core.p.betaDecayConstants)
+
+        # Test that 1 group beta components and decay constants can be assigned.
+        # Since beta is a list, ensure that it's assigned to the `betaComponents`
+        # parameter.
+        r = tests.getEmptyHexReactor()
+        cs = _getModifiedSettings(
+            customSettings={"beta": [0.0], "decayConstants": [0.0]},
+        )
+        armi.getPluginManagerOrFail().hook.onProcessCoreLoading(core=r.core, cs=cs)
+        self.assertEqual(r.core.p.beta, sum(cs["beta"]))
+        self.assertListEqual(list(r.core.p.betaComponents), cs["beta"])
+        self.assertListEqual(list(r.core.p.betaDecayConstants), cs["decayConstants"])
+
+        # Test that decay constants are not assigned without a corresponding
+        # group-wise beta input.
+        r = tests.getEmptyHexReactor()
+        cs = _getModifiedSettings(customSettings={"decayConstants": [0.0] * 6},)
+        armi.getPluginManagerOrFail().hook.onProcessCoreLoading(core=r.core, cs=cs)
+        self.assertIsNone(r.core.p.beta)
+        self.assertIsNone(r.core.p.betaComponents)
+        self.assertIsNone(r.core.p.betaDecayConstants)
+
+        # Test that decay constants are not assigned without a corresponding
+        # group-wise beta input. This also demonstrates that the total beta
+        # is still assigned.
+        r = tests.getEmptyHexReactor()
+        cs = _getModifiedSettings(
+            customSettings={"decayConstants": [0.0] * 6, "beta": 0.0},
+        )
+        armi.getPluginManagerOrFail().hook.onProcessCoreLoading(core=r.core, cs=cs)
+        self.assertEqual(r.core.p.beta, cs["beta"])
+        self.assertIsNone(r.core.p.betaComponents)
+        self.assertIsNone(r.core.p.betaDecayConstants)
+
+        # Test the demonstrates that None values are acceptable
+        # and that nothing is assigned.
+        r = tests.getEmptyHexReactor()
+        cs = _getModifiedSettings(
+            customSettings={"decayConstants": None, "beta": None},
+        )
+        armi.getPluginManagerOrFail().hook.onProcessCoreLoading(core=r.core, cs=cs)
+        self.assertEqual(r.core.p.beta, cs["beta"])
+        self.assertIsNone(r.core.p.betaComponents)
+        self.assertIsNone(r.core.p.betaDecayConstants)
+
+        # Test that an error is raised if the decay constants
+        # and group-wise beta are inconsistent sizes
+        with self.assertRaises(ValueError):
+            r = tests.getEmptyHexReactor()
+            cs = _getModifiedSettings(
+                customSettings={"decayConstants": [0.0] * 6, "beta": [0.0]},
+            )
+            armi.getPluginManagerOrFail().hook.onProcessCoreLoading(core=r.core, cs=cs)
+
+        # Test that an error is raised if the decay constants
+        # and group-wise beta are inconsistent sizes
+        with self.assertRaises(ValueError):
+            r = tests.getEmptyHexReactor()
+            cs = _getModifiedSettings(
+                customSettings={"decayConstants": [0.0] * 6, "beta": [0.0] * 5},
+            )
+            armi.getPluginManagerOrFail().hook.onProcessCoreLoading(core=r.core, cs=cs)
+
+        # The following tests check the voluptuous schema definition. This
+        # ensures that anything except NoneType, [float], float are not valid
+        # inputs.
+        with self.assertRaises(vol.AnyInvalid):
+            r = tests.getEmptyHexReactor()
+            cs = _getModifiedSettings(customSettings={"decayConstants": [1]},)
+            armi.getPluginManagerOrFail().hook.onProcessCoreLoading(core=r.core, cs=cs)
+
+        with self.assertRaises(vol.AnyInvalid):
+            r = tests.getEmptyHexReactor()
+            cs = _getModifiedSettings(customSettings={"beta": [1]},)
+            armi.getPluginManagerOrFail().hook.onProcessCoreLoading(core=r.core, cs=cs)
+
+        with self.assertRaises(vol.AnyInvalid):
+            r = tests.getEmptyHexReactor()
+            cs = _getModifiedSettings(customSettings={"beta": 1},)
+            armi.getPluginManagerOrFail().hook.onProcessCoreLoading(core=r.core, cs=cs)
+
+        with self.assertRaises(vol.AnyInvalid):
+            r = tests.getEmptyHexReactor()
+            cs = _getModifiedSettings(customSettings={"beta": (1, 2, 3)},)
+            armi.getPluginManagerOrFail().hook.onProcessCoreLoading(core=r.core, cs=cs)
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py
index 3ebcf06a..ff4f44f6 100644
--- a/armi/reactor/tests/test_reactors.py
+++ b/armi/reactor/tests/test_reactors.py
@@ -18,7 +18,6 @@ import copy
 import os
 import unittest
 
-import voluptuous as vol
 from six.moves import cPickle
 from numpy.testing import assert_allclose, assert_equal
 import armi
@@ -201,133 +200,6 @@ class ReactorTests(unittest.TestCase):
     def tearDownClass(cls):
         cls.directoryChanger.close()
 
-    def test_kineticsParameterAssignment(self):
-        """Test that the delayed neutron fraction and precursor decay constants are applied from settings."""
-
-        def _getModifiedSettings(customSettings):
-            cs = settings.Settings()
-            for key, val in customSettings.items():
-                cs[key] = val
-            return cs
-
-        r = tests.getEmptyHexReactor()
-        self.assertIsNone(r.core.p.beta)
-        self.assertIsNone(r.core.p.betaComponents)
-        self.assertIsNone(r.core.p.betaDecayConstants)
-
-        # Test that the group-wise beta and decay constants are assigned
-        # together given that they are the same length.
-        r = tests.getEmptyHexReactor()
-        cs = _getModifiedSettings(
-            customSettings={"beta": [0.0] * 6, "decayConstants": [0.0] * 6,}
-        )
-        r.core.setOptionsFromCs(cs)
-        self.assertEqual(r.core.p.beta, sum(cs["beta"]))
-        self.assertListEqual(list(r.core.p.betaComponents), cs["beta"])
-        self.assertListEqual(list(r.core.p.betaDecayConstants), cs["decayConstants"])
-
-        # Test the assignment of total beta as a float
-        r = tests.getEmptyHexReactor()
-        cs = _getModifiedSettings(customSettings={"beta": 0.00670},)
-        r.core.setOptionsFromCs(cs)
-        self.assertEqual(r.core.p.beta, cs["beta"])
-        self.assertIsNone(r.core.p.betaComponents)
-        self.assertIsNone(r.core.p.betaDecayConstants)
-
-        # Test that nothing is assigned if the beta is specified as a list
-        # without a corresponding decay constants list.
-        r = tests.getEmptyHexReactor()
-        cs = _getModifiedSettings(customSettings={"beta": [0.0] * 6,},)
-        r.core.setOptionsFromCs(cs)
-        self.assertIsNone(r.core.p.beta)
-        self.assertIsNone(r.core.p.betaComponents)
-        self.assertIsNone(r.core.p.betaDecayConstants)
-
-        # Test that 1 group beta components and decay constants can be assigned.
-        # Since beta is a list, ensure that it's assigned to the `betaComponents`
-        # parameter.
-        r = tests.getEmptyHexReactor()
-        cs = _getModifiedSettings(
-            customSettings={"beta": [0.0], "decayConstants": [0.0]},
-        )
-        r.core.setOptionsFromCs(cs)
-        self.assertEqual(r.core.p.beta, sum(cs["beta"]))
-        self.assertListEqual(list(r.core.p.betaComponents), cs["beta"])
-        self.assertListEqual(list(r.core.p.betaDecayConstants), cs["decayConstants"])
-
-        # Test that decay constants are not assigned without a corresponding
-        # group-wise beta input.
-        r = tests.getEmptyHexReactor()
-        cs = _getModifiedSettings(customSettings={"decayConstants": [0.0] * 6},)
-        r.core.setOptionsFromCs(cs)
-        self.assertIsNone(r.core.p.beta)
-        self.assertIsNone(r.core.p.betaComponents)
-        self.assertIsNone(r.core.p.betaDecayConstants)
-
-        # Test that decay constants are not assigned without a corresponding
-        # group-wise beta input. This also demonstrates that the total beta
-        # is still assigned.
-        r = tests.getEmptyHexReactor()
-        cs = _getModifiedSettings(
-            customSettings={"decayConstants": [0.0] * 6, "beta": 0.0},
-        )
-        r.core.setOptionsFromCs(cs)
-        self.assertEqual(r.core.p.beta, cs["beta"])
-        self.assertIsNone(r.core.p.betaComponents)
-        self.assertIsNone(r.core.p.betaDecayConstants)
-
-        # Test the demonstrates that None values are acceptable
-        # and that nothing is assigned.
-        r = tests.getEmptyHexReactor()
-        cs = _getModifiedSettings(
-            customSettings={"decayConstants": None, "beta": None},
-        )
-        r.core.setOptionsFromCs(cs)
-        self.assertEqual(r.core.p.beta, cs["beta"])
-        self.assertIsNone(r.core.p.betaComponents)
-        self.assertIsNone(r.core.p.betaDecayConstants)
-
-        # Test that an error is raised if the decay constants
-        # and group-wise beta are inconsistent sizes
-        with self.assertRaises(ValueError):
-            r = tests.getEmptyHexReactor()
-            cs = _getModifiedSettings(
-                customSettings={"decayConstants": [0.0] * 6, "beta": [0.0]},
-            )
-            r.core.setOptionsFromCs(cs)
-
-        # Test that an error is raised if the decay constants
-        # and group-wise beta are inconsistent sizes
-        with self.assertRaises(ValueError):
-            r = tests.getEmptyHexReactor()
-            cs = _getModifiedSettings(
-                customSettings={"decayConstants": [0.0] * 6, "beta": [0.0] * 5},
-            )
-            r.core.setOptionsFromCs(cs)
-
-        # The following tests check the voluptuous schema definition. This
-        # ensures that anything except NoneType, [float], float are not valid
-        # inputs.
-        with self.assertRaises(vol.AnyInvalid):
-            r = tests.getEmptyHexReactor()
-            cs = _getModifiedSettings(customSettings={"decayConstants": [1]},)
-            r.core.setOptionsFromCs(cs)
-
-        with self.assertRaises(vol.AnyInvalid):
-            r = tests.getEmptyHexReactor()
-            cs = _getModifiedSettings(customSettings={"beta": [1]},)
-            r.core.setOptionsFromCs(cs)
-
-        with self.assertRaises(vol.AnyInvalid):
-            r = tests.getEmptyHexReactor()
-            cs = _getModifiedSettings(customSettings={"beta": 1},)
-            r.core.setOptionsFromCs(cs)
-
-        with self.assertRaises(vol.AnyInvalid):
-            r = tests.getEmptyHexReactor()
-            cs = _getModifiedSettings(customSettings={"beta": (1, 2, 3)},)
-            r.core.setOptionsFromCs(cs)
-
 
 class HexReactorTests(ReactorTests):
     def setUp(self):