Repository: terrapower/armi
Pull request #1275 (2023-05-23 22:17:59)

Sort components on representative block.
## Description
---

There was a recent update to the representative block type for 1D cylindrical lattice physics models (#1238). This included a check for component consistency that can fail if the components are not in the same order between the real block and the representative block. The block components should be sorted before comparison.

## Checklist

- [x] This PR has only one purpose or idea.
- [x] Tests have been added/updated to verify that the new/changed code works.

<!-- 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/release/index.html) (location `doc/release/0.X.rst`) are up-to-date with any bug fixes or new features.
- [x] The documentation is still up-to-date in the `doc` folder.
- [x] The dependencies are still up-to-date in `setup.py`.



mgjarrett: This PR is ready for review

Patch:
diff --git a/armi/physics/neutronics/crossSectionGroupManager.py b/armi/physics/neutronics/crossSectionGroupManager.py
index 11a89464..8af2d686 100644
--- a/armi/physics/neutronics/crossSectionGroupManager.py
+++ b/armi/physics/neutronics/crossSectionGroupManager.py
@@ -443,7 +443,7 @@ class CylindricalComponentsAverageBlockCollection(BlockCollection):
         repBlock.p.percentBu = self._calcWeightedBurnup()
         componentsInOrder = self._orderComponentsInGroup(repBlock)
 
-        for c, allSimilarComponents in zip(repBlock, componentsInOrder):
+        for c, allSimilarComponents in zip(sorted(repBlock), componentsInOrder):
             allNucsNames, densities = self._getAverageComponentNucs(
                 allSimilarComponents, bWeights
             )
@@ -476,7 +476,7 @@ class CylindricalComponentsAverageBlockCollection(BlockCollection):
                 f"Blocks {b} and {repBlock} have differing number "
                 f"of components and cannot be homogenized"
             )
-        for c, repC in zip(b, repBlock):
+        for c, repC in zip(sorted(b), sorted(repBlock)):
             compString = (
                 f"Component {repC} in block {repBlock} and component {c} in block {b}"
             )
@@ -510,7 +510,7 @@ class CylindricalComponentsAverageBlockCollection(BlockCollection):
         """Order the components based on dimension and material type within the representative block."""
         for b in self.getCandidateBlocks():
             self._checkComponentConsistency(b, repBlock)
-        componentLists = [list(b) for b in self.getCandidateBlocks()]
+        componentLists = [list(sorted(b)) for b in self.getCandidateBlocks()]
         return [list(comps) for comps in zip(*componentLists)]
 
 
@@ -562,7 +562,7 @@ class SlabComponentsAverageBlockCollection(BlockCollection):
         return sorted(list(nucs))
 
     @staticmethod
-    def _checkComponentConsisteny(b, repBlock, components=None):
+    def _checkComponentConsistency(b, repBlock, components=None):
         """
         Verify that all components being homogenized are rectangular and have consistent dimensions.
 
@@ -665,7 +665,7 @@ class SlabComponentsAverageBlockCollection(BlockCollection):
                     )
                 )
             try:
-                self._checkComponentConsisteny(b, repBlock)
+                self._checkComponentConsistency(b, repBlock)
                 componentsToAdd = [c for c in b]
             except ValueError:
                 runLog.extra(
@@ -673,7 +673,7 @@ class SlabComponentsAverageBlockCollection(BlockCollection):
                     "representative block {}".format(b, repBlock)
                 )
                 reversedComponentOrder = self._reverseComponentOrder(b)
-                self._checkComponentConsisteny(
+                self._checkComponentConsistency(
                     b, repBlock, components=reversedComponentOrder
                 )
                 componentsToAdd = [c for c in reversedComponentOrder]
diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst
index df37766f..ed0d928d 100644
--- a/doc/release/0.2.rst
+++ b/doc/release/0.2.rst
@@ -17,8 +17,7 @@ Bug fixes
 #. Changed units.FLOAT_DIMENSION_DECIMALS from 10 to 8 (`PR#1183 <https://github.com/terrapower/armi/pull/1183>`_)
 #. Fixed a bug in the ISOTXS file name used for snapshots. (`PR#1277 <https://github.com/terrapower/armi/pull/1277>`_)
 #. Fix a bug in uniform mesh decusping when assemblies of same type have drastically different height. (`PR#1282 <https://github.com/terrapower/armi/pull/1282>`_)
-#. TBD
-
+#. Sort components on representativeBlock for consistency check. (`PR#1275 <https://github.com/terrapower/armi/pull/1275>`_)
 
 ARMI v0.2.7
 ===========


Test patch:
diff --git a/armi/nuclearDataIO/tests/test_xsCollections.py b/armi/nuclearDataIO/tests/test_xsCollections.py
index 3d6e21bc..7dffb245 100644
--- a/armi/nuclearDataIO/tests/test_xsCollections.py
+++ b/armi/nuclearDataIO/tests/test_xsCollections.py
@@ -16,9 +16,10 @@
 # pylint: disable=missing-function-docstring,missing-class-docstring,protected-access,invalid-name,no-self-use,no-method-argument,import-outside-toplevel
 import unittest
 
+from armi import settings
+from armi.reactor.blocks import HexBlock
 from armi.nuclearDataIO import isotxs
 from armi.nuclearDataIO import xsCollections
-from armi.physics.neutronics.tests import test_crossSectionManager
 from armi.tests import ISOAA_PATH
 
 
@@ -31,7 +32,7 @@ class TestXsCollections(unittest.TestCase):
         self.mc = xsCollections.MacroscopicCrossSectionCreator(
             minimumNuclideDensity=1e-13
         )
-        self.block = test_crossSectionManager.MockBlock()
+        self.block = MockBlock()
         self.block.setNumberDensity("U235", 0.02)
         self.block.setNumberDensity("FE", 0.01)
 
@@ -98,6 +99,48 @@ class TestXsCollections(unittest.TestCase):
         )
 
 
+class MockReactor:
+    def __init__(self):
+        self.blueprints = MockBlueprints()
+        self.spatialGrid = None
+
+
+class MockBlueprints:
+    # this is only needed for allNuclidesInProblem and attributes were acting funky, so this was made.
+    def __getattribute__(self, *args, **kwargs):
+        return ["U235", "U235", "FE", "NA23"]
+
+
+class MockBlock(HexBlock):
+    def __init__(self, name=None, cs=None):
+        self.density = {}
+        HexBlock.__init__(self, name or "MockBlock", cs or settings.Settings())
+        self.r = MockReactor()
+
+    @property
+    def r(self):
+        return self._r
+
+    @r.setter
+    def r(self, r):
+        self._r = r
+
+    def getVolume(self, *args, **kwargs):
+        return 1.0
+
+    def getNuclideNumberDensities(self, nucNames):
+        return [self.density.get(nucName, 0.0) for nucName in nucNames]
+
+    def _getNdensHelper(self):
+        return {nucName: density for nucName, density in self.density.items()}
+
+    def setNumberDensity(self, key, val, *args, **kwargs):
+        self.density[key] = val
+
+    def getNuclides(self):
+        return self.density.keys()
+
+
 if __name__ == "__main__":
     # import sys;sys.argv = ['', 'TestXsCollections.test_generateTotalScatteringMatrix']
     unittest.main()
diff --git a/armi/physics/neutronics/tests/test_crossSectionManager.py b/armi/physics/neutronics/tests/test_crossSectionManager.py
index c36d0a89..0d7a5254 100644
--- a/armi/physics/neutronics/tests/test_crossSectionManager.py
+++ b/armi/physics/neutronics/tests/test_crossSectionManager.py
@@ -324,6 +324,109 @@ class TestBlockCollectionComponentAverage1DCylinder(unittest.TestCase):
                     c.getNumberDensity(nuc), compDensity.get(nuc, 0.0)
                 )
 
+    def test_checkComponentConsistency(self):
+
+        xsgm = self.o.getInterface("xsGroups")
+        xsgm.interactBOL()
+        blockCollectionsByXsGroup = xsgm.makeCrossSectionGroups()
+
+        blockCollection = blockCollectionsByXsGroup["ZA"]
+        baseComponents = self.r.core.getFirstBlock(Flags.CONTROL).getComponents()
+        densities = {
+            "control": baseComponents[0].getNumberDensities(),
+            "clad": baseComponents[2].getNumberDensities(),
+            "coolant": baseComponents[4].getNumberDensities(),
+        }
+        controlComponent, cladComponent, coolantComponent = self._makeComponents(
+            7, densities
+        )
+
+        # reference block
+        refBlock = HexBlock("refBlock")
+        refBlock.add(controlComponent)
+        refBlock.add(cladComponent)
+        refBlock.add(coolantComponent)
+
+        # matching block
+        matchingBlock = HexBlock("matchBlock")
+        matchingBlock.add(controlComponent)
+        matchingBlock.add(cladComponent)
+        matchingBlock.add(coolantComponent)
+
+        # unsorted block
+        unsortedBlock = HexBlock("unsortedBlock")
+        unsortedBlock.add(cladComponent)
+        unsortedBlock.add(coolantComponent)
+        unsortedBlock.add(controlComponent)
+
+        # non-matching block length
+        nonMatchingLengthBlock = HexBlock("blockLengthDiff")
+        nonMatchingLengthBlock.add(controlComponent)
+        nonMatchingLengthBlock.add(coolantComponent)
+
+        # non-matching component multiplicity
+        nonMatchingMultBlock = HexBlock("blockComponentDiff")
+        control, clad, coolant = self._makeComponents(19, densities)
+        nonMatchingMultBlock.add(control)
+        nonMatchingMultBlock.add(clad)
+        nonMatchingMultBlock.add(coolant)
+
+        # different nuclides
+        nucDiffBlock = HexBlock("blockNucDiff")
+        mixedDensities = {
+            "clad": baseComponents[0].getNumberDensities(),
+            "coolant": baseComponents[2].getNumberDensities(),
+            "control": baseComponents[4].getNumberDensities(),
+        }
+        control, clad, coolant = self._makeComponents(7, mixedDensities)
+        nucDiffBlock.add(control)
+        nucDiffBlock.add(clad)
+        nucDiffBlock.add(coolant)
+
+        blockCollection._checkComponentConsistency(refBlock, matchingBlock)
+        blockCollection._checkComponentConsistency(refBlock, unsortedBlock)
+        for b in (nonMatchingMultBlock, nonMatchingLengthBlock, nucDiffBlock):
+            with self.assertRaises(ValueError):
+                blockCollection._checkComponentConsistency(refBlock, b)
+
+    def _makeComponents(self, multiplicity, densities):
+        from armi.reactor import components
+
+        baseComponents = self.r.core.getFirstBlock(Flags.CONTROL).getComponents()
+        controlComponent = components.Circle(
+            "control",
+            baseComponents[0].material,
+            20.0,
+            20.0,
+            id=0.0,
+            od=0.6,
+            mult=multiplicity,
+        )
+        cladComponent = components.Circle(
+            "clad",
+            baseComponents[2].material,
+            20.0,
+            20.0,
+            id=0.6,
+            od=0.7,
+            mult=multiplicity,
+        )
+        coolantComponent = components.Circle(
+            "coolant",
+            baseComponents[4].material,
+            20.0,
+            20.0,
+            id=0.7,
+            od=0.8,
+            mult=multiplicity,
+        )
+
+        controlComponent.setNumberDensities(densities["control"])
+        cladComponent.setNumberDensities(densities["clad"])
+        coolantComponent.setNumberDensities(densities["coolant"])
+
+        return controlComponent, cladComponent, coolantComponent
+
 
 class TestBlockCollectionFluxWeightedAverage(unittest.TestCase):
     def setUp(self):
@@ -580,48 +683,6 @@ class TestXSNumberConverters(unittest.TestCase):
         self.assertEqual(num, 9090)
 
 
-class MockReactor:
-    def __init__(self):
-        self.blueprints = MockBlueprints()
-        self.spatialGrid = None
-
-
-class MockBlueprints:
-    # this is only needed for allNuclidesInProblem and attributes were acting funky, so this was made.
-    def __getattribute__(self, *args, **kwargs):
-        return ["U235", "U235", "FE", "NA23"]
-
-
-class MockBlock(HexBlock):
-    def __init__(self, name=None, cs=None):
-        self.density = {}
-        HexBlock.__init__(self, name or "MockBlock", cs or settings.Settings())
-        self.r = MockReactor()
-
-    @property
-    def r(self):
-        return self._r
-
-    @r.setter
-    def r(self, r):
-        self._r = r
-
-    def getVolume(self, *args, **kwargs):
-        return 1.0
-
-    def getNuclideNumberDensities(self, nucNames):
-        return [self.density.get(nucName, 0.0) for nucName in nucNames]
-
-    def _getNdensHelper(self):
-        return {nucName: density for nucName, density in self.density.items()}
-
-    def setNumberDensity(self, key, val, *args, **kwargs):
-        self.density[key] = val
-
-    def getNuclides(self):
-        return self.density.keys()
-
-
 def makeBlocks(howMany=20):
     _o, r = test_reactors.loadTestReactor(TEST_ROOT)
     return r.core.getBlocks(Flags.FUEL)[