Repository: terrapower/armi

Issue:
No warning when material modification is unused
Right now if you define an assembly with a material modification that is not recognized, ARMI does not tell you that it isn't recognized or that it is unused. For instance, I can define an assembly:

```yaml
assemblies:
  Testing123456:
    specifier: IC
    blocks:
    - name: DriverFuelBlock
      flags: FUEL
      fuel:
        shape: Circle
        material: UZr
        Tinput: 25.0
        Thot: 1500.0
        id: 0.0
        od: 0.5
        mult: 217
    height:
    - 666.0
    axial mesh points:
    - 666
    xs types:
    - B
    material modifications:
      this_is_a_fake_setting: # <--------------------------
        - 0.5
   ```

No mention of the modification `this_is_a_fake_setting` is made in the output. It would be useful for the user to know that their modifications are not recognized, at the very least as an input verification mechanism.

Discussion:
john-science: This is a much cleaner solution than I expected.

It really seems like `from inspect import signature` is the star of the show here.  Nice find.

I'm trying to decide if this solution will always work in the situation where we have other materials libraries ingested in some big ARMI Application.  I think it will, because by the time you get to reading the blueprints, all the materials needs-must have been designed. Wherever they came from.



Patch:
diff --git a/armi/reactor/blueprints/blockBlueprint.py b/armi/reactor/blueprints/blockBlueprint.py
index 0a41a9e0..324bffce 100644
--- a/armi/reactor/blueprints/blockBlueprint.py
+++ b/armi/reactor/blueprints/blockBlueprint.py
@@ -16,6 +16,7 @@
 This module defines the ARMI input for a block definition, and code for constructing an ARMI ``Block``.
 """
 import collections
+from inspect import signature
 
 import yamlize
 
@@ -24,6 +25,7 @@ from armi.reactor import blocks
 from armi.reactor import parameters
 from armi.reactor.flags import Flags
 from armi.reactor.blueprints import componentBlueprint
+from armi.reactor.components.component import Component
 from armi.reactor.converters import blockConverters
 from armi.settings.fwSettings import globalSettings
 
@@ -115,11 +117,26 @@ class BlockBlueprint(yamlize.KeyedList):
         self._checkByComponentMaterialInput(materialInput)
 
         for componentDesign in self:
-            filteredMaterialInput = self._filterMaterialInput(
+            filteredMaterialInput, byComponentMatModKeys = self._filterMaterialInput(
                 materialInput, componentDesign
             )
             c = componentDesign.construct(blueprint, filteredMaterialInput)
             components[c.name] = c
+
+            # check that the mat mods for this component are valid options
+            # this will only examine by-component mods, block mods are done later
+            if isinstance(c, Component):
+                # there are other things like composite groups that don't get
+                # material modifications -- skip those
+                validMatModOptions = signature(
+                    c.material.applyInputParams
+                ).parameters.keys()
+                for key in byComponentMatModKeys:
+                    if key not in validMatModOptions:
+                        raise ValueError(
+                            f"{c} in block {self.name} has invalid material modification: {key}"
+                        )
+
             if spatialGrid:
                 componentLocators = gridDesign.getMultiLocator(
                     spatialGrid, componentDesign.latticeIDs
@@ -141,6 +158,24 @@ class BlockBlueprint(yamlize.KeyedList):
                         # learn mult from grid definition
                         c.setDimension("mult", len(c.spatialLocator))
 
+        # check that the block level mat mods use valid options in the same way
+        # as we did for the by-component mods above
+        validMatModOptions = set()
+        for c in components.values():
+            if isinstance(c, Component):
+                # there are other things like composite groups that don't get
+                # material modifications -- skip those
+                validMatModOptions |= signature(
+                    c.material.applyInputParams
+                ).parameters.keys()
+
+        if "byBlock" in materialInput:
+            for key in materialInput["byBlock"]:
+                if key not in validMatModOptions:
+                    raise ValueError(
+                        f"Block {self.name} has invalid material modification key: {key}"
+                    )
+
         # Resolve linked dims after all components in the block are created
         for c in components.values():
             c.resolveLinkedDims(components)
@@ -216,6 +251,7 @@ class BlockBlueprint(yamlize.KeyedList):
         for a given component, the by-component value will be used.
         """
         filteredMaterialInput = {}
+        byComponentMatModKeys = set()
 
         # first add the by-block modifications without question
         if "byBlock" in materialInput:
@@ -232,9 +268,10 @@ class BlockBlueprint(yamlize.KeyedList):
                 # overwriting any by-block modifications of the same type
                 if component == componentDesign.name:
                     for modName, modVal in mod.items():
+                        byComponentMatModKeys.add(modName)
                         filteredMaterialInput[modName] = modVal
 
-        return filteredMaterialInput
+        return filteredMaterialInput, byComponentMatModKeys
 
     def _getGridDesign(self, blueprint):
         """


Test patch:
diff --git a/armi/reactor/blueprints/tests/test_materialModifications.py b/armi/reactor/blueprints/tests/test_materialModifications.py
index f390710c..5109d246 100644
--- a/armi/reactor/blueprints/tests/test_materialModifications.py
+++ b/armi/reactor/blueprints/tests/test_materialModifications.py
@@ -189,7 +189,7 @@ assemblies:
             "fuel2": {"ZR_wt_frac": 0.3, "U235_wt_frac": 0.3},
         }
         componentDesign = a[0][0]
-        filteredMaterialInput = BlockBlueprint._filterMaterialInput(
+        filteredMaterialInput, _ = BlockBlueprint._filterMaterialInput(
             materialInput, componentDesign
         )
 
@@ -197,6 +197,48 @@ assemblies:
 
         self.assertEqual(filteredMaterialInput, filteredMaterialInput_reference)
 
+    def test_invalidMatModName(self):
+        """
+        This test shows proves 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.
+        """
+        a = self.loadUZrAssembly(
+            """
+        material modifications:
+            ZR_wt_frac: [1]
+            by component:
+                fuel2:
+                    ZR_wt_frac: [0]
+        """
+        )
+        # just to prove that the above works fine before we modify it
+        self.assertAlmostEqual(a[0][0].getMassFrac("ZR"), 1)
+        self.assertAlmostEqual(a[0][1].getMassFrac("ZR"), 0)
+
+        with self.assertRaises(ValueError):
+            a = self.loadUZrAssembly(
+                """
+        material modifications:
+            this_is_a_fake_name: [1]
+            by component:
+                fuel2:
+                    ZR_wt_frac: [0]
+        """
+            )
+
+        with self.assertRaises(ValueError):
+            a = self.loadUZrAssembly(
+                """
+        material modifications:
+            ZR_wt_frac: [1]
+            by component:
+                fuel2:
+                    this_is_a_fake_name: [0]
+        """
+            )
+
 
 if __name__ == "__main__":
     unittest.main()