Repository: terrapower/armi
Pull request #2086 (2025-02-26 19:29:35)

Account for symmetry factor in `getMgFlux`
## What is the change?

Implement a `getMgFlux` method for `Component` instead of inheriting from the `Composite` class.

## Why is the change being made?

The component volume does not account for the parent symmetry factor (see #2006), so we need a special `getMgFlux` that incorporates the symmetry factor here.

This should have been included in #2068 , which addressed #2059 , but this was missed. `getMgFlux` uses `getIntegratedMgFlux`, which was updated to incorporate the symmetry factor. `getMgFlux` needs to also incoporate the `symmetryFactor` to stay consistent.


---

## Checklist

<!--
    You (the pull requester) should put an `x` if the condition is met.

    (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.
- [x] The code style follows [good practices](https://terrapower.github.io/armi/developer/standards_and_practices.html).
- [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`.

john-science: Hmm...  I kind of hate that we have `Composite.getMgFlux()` and `Block.getMgFlux()`. That we have to add `Component.getMgFlux()` just compounds it.

Is there no more generic way to solve this problem that adding a `getMyFavoriteParameter()` method for every parameter in the world to every class in the data object model?

I really feel like there should be.
mgjarrett: > Hmm... I kind of hate that we have `Composite.getMgFlux()` and `Block.getMgFlux()`. That we have to add `Component.getMgFlux()` just compounds it.
> 
> Is there no more generic way to solve this problem that adding a `getMyFavoriteParameter()` method for every parameter in the world to every class in the data object model?
> 
> I really feel like there should be.

If we resolve #2006 then we would be able to let `Component` inherit `getMgFlux` from `Composite`

Patch:
diff --git a/armi/reactor/components/component.py b/armi/reactor/components/component.py
index 6fe3ca58..b2640f53 100644
--- a/armi/reactor/components/component.py
+++ b/armi/reactor/components/component.py
@@ -1221,6 +1221,41 @@ class Component(composites.Composite, metaclass=ComponentType):
             )
         self.setMassFracs(adjustedMassFracs)
 
+    def getMgFlux(self, adjoint=False, average=False, volume=None, gamma=False):
+        """
+        Return the multigroup neutron flux in [n/cm^2/s].
+
+        The first entry is the first energy group (fastest neutrons). Each additional
+        group is the next energy group, as set in the ISOTXS library.
+
+        Parameters
+        ----------
+        adjoint : bool, optional
+            Return adjoint flux instead of real
+        average : bool, optional
+            If True, will return average flux between latest and previous. Doesn't work
+            for pin detailed.
+        volume: float, optional
+            The volume-integrated flux is divided by volume before
+            being returned. The user may specify a volume here, or the function
+            will obtain the block volume directly.
+        gamma : bool, optional
+            Whether to return the neutron flux or the gamma flux.
+
+        Returns
+        -------
+        flux : np.ndarray
+            multigroup neutron flux in [n/cm^2/s]
+        """
+        if average:
+            raise NotImplementedError(
+                "Component has no method for producing average MG flux -- try"
+                "using blocks"
+            )
+
+        volume = volume or self.getVolume() / self.parent.getSymmetryFactor()
+        return self.getIntegratedMgFlux(adjoint=adjoint, gamma=gamma) / volume
+
     def getIntegratedMgFlux(self, adjoint=False, gamma=False):
         """
         Return the multigroup neutron tracklength in [n-cm/s].
diff --git a/armi/reactor/composites.py b/armi/reactor/composites.py
index 974ade8b..8d4420b8 100644
--- a/armi/reactor/composites.py
+++ b/armi/reactor/composites.py
@@ -2079,8 +2079,8 @@ class ArmiObject(metaclass=CompositeModelType):
             for pin detailed yet
 
         volume: float, optional
-            If average=True, the volume-integrated flux is divided by volume before
-            being returned. The user may specify a volume here, or the function will
+            The volume-integrated flux is divided by volume before being
+            returned. The user may specify a volume here, or the function will
             obtain the block volume directly.
 
         gamma : bool, optional
diff --git a/doc/release/0.5.rst b/doc/release/0.5.rst
index bf68048d..9a0d9385 100644
--- a/doc/release/0.5.rst
+++ b/doc/release/0.5.rst
@@ -27,7 +27,7 @@ Bug Fixes
 #. 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>`_)
-#  Fixing treatment of symmetry factors when calculating component flux and reaction rates. (`PR#2068 <https://github.com/terrapower/armi/pull/2068>`_)
+#  Fixing treatment of symmetry factors when calculating component flux and reaction rates. (`PR#2068 <https://github.com/terrapower/armi/pull/2068>`_, `PR#2086 <https://github.com/terrapower/armi/pull/2086>`_))
 #. TBD
 
 Quality Work


Test patch:
diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py
index e57298e2..88482103 100644
--- a/armi/reactor/tests/test_blocks.py
+++ b/armi/reactor/tests/test_blocks.py
@@ -1262,29 +1262,54 @@ class Block_TestCase(unittest.TestCase):
     def test_getMgFlux(self, mock_sf):
         # calculate Mg Flux with a Symmetry Factor of 3
         mock_sf.return_value = 3
-        self.block.p.mgFlux = np.array([1, 1, 1, 1, 1])
-        self.block.p.mgFluxGamma = np.array([2, 2, 2, 2])
+        neutronFlux = 1.0
+        gammaFlux = 2.0
+        self.block.p.mgFlux = np.full(5, neutronFlux)
+        self.block.p.mgFluxGamma = np.full(4, gammaFlux)
         fuel = self.block.getComponent(Flags.FUEL)
         blockVol = self.block.getVolume()
         fuelVol = fuel.getVolume()
+        # compute volume fraction of component; need symmetry factor
         volFrac = fuelVol / blockVol / self.block.getSymmetryFactor()
-        neutronFlux = fuel.getIntegratedMgFlux()
-        gammaFlux = fuel.getIntegratedMgFlux(gamma=True)
-        np.testing.assert_almost_equal(neutronFlux, np.ones(5) * volFrac)
-        np.testing.assert_almost_equal(gammaFlux, np.ones(4) * volFrac * 2.0)
+        neutronFluxInt = fuel.getIntegratedMgFlux()
+        gammaFluxInt = fuel.getIntegratedMgFlux(gamma=True)
+        # getIntegratedMgFlux should be scaled by the component volume fraction
+        np.testing.assert_almost_equal(
+            neutronFluxInt, np.full(5, neutronFlux * volFrac)
+        )
+        np.testing.assert_almost_equal(gammaFluxInt, np.full(4, gammaFlux * volFrac))
+
+        # getMgFlux should return regular, non-integrated flux
+        neutronMgFlux = fuel.getMgFlux()
+        gammaMgFlux = fuel.getMgFlux(gamma=True)
+        np.testing.assert_almost_equal(
+            neutronMgFlux, np.full(5, neutronFlux / blockVol)
+        )
+        np.testing.assert_almost_equal(gammaMgFlux, np.full(4, gammaFlux / blockVol))
 
         # calculate Mg Flux with a Symmetry Factor of 1
         mock_sf.return_value = 1
-        self.block.p.mgFlux = np.array([1, 1, 1, 1, 1])
-        self.block.p.mgFluxGamma = np.array([2, 2, 2, 2])
+        self.block.p.mgFlux = np.full(5, neutronFlux)
+        self.block.p.mgFluxGamma = np.full(4, gammaFlux)
         fuel = self.block.getComponent(Flags.FUEL)
         blockVol = self.block.getVolume()
         fuelVol = fuel.getVolume()
         volFrac = fuelVol / blockVol / self.block.getSymmetryFactor()
-        neutronFlux = fuel.getIntegratedMgFlux()
-        gammaFlux = fuel.getIntegratedMgFlux(gamma=True)
-        np.testing.assert_almost_equal(neutronFlux, np.ones(5) * volFrac)
-        np.testing.assert_almost_equal(gammaFlux, np.ones(4) * volFrac * 2.0)
+        neutronFluxInt = fuel.getIntegratedMgFlux()
+        gammaFluxInt = fuel.getIntegratedMgFlux(gamma=True)
+        # getIntegratedMgFlux should be scaled by the component volume fraction
+        np.testing.assert_almost_equal(
+            neutronFluxInt, np.full(5, neutronFlux * volFrac)
+        )
+        np.testing.assert_almost_equal(gammaFluxInt, np.full(4, gammaFlux * volFrac))
+
+        # getMgFlux should return regular, non-integrated flux
+        neutronMgFlux = fuel.getMgFlux()
+        gammaMgFlux = fuel.getMgFlux(gamma=True)
+        np.testing.assert_almost_equal(
+            neutronMgFlux, np.full(5, neutronFlux / blockVol)
+        )
+        np.testing.assert_almost_equal(gammaMgFlux, np.full(4, gammaFlux / blockVol))
 
     @patch.object(blocks.HexBlock, "getSymmetryFactor")
     def test_completeInitialLoading(self, mock_sf):