Repository: terrapower/armi
Pull request #1903 (2024-09-25 14:56:40)

Add some missing nuclides from nuclides.dat
## What is the change?
A simple change to add some nuclides to nuclides.dat to support downstream analysis. 

## Why is the change being made?
It's good to account for all the nuclides that one _might_ care about. 

---

## Checklist

- [x] This PR has only [one purpose or idea](https://terrapower.github.io/armi/developer/tooling.html#one-idea-one-pr).
- [Tests](https://terrapower.github.io/armi/developer/tooling.html#test-it) have been added/updated to verify 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`.

albeanth: @john-science just a heads up. This should be pretty simple to push through. I can handle internal QA-related things. I've pinged @jakehader and Connor for an SME review. 
john-science: @albeanth Can you run the big internal suite of unit tests against this PR? Obviously, functionally, this PR shouldn't make a big difference. But I imagine some "diffs" popping up in your internal integration tests, and who knows maybe someone is checking an exact number in their unit tests somewhere.
john-science: I can verify but not validate this change.

Holler when you guys want to merge this.
jakehader: > I can verify but not validate this change.
> 
> Holler when you guys want to merge this.

I checked them @john-science.
john-science: @jakehader @albeanth This appears complete to me.  Status?

Patch:
diff --git a/armi/nuclearDataIO/xsCollections.py b/armi/nuclearDataIO/xsCollections.py
index faf7978b..91914fcf 100644
--- a/armi/nuclearDataIO/xsCollections.py
+++ b/armi/nuclearDataIO/xsCollections.py
@@ -50,7 +50,7 @@ from armi.utils import units
 # they have some origin in the ISOTXS file format card 04 definition
 # fmt: off
 NGAMMA = "nGamma"      # radiative capture
-NAPLHA = "nalph"       # (n, alpha)
+NALPHA = "nalph"       # (n, alpha)
 NP = "np"              # (n, proton)
 ND = "nd"              # (n, deuteron)
 NT = "nt"              # (n, triton)
@@ -59,7 +59,7 @@ N2N_XS = "n2n"         # (n,2n)
 NUSIGF = "nuSigF"
 NU = "neutronsPerFission"
 # fmt: on
-CAPTURE_XS = [NGAMMA, NAPLHA, NP, ND, NT]
+CAPTURE_XS = [NGAMMA, NALPHA, NP, ND, NT]
 
 # Cross section types that are represented by 2-D matrices in the multigroup approximation
 BASIC_SCAT_MATRIX = ["elasticScatter", "inelasticScatter", "n2nScatter"]
diff --git a/armi/physics/neutronics/globalFlux/__init__.py b/armi/physics/neutronics/globalFlux/__init__.py
index 4d4496f5..98c44bab 100644
--- a/armi/physics/neutronics/globalFlux/__init__.py
+++ b/armi/physics/neutronics/globalFlux/__init__.py
@@ -13,3 +13,6 @@
 # limitations under the License.
 
 """Global flux solvers."""
+
+RX_ABS_MICRO_LABELS = ["nGamma", "fission", "nalph", "np", "nd", "nt"]
+RX_PARAM_NAMES = ["rateCap", "rateFis", "rateProdN2n", "rateProdFis", "rateAbs"]
diff --git a/armi/physics/neutronics/globalFlux/globalFluxInterface.py b/armi/physics/neutronics/globalFlux/globalFluxInterface.py
index ca97f299..9e51adab 100644
--- a/armi/physics/neutronics/globalFlux/globalFluxInterface.py
+++ b/armi/physics/neutronics/globalFlux/globalFluxInterface.py
@@ -27,6 +27,7 @@ from armi.physics import executers
 from armi.physics import neutronics
 from armi.reactor import geometry
 from armi.reactor import reactors
+from armi.physics.neutronics.globalFlux import RX_PARAM_NAMES, RX_ABS_MICRO_LABELS
 from armi.reactor.blocks import Block
 from armi.reactor.converters import geometryConverters
 from armi.reactor.converters import uniformMesh
@@ -36,9 +37,6 @@ from armi.utils import units, codeTiming, getMaxBurnSteps, getBurnSteps
 
 ORDER = interfaces.STACK_ORDER.FLUX
 
-RX_ABS_MICRO_LABELS = ["nGamma", "fission", "nalph", "np", "nd", "nt"]
-RX_PARAM_NAMES = ["rateCap", "rateFis", "rateProdN2n", "rateProdFis", "rateAbs"]
-
 
 class GlobalFluxInterface(interfaces.Interface):
     """
diff --git a/armi/reactor/converters/uniformMesh.py b/armi/reactor/converters/uniformMesh.py
index 47d67092..47f4917d 100644
--- a/armi/reactor/converters/uniformMesh.py
+++ b/armi/reactor/converters/uniformMesh.py
@@ -72,6 +72,8 @@ from armi.reactor.converters.geometryConverters import GeometryConverter
 from armi.reactor import parameters
 from armi.reactor.reactors import Reactor
 from armi.settings.fwSettings.globalSettings import CONF_UNIFORM_MESH_MINIMUM_SIZE
+from armi.physics.neutronics.globalFlux import RX_PARAM_NAMES, RX_ABS_MICRO_LABELS
+
 
 HEAVY_METAL_PARAMS = ["molesHmBOL", "massHmBOL"]
 
@@ -1100,13 +1102,42 @@ class UniformMeshGeometryConverter(GeometryConverter):
                     aDest,
                     self.paramMapper,
                     mapNumberDensities,
-                    calcReactionRates=self.calcReactionRates,
+                    calcReactionRates=False,
+                )
+
+            # If requested, the reaction rates will be calculated based on the
+            # mapped neutron flux and the XS library.
+            if self.calcReactionRates:
+                self._calculateReactionRatesEfficient(
+                    destReactor.core, sourceReactor.core.p.keff
                 )
 
         # Clear the cached data after it has been mapped to prevent issues with
         # holding on to block data long-term.
         self._cachedReactorCoreParamData = {}
 
+    @staticmethod
+    def _calculateReactionRatesEfficient(core, keff):
+        """
+        First, sort blocks into groups by XS type. Then, we just need to grab micros for each XS type once.
+
+        Iterate over list of blocks with the given XS type; calculate reaction rates for these blocks
+        """
+        xsTypeGroups = collections.defaultdict(list)
+        for b in core.getBlocks():
+            xsTypeGroups[b.getMicroSuffix()].append(b)
+
+        for xsID, blockList in xsTypeGroups.items():
+            nucSet = set()
+            for b in blockList:
+                nucSet.update(
+                    nuc for nuc, ndens in b.getNumberDensities().items() if ndens > 0.0
+                )
+            xsNucDict = {nuc: core.lib.getNuclide(nuc, xsID) for nuc in nucSet}
+            UniformMeshGeometryConverter._calcReactionRatesBlockList(
+                blockList, keff, xsNucDict
+            )
+
     @staticmethod
     def _calculateReactionRates(lib, keff, assem):
         """
@@ -1130,6 +1161,87 @@ class UniformMeshGeometryConverter(GeometryConverter):
                 continue
             globalFluxInterface.calcReactionRates(b, keff, lib)
 
+    @staticmethod
+    def _calcReactionRatesBlockList(objList, keff, xsNucDict):
+        r"""
+        Compute 1-group reaction rates for the objects in objList (usually a block).
+
+        :meta public:
+
+        .. impl:: Return the reaction rates for a given ArmiObject
+            :id: I_ARMI_FLUX_RX_RATES_BY_XS_ID
+            :implements: R_ARMI_FLUX_RX_RATES
+
+            This is an alternative implementation of :need:`I_ARMI_FLUX_RX_RATES` that
+            is more efficient when computing reaction rates for a large set of blocks
+            that share a common set of microscopic cross sections.
+
+            For more detail on the reation rate calculations, see :need:`I_ARMI_FLUX_RX_RATES`.
+
+        Parameters
+        ----------
+        objList : List[Block]
+            The list of objects to compute reaction rates on. Notionally this could be upgraded to be
+            any kind of ArmiObject but with params defined as they are it currently is only
+            implemented for a block.
+
+        keff : float
+            The keff of the core. This is required to get the neutron production rate correct
+            via the neutron balance statement (since nuSigF has a 1/keff term).
+
+        xsNucDict: Dict[str, XSNuclide]
+            Microscopic cross sections to use in computing the reaction rates. Keys are
+            nuclide names (e.g., "U235") and values are the associated XSNuclide objects
+            from the cross section library, which contain the microscopic cross section
+            data for a given nuclide in the current cross section group.
+        """
+        for obj in objList:
+            rate = collections.defaultdict(float)
+
+            numberDensities = obj.getNumberDensities()
+            try:
+                mgFlux = np.array(obj.getMgFlux())
+            except TypeError:
+                continue
+
+            for nucName, numberDensity in numberDensities.items():
+                if numberDensity == 0.0:
+                    continue
+                nucRate = collections.defaultdict(float)
+
+                micros = xsNucDict[nucName].micros
+
+                # absorption is fission + capture (no n2n here)
+                for name in RX_ABS_MICRO_LABELS:
+                    volumetricRR = numberDensity * mgFlux.dot(micros[name])
+                    nucRate["rateAbs"] += volumetricRR
+                    if name != "fission":
+                        nucRate["rateCap"] += volumetricRR
+                    else:
+                        nucRate["rateFis"] += volumetricRR
+                        # scale nu by keff.
+                        nusigmaF = micros["fission"] * micros.neutronsPerFission
+                        nucRate["rateProdFis"] += (
+                            numberDensity * mgFlux.dot(nusigmaF) / keff
+                        )
+
+                nucRate["rateProdN2n"] += 2.0 * numberDensity * mgFlux.dot(micros.n2n)
+
+                for rx in RX_PARAM_NAMES:
+                    if nucRate[rx]:
+                        rate[rx] += nucRate[rx]
+
+            for paramName, val in rate.items():
+                obj.p[paramName] = val  # put in #/cm^3/s
+
+            if rate["rateFis"] > 0.0:
+                fuelVolFrac = obj.getComponentAreaFrac(Flags.FUEL)
+                obj.p.fisDens = rate["rateFis"] / fuelVolFrac
+                obj.p.fisDensHom = rate["rateFis"]
+            else:
+                obj.p.fisDens = 0.0
+                obj.p.fisDensHom = 0.0
+
     def updateReactionRates(self):
         """
         Update reaction rates on converted assemblies.
@@ -1147,10 +1259,9 @@ class UniformMeshGeometryConverter(GeometryConverter):
                     self.convReactor.core.lib, self.convReactor.core.p.keff, assem
                 )
         else:
-            for assem in self.convReactor.core.getAssemblies():
-                self._calculateReactionRates(
-                    self.convReactor.core.lib, self.convReactor.core.p.keff, assem
-                )
+            self._calculateReactionRatesEfficient(
+                self.convReactor.core, self.convReactor.core.p.keff
+            )
 
 
 class NeutronicsUniformMeshConverter(UniformMeshGeometryConverter):
diff --git a/armi/resources/nuclides.dat b/armi/resources/nuclides.dat
index c34d76cb..f05d1907 100644
--- a/armi/resources/nuclides.dat
+++ b/armi/resources/nuclides.dat
@@ -1,7 +1,7 @@
 # ---------------------------------------------------------------------
 # NUCLIDE DATA
 # ---------------------------------------------------------------------
-# This file contains a set of 4614 nuclides and their associated metadata
+# This file contains a set of 4624 nuclides and their associated metadata
 # of atomic number (Z), number of neutrons (N), mass number (A), stability
 # state number (S), Mass (amu), natural abundance (fractional), half-life (sec), 
 # and the spontaneous fission neutron yield (nu-bar).
@@ -2911,6 +2911,7 @@ Z	N	A	S	El	  Mass, amu	        Abundance, frac.  Half-life (sec)   Spontaneous F
 70  115 185 0   YB    1.84967901031e+02 0.00000000000e+00 1.60000000000e-07 0.000000
 70  116 186 0   YB    1.85960897004e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 70  117 187 0   YB    1.86964769503e+02 0.00000000000e+00 1.11110000000e+01 0.000000
+70  118 188 0   YB    1.87966850000e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 71  79  150 0   LU    1.49973227957e+02 0.00000000000e+00 4.50000000000e-02 0.000000
 71  79  150 1   LU    1.49973251575e+02 0.00000000000e+00 3.90000000000e-05 0.000000
 71  80  151 0   LU    1.50967576821e+02 0.00000000000e+00 8.06000000000e-02 0.000000
@@ -2979,6 +2980,7 @@ Z	N	A	S	El	  Mass, amu	        Abundance, frac.  Half-life (sec)   Spontaneous F
 71  117 188 0   LU    1.87973172133e+02 0.00000000000e+00 inf               0.000000
 71  118 189 0   LU    1.88970840395e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 71  119 190 0   LU    1.89969390037e+02 0.00000000000e+00 1.11110000000e+01 0.000000
+71  120 191 0   LU    1.90971560000e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 72  81  153 0   HF    1.52970690099e+02 0.00000000000e+00 inf               0.000000
 72  82  154 0   HF    1.53964859681e+02 0.00000000000e+00 2.00000000000e+00 0.000000
 72  83  155 0   HF    1.54963389999e+02 0.00000000000e+00 8.43000000000e-01 0.000000
@@ -3087,6 +3089,7 @@ Z	N	A	S	El	  Mass, amu	        Abundance, frac.  Half-life (sec)   Spontaneous F
 73  119 192 0   TA    1.91975147454e+02 0.00000000000e+00 2.20000000000e+00 0.000000
 73  120 193 0   TA    1.92977401897e+02 0.00000000000e+00 inf               0.000000
 73  121 194 0   TA    1.93981277391e+02 0.00000000000e+00 inf               0.000000
+73  122 195 0   TA    1.94972540000e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 74  83  157 0   W     1.56978357351e+02 0.00000000000e+00 2.75000000000e-01 0.000000
 74  84  158 0   W     1.57974562372e+02 0.00000000000e+00 1.25000000000e-03 0.000000
 74  84  158 1   W     1.57976589224e+02 0.00000000000e+00 1.43000000000e-04 0.000000
@@ -4246,6 +4249,7 @@ Z	N	A	S	El	  Mass, amu	        Abundance, frac.  Half-life (sec)   Spontaneous F
 95  156 251 0   AM    2.51082286436e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 95  157 252 0   AM    2.52084870103e+02 0.00000000000e+00 inf               0.000000
 95  158 253 0   AM    2.53086880851e+02 0.00000000000e+00 1.11110000000e+01 0.000000
+96  134 230 0   CM    2.30046089000e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 96  135 231 0   CM    2.31050381425e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 96  136 232 0   CM    2.32049576267e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 96  137 233 0   CM    2.33050771121e+02 0.00000000000e+00 2.30000000000e+01 0.000000
@@ -4272,6 +4276,8 @@ Z	N	A	S	El	  Mass, amu	        Abundance, frac.  Half-life (sec)   Spontaneous F
 96  157 253 0   CM    2.53086880851e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 96  158 254 0   CM    2.54090599607e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 96  159 255 0   CM    2.55091048349e+02 0.00000000000e+00 1.11110000000e+01 0.000000
+97  134 231 0   BK    2.31045560000e+02 0.00000000000e+00 1.11110000000e+01 0.000000
+97  135 232 0   BK    2.32049817000e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 97  136 233 0   BK    2.33056210769e+02 0.00000000000e+00 2.10000000000e+01 0.000000
 97  137 234 0   BK    2.34056940779e+02 0.00000000000e+00 1.40000000000e+02 0.000000
 97  138 235 0   BK    2.35056580068e+02 0.00000000000e+00 1.11110000000e+01 0.000000
@@ -4377,6 +4383,7 @@ Z	N	A	S	El	  Mass, amu	        Abundance, frac.  Half-life (sec)   Spontaneous F
 100 161 261 0   FM    2.61092087368e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 100 162 262 0   FM    2.62095806125e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 100 163 263 0   FM    2.63096254866e+02 0.00000000000e+00 1.11110000000e+01 0.000000
+101 142 243 0   MD    2.43070833000e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 101 143 244 0   MD    2.44080977432e+02 0.00000000000e+00 3.80000000000e-01 0.000000
 101 144 245 0   MD    2.45080829283e+02 0.00000000000e+00 9.00000000000e-04 0.000000
 101 145 246 0   MD    2.46081885650e+02 0.00000000000e+00 9.00000000000e-01 0.000000
@@ -4429,6 +4436,7 @@ Z	N	A	S	El	  Mass, amu	        Abundance, frac.  Half-life (sec)   Spontaneous F
 102 163 265 0   NO    2.65094690616e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 102 164 266 0   NO    2.66098409372e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 102 165 267 0   NO    2.67098858114e+02 0.00000000000e+00 1.11110000000e+01 0.000000
+103 145 248 0   LR    2.48074127000e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 103 146 249 0   LR    2.49083411070e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 103 147 250 0   LR    2.50084316068e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 103 148 251 0   LR    2.51094360232e+02 0.00000000000e+00 1.11110000000e+01 0.000000
@@ -4477,6 +4485,8 @@ Z	N	A	S	El	  Mass, amu	        Abundance, frac.  Half-life (sec)   Spontaneous F
 104 165 269 0   RF    2.69097293874e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 104 166 270 0   RF    2.70101012631e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 104 167 271 0   RF    2.71101461372e+02 0.00000000000e+00 1.11110000000e+01 0.000000
+105 147 252 0   DB    2.52076730000e+02 0.00000000000e+00 1.11110000000e+01 0.000000
+105 148 253 0   DB    2.53086014000e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 105 149 254 0   DB    2.54086919316e+02 0.00000000000e+00 1.11110000000e+01 0.000000
 105 150 255 0   DB    2.55107398426e+02 0.00000000000e+00 1.60000000000e+00 0.000000
 105 151 256 0   DB    2.56108127362e+02 0.00000000000e+00 1.60000000000e+00 0.000000
diff --git a/doc/release/0.4.rst b/doc/release/0.4.rst
index 80ef1f4a..c2f54a32 100644
--- a/doc/release/0.4.rst
+++ b/doc/release/0.4.rst
@@ -19,11 +19,11 @@ New Features
 #. Provide ``Parameter.hasCategory`` for quickly checking if a parameter is defined with a given category. (`PR#1899 <https://github.com/terrapower/armi/pull/1899>`_)
 #. Provide ``ParameterCollection.where`` for efficient iteration over parameters who's definition matches a given condition. (`PR#1899 <https://github.com/terrapower/armi/pull/1899>`_)
 #. Flags can now be defined with letters and numbers. (`PR#1966 <https://github.com/terrapower/armi/pull/1966>`_)
-#. Plugins can provide the ``getAxialExpansionChanger`` hook to customize axial expansion. (`PR#1870 <https://github.com/terrapower/armi/pull/1870`_)
 #. Plugins can provide the ``getAxialExpansionChanger`` hook to customize axial expansion. (`PR#1870 <https://github.com/terrapower/armi/pull/1870>`_)
 #. New plugin hook ``beforeReactorConstruction`` added to enable plugins to process case settings before reactor init. (`PR#1945 <https://github.com/terrapower/armi/pull/1945>`_)
 #. Provide ``Block.getInputHeight`` for determining the height of a block from blueprints. (`PR#1927 <https://github.com/terrapower/armi/pull/1927`_)
 #. Improve performance by changing the lattice physics interface so that cross sections are not updated on ``everyNode`` calls during coupled calculations (`PR#1963 <https://github.com/terrapower/armi/pull/1963>`_)
+#. Improve efficiency of reaction rate calculations. (`PR#1887 <https://github.com/terrapower/armi/pull/1887>`_)
 #. TBD
 
 API Changes


Test patch:
diff --git a/armi/reactor/converters/tests/test_uniformMesh.py b/armi/reactor/converters/tests/test_uniformMesh.py
index 11f78153..2edaf6a4 100644
--- a/armi/reactor/converters/tests/test_uniformMesh.py
+++ b/armi/reactor/converters/tests/test_uniformMesh.py
@@ -16,6 +16,7 @@ import collections
 import os
 import random
 import unittest
+import copy
 
 from armi.nuclearDataIO.cccc import isotxs
 from armi.physics.neutronics.settings import CONF_XS_KERNEL
@@ -23,6 +24,7 @@ from armi.settings.fwSettings.globalSettings import CONF_UNIFORM_MESH_MINIMUM_SI
 from armi.reactor.converters import uniformMesh
 from armi.reactor.flags import Flags
 from armi.reactor.tests import test_assemblies
+from armi.reactor.tests import test_blocks
 from armi.reactor.tests.test_reactors import loadTestReactor, reduceTestReactorRings
 from armi.tests import TEST_ROOT, ISOAA_PATH
 
@@ -498,6 +500,31 @@ class TestUniformMesh(unittest.TestCase):
                 self.assertTrue(b.p.rateCap)
 
 
+class TestCalcReationRates(unittest.TestCase):
+    def test_calcReactionRatesBlockList(self):
+        """
+        Test that the efficient reaction rate code executes and sets a param > 0.0.
+
+        .. test:: Return the reaction rates for a given list of ArmiObjects.
+            :id: T_ARMI_FLUX_RX_RATES_BY_XS_ID
+            :tests: R_ARMI_FLUX_RX_RATES
+        """
+        b = test_blocks.loadTestBlock()
+        test_blocks.applyDummyData(b)
+        self.assertAlmostEqual(b.p.rateAbs, 0.0)
+        blockList = [copy.deepcopy(b) for _i in range(3)]
+        xsID = b.getMicroSuffix()
+        xsNucDict = {nuc: b.core.lib.getNuclide(nuc, xsID) for nuc in b.getNuclides()}
+        uniformMesh.UniformMeshGeometryConverter._calcReactionRatesBlockList(
+            blockList, 1.01, xsNucDict
+        )
+        for b in blockList:
+            self.assertGreater(b.p.rateAbs, 0.0)
+            vfrac = b.getComponentAreaFrac(Flags.FUEL)
+            self.assertEqual(b.p.fisDens, b.p.rateFis / vfrac)
+            self.assertEqual(b.p.fisDensHom, b.p.rateFis)
+
+
 class TestGammaUniformMesh(unittest.TestCase):
     """
     Tests gamma uniform mesh converter.