Repository: terrapower/armi
Pull request #2050 (2025-01-10 22:16:11)

Update doc build to py313
## What is the change?

The docs are being built with python 3.9. This updates them to python 3.13 (updating dependency pins in the process)

## Why is the change being made?

Keepin' up with the times!

---

## Checklist

<!--
    You (the pull requester) should put an `x` in the boxes below you have completed.

    (If a checkbox doesn't apply to your PR, check it anyway.)
-->

- [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`.

ntouran: was just talking to @john-science  about the nbextensions. That's from back when we had an awesome dockerized jupyterhub instance of ARMI with all plugins. It added a few neat tools that some users liked. It's totally fine to remove it now since we don't know of anyone using them in jupyterhub at the moment. 
john-science: Okay, so I opened the docs your PR generates: https://github.com/terrapower/armi/actions/runs/13082534763?pr=2050

Almost everything is working, and looks great.

But the "Gallery" section is broken:

![image](https://github.com/user-attachments/assets/929f4f98-80dd-48fa-9b3a-7c0a29888b18)

There are no links to the generated pages.  I downloaded another PRs recently generated docs and they did not have this problem.

@ntouran Could this be the dependency we dropped?

Patch:
diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
index 5ee16308..4080a44e 100644
--- a/.github/workflows/docs.yaml
+++ b/.github/workflows/docs.yaml
@@ -21,7 +21,7 @@ jobs:
       - name: Setup Python
         uses: actions/setup-python@v2
         with:
-          python-version: 3.9
+          python-version: 3.13
       - name: Update package index
         run: sudo apt-get update
       - name: Install mpi libs
diff --git a/armi/nucDirectory/nuclideBases.py b/armi/nucDirectory/nuclideBases.py
index d9e1130f..2821e116 100644
--- a/armi/nucDirectory/nuclideBases.py
+++ b/armi/nucDirectory/nuclideBases.py
@@ -800,22 +800,6 @@ class NaturalNuclideBase(INuclide, IMcnpNuclide):
         """
         return "{0:d}000".format(self.z)
 
-    def getAAAZZZSId(self):
-        """Gets the AAAZZZS ID for a few elements.
-
-        Notes
-        -----
-        the natural nuclides 'C' and 'V' do not have isotopic nuclide data for MC2 so sometimes they tag along in the
-        list of active nuclides. This method is designed to fail in the same as if there was not getAAAZZZSId method
-        defined.
-        """
-        if self.element.symbol == "C":
-            return "120060"
-        elif self.element.symbol == "V":
-            return "510230"
-        else:
-            return None
-
     def getMcc2Id(self):
         """Return the MC2-2 nuclide identification label based on the ENDF/B-V.2 cross section library."""
         return self.mcc2id
@@ -1442,6 +1426,8 @@ def addGlobalNuclide(nuclide: NuclideBase):
                 f"{nuclide} with McnpId {nuclide.getMcnpId()} has already been added and cannot be duplicated."
             )
         byMcnpId[nuclide.getMcnpId()] = nuclide
+    if not isinstance(nuclide, (NaturalNuclideBase, LumpNuclideBase, DummyNuclideBase)):
+        # There are no AZS ID for elements / natural nuclides, or ficticious lump or dummy nuclides
         byAAAZZZSId[nuclide.getAAAZZZSId()] = nuclide
 
 
diff --git a/armi/physics/neutronics/crossSectionGroupManager.py b/armi/physics/neutronics/crossSectionGroupManager.py
index 1eca9a38..f049890b 100644
--- a/armi/physics/neutronics/crossSectionGroupManager.py
+++ b/armi/physics/neutronics/crossSectionGroupManager.py
@@ -63,7 +63,6 @@ from armi.physics.neutronics import LatticePhysicsFrequency
 from armi.physics.neutronics.const import CONF_CROSS_SECTION
 from armi.reactor import flags
 from armi.reactor.components import basicShapes
-from armi.reactor.converters.blockConverters import stripComponents
 from armi.reactor.flags import Flags
 from armi.utils import safeCopy
 from armi.utils.units import C_TO_K, TRACE_NUMBER_DENSITY
@@ -710,6 +709,8 @@ class CylindricalComponentsDuctHetAverageBlockCollection(
 
     def _getNucTempHelper(self):
         """All candidate blocks are used in the average."""
+        from armi.reactor.converters.blockConverters import stripComponents
+
         nvt = np.zeros(len(self.allNuclidesInProblem))
         nv = np.zeros(len(self.allNuclidesInProblem))
         for block in self.getCandidateBlocks():
diff --git a/armi/reactor/__init__.py b/armi/reactor/__init__.py
index 3536b698..85bddbb2 100644
--- a/armi/reactor/__init__.py
+++ b/armi/reactor/__init__.py
@@ -16,36 +16,6 @@ r"""
 The reactor package houses the data model used in ARMI to represent the reactor during its
 simulation. It contains definitions of the reactor, assemblies, blocks, components, etc.
 
-The key classes of the reactor package are shown below:
-
-.. _reactor-class-diagram:
-
-.. pyreverse:: armi.reactor -A -k --ignore=
-               assemblyParameters.py,
-               basicShapes.py,
-               blockParameters.py,
-               blueprints,
-               complexShapes.py,
-               componentParameters.py,
-               converters,
-               excoreStructure.py,
-               flags.py,
-               geometry.py,
-               grids.py,
-               parameters,
-               plugins.py,
-               reactorParameters.py,
-               shapes.py,
-               spentFuelPool.py,
-               tests,
-               volumetricShapes.py,
-               zones.py
-    :align: center
-    :alt: Reactor class diagram
-    :width: 90%
-
-    Class inheritance diagram for :py:mod:`armi.reactor`.
-
 See :doc:`/developer/index`.
 """
 
diff --git a/armi/reactor/blueprints/componentBlueprint.py b/armi/reactor/blueprints/componentBlueprint.py
index c871d8ca..838c5525 100644
--- a/armi/reactor/blueprints/componentBlueprint.py
+++ b/armi/reactor/blueprints/componentBlueprint.py
@@ -290,20 +290,29 @@ class ComponentBlueprint(yamlize.Object):
                     "Cannot apply custom densities to materials without density."
                 )
 
-            # Apply a density scaling to account for the temperature change between Tinput
-            # Thot. There may be a better place in the initialization to determine
-            # if the block height will be interpreted as hot dimensions, which would
-            # allow us to not have to pass the case settings down this far
-            dLL = comp.material.linearExpansionFactor(
-                Tc=comp.temperatureInC, T0=comp.inputTemperatureInC
-            )
-            if inputHeightsConsideredHot:
-                f = 1.0 / (1 + dLL) ** 2
+            # Apply a density scaling to account for the temperature change between
+            # Tinput and Thot
+            if isinstance(mat, materials.Fluid):
+                densityRatio = densityFromCustomIsotopic / mat.density(
+                    Tc=comp.inputTemperatureInC
+                )
             else:
-                f = 1.0 / (1 + dLL) ** 3
+                # for solids we need to consider if the input heights are hot or
+                # cold, in order to get the density correct.
+                # There may be a better place in the initialization to determine
+                # if the block height will be interpreted as hot dimensions, which would
+                # allow us to not have to pass the case settings down this far
+                dLL = mat.linearExpansionFactor(
+                    Tc=comp.temperatureInC, T0=comp.inputTemperatureInC
+                )
+                if inputHeightsConsideredHot:
+                    f = 1.0 / (1 + dLL) ** 2
+                else:
+                    f = 1.0 / (1 + dLL) ** 3
+
+                scaledDensity = comp.density() / f
+                densityRatio = densityFromCustomIsotopic / scaledDensity
 
-            scaledDensity = comp.density() / f
-            densityRatio = densityFromCustomIsotopic / scaledDensity
             comp.changeNDensByFactor(densityRatio)
 
             runLog.important(
@@ -369,10 +378,18 @@ class ComponentBlueprint(yamlize.Object):
             try:
                 # update material with updated input params from blueprints file.
                 mat.applyInputParams(**matMods)
-            except TypeError:
-                # This component does not accept material modification inputs of the names passed in
-                # Keep going since the modification could work for another component
-                pass
+            except TypeError as ee:
+                errorMessage = ee.args[0]
+                if "got an unexpected keyword argument" in errorMessage:
+                    # This component does not accept material modification inputs of the names passed in
+                    # Keep going since the modification could work for another component
+                    pass
+                else:
+                    raise ValueError(
+                        f"Something went wrong in applying the material modifications {matMods} "
+                        f"to component {self.name}.\n"
+                        f"Error message is: \n{errorMessage}."
+                    )
 
         expandElementals(mat, blueprint)
 
diff --git a/armi/runLog.py b/armi/runLog.py
index 484672df..b7393bd6 100644
--- a/armi/runLog.py
+++ b/armi/runLog.py
@@ -572,7 +572,7 @@ class RunLogger(logging.Logger):
         handler.setFormatter(form)
         self.addHandler(handler)
 
-    def log(self, msgType, msg, single=False, label=None, **kwargs):
+    def log(self, msgType, msg, single=False, label=None, *args, **kwargs):
         """
         This is a wrapper around logger.log() that does most of the work.
 
diff --git a/doc/conf.py b/doc/conf.py
index e83bc4c8..e40b0367 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -24,6 +24,7 @@ autogenerated file.
 All configuration values have a default; values that are commented out
 serve to show the default.
 """
+
 # ruff: noqa: E402
 import datetime
 import inspect
@@ -35,7 +36,7 @@ import subprocess
 import sys
 import warnings
 
-import sphinx_rtd_theme
+import sphinx_rtd_theme  # noqa: F401
 from docutils import nodes, statemachine
 from docutils.parsers.rst import Directive, directives
 from sphinx.domains.python import PythonDomain
@@ -96,17 +97,15 @@ class ExecDirective(Directive):
 
     def run(self):
         try:
-            code = inspect.cleandoc(
-                """
-            def usermethod():
-                {}
-            """
-            ).format("\n    ".join(self.content))
-            exec(code)
-            result = locals()["usermethod"]()
+            # clean the content, then put back into a list
+            cleancode = inspect.cleandoc("\n".join(self.content)).split("\n")
+            code = "def usermethod():\n    " + "\n    ".join(cleancode)
+            globals = {}
+            exec(code, globals)
 
-            if result is None:
+            result = globals["usermethod"]()
 
+            if result is None:
                 raise self.error(
                     "Return value needed! The body of your `.. exec::` is used as a "
                     "function call that must return a value."
@@ -119,9 +118,7 @@ class ExecDirective(Directive):
         except Exception as e:
             docname = self.state.document.settings.env.docname
             raise self.error(
-                "Unable to execute embedded doc code at {}:{} ... {}\n{}".format(
-                    docname, self.lineno, datetime.datetime.now(), str(e)
-                )
+                f"Unable to execute embedded doc code at {docname}:{self.lineno}\n{str(e)}"
             )
 
 
@@ -363,6 +360,11 @@ release = meta.__version__
 exclude_patterns = [
     "**/Python27*",
     "**/ccl*",
+    # prevent sphinx-gallery from causing duplicate source file errors
+    "gallery/**/*.ipynb",
+    "gallery/**/*.md5",
+    "gallery/**/*.zip",
+    "gallery/**/*.json",
     "**.ipynb_checkpoints",
     "_build",
 ]  # , '**/tests*']
@@ -410,9 +412,6 @@ html_theme_options = {
     "titles_only": False,
 }
 
-# Add any paths that contain custom themes here, relative to this directory.
-html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
-
 # as long as this file @import's the theme's main css it won't break anything
 html_style = "css/theme_fixes.css"
 
diff --git a/doc/gallery-src/framework/run_chartOfNuclides.py b/doc/gallery-src/framework/run_chartOfNuclides.py
index f0818d7e..5a4516ff 100644
--- a/doc/gallery-src/framework/run_chartOfNuclides.py
+++ b/doc/gallery-src/framework/run_chartOfNuclides.py
@@ -22,7 +22,9 @@ coloring the squares with the natural abundance.
 
     Our :ref:`extended tutorial for nuclides </tutorials/nuclide_demo.ipynb>` and
     detailed :py:mod:`nucDirectory docs <armi.nucDirectory>` may also be of interest.
+
 """
+
 import matplotlib.pyplot as plt
 
 from armi import configure
diff --git a/doc/gallery-src/framework/run_computeReactionRates.py b/doc/gallery-src/framework/run_computeReactionRates.py
index 1a7c2d52..2c2578e2 100644
--- a/doc/gallery-src/framework/run_computeReactionRates.py
+++ b/doc/gallery-src/framework/run_computeReactionRates.py
@@ -28,7 +28,7 @@ import numpy as np
 from armi import configure, nuclideBases, settings
 from armi.materials import ht9, sodium, uZr
 from armi.nuclearDataIO.cccc import isotxs
-from armi.reactor import assemblies, blocks, blueprints, geometry, grids, reactors
+from armi.reactor import assemblies, blocks, geometry, grids, reactors
 from armi.reactor.components import Circle, DerivedShape, Hexagon
 from armi.reactor.flags import Flags
 from armi.tests import ISOAA_PATH
@@ -56,7 +56,9 @@ def createDummyReactor():
     Often, a reactor model like this is built directly from input files rather
     than from code as done here.
     """
-    bp = blueprints.Blueprints()
+    from armi.reactor.blueprints import Blueprints
+
+    bp = Blueprints()
     cs = settings.Settings()
 
     r = reactors.Reactor("Reactor", bp)
diff --git a/doc/gallery-src/framework/run_fuelManagement.py b/doc/gallery-src/framework/run_fuelManagement.py
index a55adcab..7512f8c1 100644
--- a/doc/gallery-src/framework/run_fuelManagement.py
+++ b/doc/gallery-src/framework/run_fuelManagement.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 """
 Fuel management in a LWR.
+=========================
 
 Demo of locating and swapping assemblies in a core with Cartesian geometry. Given a burnup
 distribution, this swaps high burnup assemblies with low ones.
@@ -26,6 +27,7 @@ flux/depletion results would need to be provided by a physics plugin before actu
 ARMI to do fuel management. Thus, this example applies a dummy burnup distribution for
 demonstration purposes.
 """
+
 # Tell the gallery to feature the 2nd image
 # sphinx_gallery_thumbnail_number = 2
 import math
diff --git a/doc/release/0.5.rst b/doc/release/0.5.rst
index 33051845..1dc228f6 100644
--- a/doc/release/0.5.rst
+++ b/doc/release/0.5.rst
@@ -21,6 +21,7 @@ Bug Fixes
 ---------
 #. Fixing check for jagged arrays during ``_writeParams``. (`PR#2051 <https://github.com/terrapower/armi/pull/2051>`_)
 #. Fixing BP-section ignoring tool in ``PassiveDBLoadPlugin``. (`PR#2055 <https://github.com/terrapower/armi/pull/2055>`_)
+#. Fixing number densities when custom isotopics are combined with Fluid components. (`PR#2071 <https://github.com/terrapower/armi/pull/2071>`_)
 #. Fixing scaling of volume-integrated parameters on edge assemblies. (`PR#2060 <https://github.com/terrapower/armi/pull/2060>`_)
 #. Fixing strictness of ``HexGrid`` rough equality check. (`PR#2058 <https://github.com/terrapower/armi/pull/2058>`_)
 #. TBD
@@ -28,6 +29,7 @@ Bug Fixes
 Quality Work
 ------------
 #. Adding PDF versions of the ARMI docs. (`PR#2072 <https://github.com/terrapower/armi/pull/2072>`_)
+#. Update docs build to occur with python 3.13 and updated docs dependencies. (`PR#2050 <https://github.com/terrapower/armi/pull/2050>`_)
 #. TBD
 
 
diff --git a/pyproject.toml b/pyproject.toml
index 033b68a7..cd188f8f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -92,31 +92,27 @@ test = [
 docs = [
 #######################################################################
 # These are most specified that usual, because Sphinx docs seem to be
-# quite fragile limited <7 by sphinx-rtd-theme at the moment.
+# quite fragile.
 #
-# sphinx-rtd-theme requires docutils <0.19 but sphinx dropped support
-# for 0.18 in 6.0.0 so we're stuck at these versions.
-#
-# We are only building our docs with Python 3.9.
+# We are only building our docs with Python 3.13.
 #######################################################################
-    "Sphinx==5.3.0", # central library used to build our docs
-    "docutils==0.18.1", # Needed by sphinx-rtd-them
-    "sphinx-rtd-theme==1.2.2", # Read-The-Docs theme for Sphinx
-    "nbsphinx==0.9.2", # Parses Jupyter notebooks
-    "nbsphinx-link==1.3.0", # Adds Jupyter NBs to Sphinx source root
-    "sphinx-gallery==0.13.0", # Builds an HTML version of a Python script and puts it into a gallery
-    "sphinxcontrib-apidoc==0.3.0", # More easily document our API
-    "sphinxext-opengraph==0.8.2", # Generates OpenGraph metadata to make good-looking cards on social media
-    "sphinx-needs==1.2.2", # Requirements traceability matrices for QA
-    "sphinxcontrib-plantuml==0.25", # UML support in sphinx-needs
+    "Sphinx==8.1.3", # central library used to build our docs
+    "docutils==0.21.2", # Needed by sphinx-rtd-them
+    "sphinx-rtd-theme==3.0.2", # Read-The-Docs theme for Sphinx
+    "nbsphinx", # Parses Jupyter notebooks
+    "nbsphinx-link", # Adds Jupyter NBs to Sphinx source root
+    "sphinx-gallery", # Builds an HTML version of a Python script and puts it into a gallery
+    "sphinxcontrib-apidoc==0.5.0", # More easily document our API
+    "sphinxext-opengraph", # Generates OpenGraph metadata to make good-looking cards on social media
+    "sphinx-needs==4.1.0", # Requirements traceability matrices for QA
+    "sphinxcontrib-plantuml==0.30", # UML support in sphinx-needs
     "pandoc", # Must be in the path (to convert file formats)
-    "ipykernel==6.25.1", # iPython kernel to run Jupyter notebooks
-    "pylint==2.17.5", # Generates UML diagrams
-    "Jinja2==3.0.3", # Used in numpydoc and nbconvert
+    "ipykernel", # iPython kernel to run Jupyter notebooks
+    "pylint", # Generates UML diagrams
+    "Jinja2", # Used in numpydoc and nbconvert
     "sphinxcontrib-jquery==4.1", # Handle missing jquery errors
-    "jupyter-contrib-nbextensions", # A collections of JS extensions for jupyter notebooks
-    "lxml<5.0.0", # Needed because the dep above is no longer an active project
     "sphinx-simplepdf==1.6.0", # Used to make PDF versions of the docs
+    "setuptools",  # needed for conf.py tooling
 ]
 
 [project.scripts]


Test patch:
diff --git a/armi/nucDirectory/tests/test_nuclideBases.py b/armi/nucDirectory/tests/test_nuclideBases.py
index 1ef1b590..066d0f5b 100644
--- a/armi/nucDirectory/tests/test_nuclideBases.py
+++ b/armi/nucDirectory/tests/test_nuclideBases.py
@@ -466,11 +466,9 @@ class TestAAAZZZSId(unittest.TestCase):
             :tests: R_ARMI_ND_ISOTOPES
         """
         referenceNucNames = [
-            ("C", "120060"),
+            ("C12", "120060"),
             ("U235", "2350920"),
             ("AM242M", "2420951"),
-            ("LFP35", None),
-            ("DUMP1", None),
         ]
 
         for nucName, refAaazzzs in referenceNucNames:
diff --git a/armi/reactor/blueprints/tests/test_customIsotopics.py b/armi/reactor/blueprints/tests/test_customIsotopics.py
index 27ce95ed..c79a2343 100644
--- a/armi/reactor/blueprints/tests/test_customIsotopics.py
+++ b/armi/reactor/blueprints/tests/test_customIsotopics.py
@@ -19,6 +19,7 @@ from logging import DEBUG
 import yamlize
 
 from armi import runLog, settings
+from armi.materials import Fluid, Sodium
 from armi.physics.neutronics.settings import (
     CONF_MCNP_LIB_BASE,
     CONF_NEUTRONICS_KERNEL,
@@ -42,6 +43,7 @@ nuclide flags:
     AL: {burn: false, xs: true}
     FE: {burn: false, xs: true}
     C: {burn: false, xs: true}
+    NA: {burn: false, xs: true}
     DUMP2: {burn: true, xs: true}
     DUMP1: {burn: true, xs: true}
     LFP35: {burn: true, xs: true}
@@ -118,6 +120,11 @@ custom isotopics:
         C: 0.3
         density: 7.0
 
+    sodium custom isotopics:
+        input format: mass fractions
+        NA: 1
+        density: 666
+
 """
 
     yamlGoodBlocks = r"""
@@ -141,6 +148,25 @@ blocks:
             mult: 1.0
             od: 10.0
 
+        sodium1:
+            shape: Circle
+            material: Sodium
+            Tinput: 25
+            Thot: 600
+            id: 0
+            mult: 1
+            od: 1
+
+        sodium2:
+            shape: Circle
+            material: Sodium
+            isotopics: sodium custom isotopics
+            Tinput: 25
+            Thot: 600
+            id: 0
+            mult: 1
+            od: 1
+
     uranium fuel from isotopic mass fractions : &block_1
         fuel:
             <<: *basic_fuel
@@ -381,6 +407,35 @@ assemblies:
         self.assertEqual(fuel6.material.name, fuel0.material.name)
         self.assertEqual("UZr", fuel0.material.name)
 
+    def test_densitiesAppliedToNonCustomMaterialsFluid(self):
+        """
+        Ensure that a density can be set in custom isotopics for components using library materials,
+        specifically in the case of a fluid component. In this case, inputHeightsConsideredHot
+        does not matter, and the material has a zero dLL value.
+        """
+        # The template block
+        sodium1 = self.a[0].getComponentByName("sodium1")
+        sodium2 = self.a[0].getComponentByName("sodium2")
+
+        self.assertEqual(sodium1.material.name, "Sodium")
+        self.assertEqual(sodium2.material.name, "Sodium")
+        self.assertTrue(isinstance(sodium1.material, Fluid))
+        self.assertTrue(isinstance(sodium2.material, Fluid))
+        self.assertEqual(sodium1.p.customIsotopicsName, "")
+        self.assertEqual(sodium2.p.customIsotopicsName, "sodium custom isotopics")
+
+        # show that, even though the two components have the same material class
+        # and the same temperatures, their densities are different
+        self.assertNotEqual(sodium1.density(), sodium2.density())
+
+        # show that sodium1 has a density from the material class, while sodium2
+        # has a density from the blueprint and adjusted from Tinput -> Thot
+        s = Sodium()
+        self.assertAlmostEqual(sodium1.density(), s.density(Tc=600))
+        self.assertAlmostEqual(
+            sodium2.density(), s.density(Tc=600) * (666 / s.density(Tc=25))
+        )
+
     def test_customDensityLogsAndErrors(self):
         """Test that the right warning messages and errors are emitted when applying custom densities."""
         # Check for warnings when specifying both TD_frac and custom isotopics
diff --git a/armi/reactor/blueprints/tests/test_materialModifications.py b/armi/reactor/blueprints/tests/test_materialModifications.py
index 52ada251..eaccb00d 100644
--- a/armi/reactor/blueprints/tests/test_materialModifications.py
+++ b/armi/reactor/blueprints/tests/test_materialModifications.py
@@ -259,7 +259,7 @@ assemblies:
 
     def test_invalidMatModName(self):
         """
-        This test shows proves that we can detect invalid material modification
+        This test shows that we can detect invalid material modification
         names when they are specified on an assembly blueprint. We happen to know
         that ZR_wt_frac is a valid modification for the UZr material class, so we
         use that in the first call to prove that things initially work fine.
@@ -299,6 +299,35 @@ assemblies:
         """
             )
 
+    def test_invalidMatModType(self):
+        """
+        This test shows that we can detect material modifications that are invalid
+        because of their values, not just their names.
+        We happen to know that ZR_wt_frac is a valid modification for UZr, so we
+        use that in the first call to prove that things initially work fine.
+        """
+        a = self.loadUZrAssembly(
+            """
+        material modifications:
+            ZR_wt_frac: [1]
+        """
+        )
+        # just to prove that the above works fine before we modify it
+        self.assertAlmostEqual(a[0][0].getMassFrac("ZR"), 1)
+
+        with self.assertRaises(ValueError) as ee:
+            a = self.loadUZrAssembly(
+                """
+        material modifications:
+            ZR_wt_frac: [this_is_a_value_of_incompatible_type]
+        """
+            )
+
+            self.assertIn(
+                "Something went wrong in applying the material modifications",
+                ee.args[0],
+            )
+
     def test_matModsUpTheMRO(self):
         """
         Make sure that valid/invalid material modifications are searched up
diff --git a/armi/reactor/blueprints/tests/test_reactorBlueprints.py b/armi/reactor/blueprints/tests/test_reactorBlueprints.py
index 2e107517..0fa76239 100644
--- a/armi/reactor/blueprints/tests/test_reactorBlueprints.py
+++ b/armi/reactor/blueprints/tests/test_reactorBlueprints.py
@@ -143,7 +143,12 @@ class TestReactorBlueprints(unittest.TestCase):
 
     def test_materialDataSummary(self):
         """Test that the material data summary for the core is valid as a printout to the stdout."""
-        expectedMaterialData = [("Custom", "ARMI"), ("HT9", "ARMI"), ("UZr", "ARMI")]
+        expectedMaterialData = [
+            ("Custom", "ARMI"),
+            ("HT9", "ARMI"),
+            ("Sodium", "ARMI"),
+            ("UZr", "ARMI"),
+        ]
         core, _sfp, _evst = self._setupReactor()
         materialData = reactorBlueprint.summarizeMaterialData(core)
         for actual, expected in zip(materialData, expectedMaterialData):