Repository: terrapower/armi
Pull request #2075 (2025-02-04 19:11:37)

Ignoring Fluid density warning for Component Fluids
## What is the change?

Calling `Component.material.density` does not log a warning with a traceback if the material is a subclass of `Fluid`

## Why is the change being made?

TH applications need fluid densities and the traceback invocation is kind of a bottleneck.

https://github.com/terrapower/armi/pull/1852 introduced a warning when `Component.material.density(...)` was invoked. The rationale is the `Component` is the source of truth for volume, density, mass, etc. And components are the things that are thermally expanded.

However, fluids are not thermally expanded and so the density of a fluid material is appropriate.

This change undoes the density wrapper applied by the base `Material` when creating a `Fluid` subclass.

A test is added to show that any non-`Fluid` subclass still logs the warning, but a `Fluid` subclass does not.

I attempted to use a "real" material like `Sodium` but lots of other warnings can be logged depending on appropriate bounds of temperatures. So a fake Fluid is used to have more control over what warnings are logged.

I contend that the stack trace should still be used in this message. This is a message meant for application developers to root out problematic parts of their codebase. And it helped us pinpoint very specific call locations with ease.

---

## 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 -->

- [ ] 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`.


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/materials/material.py b/armi/materials/material.py
index 06593dbe..dae46a74 100644
--- a/armi/materials/material.py
+++ b/armi/materials/material.py
@@ -757,6 +757,12 @@ class Material:
 class Fluid(Material):
     """A material that fills its container. Could also be a gas."""
 
+    def __init_subclass__(cls):
+        # Undo the parent-aware density wrapping. Fluids do not expand in the same way solids, so
+        # Fluid.density(T) is correct. This does not hold for solids because they thermally expand.
+        if hasattr(cls.density, "__wrapped__"):
+            cls.density = cls.density.__wrapped__
+
     def getThermalExpansionDensityReduction(self, prevTempInC, newTempInC):
         """Return the factor required to update thermal expansion going from one temperature (in
         Celcius) to a new temperature.
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 54676cbe..838c5525 100644
--- a/armi/reactor/blueprints/componentBlueprint.py
+++ b/armi/reactor/blueprints/componentBlueprint.py
@@ -378,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 11a81876..5ddbe734 100644
--- a/doc/release/0.5.rst
+++ b/doc/release/0.5.rst
@@ -10,6 +10,8 @@ New Features
 ------------
 #. Move instead of copy files from TemporaryDirectoryChanger. (`PR#2022 <https://github.com/terrapower/armi/pull/2022>`_)
 #. Creating the ``armi.testing`` module, to share ARMI testing tools. (`PR#2028 <https://github.com/terrapower/armi/pull/2028>`_)
+#. Invoking ``component.material.density()``  does not log an expensive stack trace if the material
+   is a fluid. (`PR#2075 <https://github.com/terrapower/armi/pull/2075>`_)
 #. TBD
 
 API Changes
@@ -29,6 +31,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/materials/tests/test_fluids.py b/armi/materials/tests/test_fluids.py
new file mode 100644
index 00000000..ad59c8fd
--- /dev/null
+++ b/armi/materials/tests/test_fluids.py
@@ -0,0 +1,63 @@
+# Copyright 2025 TerraPower, LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Unit tests for fluid-specific behaviors.
+
+The ARMI framework has a lot of thermal expansion machinery that applies to all components
+but doesn't make sense for fluids. The tests here help show fluid materials still
+play nice with the rest of the framework.
+"""
+
+from unittest import TestCase
+
+from armi.materials.material import Fluid, Material
+from armi.reactor.components import Circle
+from armi.tests import mockRunLogs
+
+
+class TestFluids(TestCase):
+    class MyFluid(Fluid):
+        """Stand-in fluid that doesn't provide lots of functionality."""
+
+    class MySolid(Material):
+        """Stand-in solid that doesn't provide lots of functionality."""
+
+    def test_fluidDensityWrapperNoWarning(self):
+        """Test that Component.material.density does not raise a warning for fluids.
+
+        The ARMI Framework contains a mechanism to warn users if they ask for the density of a
+        material attached to a component. But the component is the source of truth for volume and
+        composition. And can be thermally expanded during operation. Much of the framework operates
+        on ``Component.density`` and other ``Component`` methods for mass accounting. However,
+        ``comp.material.density`` does not know about the new composition or volumes and can diverge
+        from ``component.density``.
+
+        Additionally, the framework does not do any thermal expansion on fluids. So the above calls
+        to ``component.material.density`` are warranted for fluids.
+        """
+        self._checkCompDensityLogs(
+            mat=self.MySolid(),
+            nExpectedWarnings=1,
+            msg="Solids should have the density warning logged.",
+        )
+        self._checkCompDensityLogs(
+            mat=self.MyFluid(),
+            nExpectedWarnings=0,
+            msg="Fluids should not have the density warning logged.",
+        )
+
+    def _checkCompDensityLogs(self, mat: Material, nExpectedWarnings: int, msg: str):
+        comp = Circle(name="test", material=mat, Tinput=20, Thot=20, id=0, od=1, mult=1)
+        with mockRunLogs.LogCounter() as logs:
+            comp.material.density(Tc=comp.temperatureInC)
+        self.assertEqual(logs.messageCounts["warning"], nExpectedWarnings, msg=msg)
diff --git a/armi/materials/tests/test_materials.py b/armi/materials/tests/test_materials.py
index 22b125f7..5d6afd43 100644
--- a/armi/materials/tests/test_materials.py
+++ b/armi/materials/tests/test_materials.py
@@ -96,8 +96,12 @@ class _Material_Test:
         self.assertEqual(dens * 1000.0, densKgM3)
 
     def test_wrappedDensity(self):
-        """Test that the density decorator is applied."""
-        self.assertTrue(hasattr(self.mat.density, "__wrapped__"))
+        """Test that the density decorator is applied to non-fluids."""
+        self.assertEqual(
+            hasattr(self.mat.density, "__wrapped__"),
+            not isinstance(self.mat, materials.Fluid),
+            msg=self.mat,
+        )
 
 
 class MaterialConstructionTests(unittest.TestCase):
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_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