From bf929ea5dbd124d24f130c98375f56401bbacd53 Mon Sep 17 00:00:00 2001 From: Michael Jarrett Date: Thu, 27 Aug 2026 16:13:13 +0000 Subject: [PATCH 1/9] Ignore negative-area components in representative block construction. --- armi/physics/neutronics/crossSectionGroupManager.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/armi/physics/neutronics/crossSectionGroupManager.py b/armi/physics/neutronics/crossSectionGroupManager.py index 2326da92e..fc2a2621d 100644 --- a/armi/physics/neutronics/crossSectionGroupManager.py +++ b/armi/physics/neutronics/crossSectionGroupManager.py @@ -451,13 +451,13 @@ def _getAverageComponentTemperature(self, compIndex): weights = np.array([self.getWeight(b) / b.getHeight() for b in blocks]) weights /= weights.sum() # normalize by total weight components = [sorted(b.getComponents())[compIndex] for b in blocks] - weightedAvgComponentMass = sum(w * c.getMass() for w, c in zip(weights, components)) + weightedAvgComponentMass = sum(w * c.getMass() for w, c in zip(weights, components) if c.getMass() > 0.0) if weightedAvgComponentMass == 0.0: # if there is no component mass (e.g., gap), do a regular average return np.mean(np.array([c.temperatureInC for c in components])) else: return ( - weights.dot(np.array([c.temperatureInC * c.getMass() for c in components])) / weightedAvgComponentMass + weights.dot(np.array([c.temperatureInC * c.getMass() for c in components if c.getMass() > 0.0])) / weightedAvgComponentMass ) def _performAverageByComponent(self): @@ -844,7 +844,10 @@ def _getAverageComponentNucs(self, components, bWeights): densities = np.zeros(len(allNucNames)) totalWeight = 0.0 for c, bWeight in zip(components, bWeights): - weight = bWeight * c.getArea() + compArea = c.getArea() + if compArea <= 0.0: + continue + weight = bWeight * compArea totalWeight += weight densities += weight * np.array(c.getNuclideNumberDensities(allNucNames)) if totalWeight > 0.0: From 8339bd35156438b1ba946565b97a4c5d8b318db1 Mon Sep 17 00:00:00 2001 From: Michael Jarrett Date: Thu, 27 Aug 2026 16:16:14 +0000 Subject: [PATCH 2/9] Ruff format. --- armi/physics/neutronics/crossSectionGroupManager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/armi/physics/neutronics/crossSectionGroupManager.py b/armi/physics/neutronics/crossSectionGroupManager.py index fc2a2621d..39e524e28 100644 --- a/armi/physics/neutronics/crossSectionGroupManager.py +++ b/armi/physics/neutronics/crossSectionGroupManager.py @@ -457,7 +457,8 @@ def _getAverageComponentTemperature(self, compIndex): return np.mean(np.array([c.temperatureInC for c in components])) else: return ( - weights.dot(np.array([c.temperatureInC * c.getMass() for c in components if c.getMass() > 0.0])) / weightedAvgComponentMass + weights.dot(np.array([c.temperatureInC * c.getMass() for c in components if c.getMass() > 0.0])) + / weightedAvgComponentMass ) def _performAverageByComponent(self): From 0a71e884e463161525d58a61c0e6b58ccd2f31f5 Mon Sep 17 00:00:00 2001 From: Michael Jarrett Date: Thu, 27 Aug 2026 17:52:16 +0000 Subject: [PATCH 3/9] Fix a bug. --- armi/physics/neutronics/crossSectionGroupManager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/armi/physics/neutronics/crossSectionGroupManager.py b/armi/physics/neutronics/crossSectionGroupManager.py index 39e524e28..9184793b6 100644 --- a/armi/physics/neutronics/crossSectionGroupManager.py +++ b/armi/physics/neutronics/crossSectionGroupManager.py @@ -451,13 +451,13 @@ def _getAverageComponentTemperature(self, compIndex): weights = np.array([self.getWeight(b) / b.getHeight() for b in blocks]) weights /= weights.sum() # normalize by total weight components = [sorted(b.getComponents())[compIndex] for b in blocks] - weightedAvgComponentMass = sum(w * c.getMass() for w, c in zip(weights, components) if c.getMass() > 0.0) + weightedAvgComponentMass = sum(w * c.getMass() if c.getMass() > 0.0 else 0.0 for w, c in zip(weights, components)) if weightedAvgComponentMass == 0.0: # if there is no component mass (e.g., gap), do a regular average return np.mean(np.array([c.temperatureInC for c in components])) else: return ( - weights.dot(np.array([c.temperatureInC * c.getMass() for c in components if c.getMass() > 0.0])) + weights.dot(np.array([c.temperatureInC * c.getMass() if c.getMass() > 0.0 else 0.0 for c in components])) / weightedAvgComponentMass ) From 3ca8838441eee82bd50963c039b48f1f6a392207 Mon Sep 17 00:00:00 2001 From: Michael Jarrett Date: Thu, 27 Aug 2026 19:44:06 +0000 Subject: [PATCH 4/9] Fix test. --- .../tests/test_crossSectionManager.py | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/armi/physics/neutronics/tests/test_crossSectionManager.py b/armi/physics/neutronics/tests/test_crossSectionManager.py index 0d0913ae5..2d61d49e4 100644 --- a/armi/physics/neutronics/tests/test_crossSectionManager.py +++ b/armi/physics/neutronics/tests/test_crossSectionManager.py @@ -27,6 +27,7 @@ from armi import settings from armi.context import PLATFORM, Platform +from armi.materials.water import Water from armi.physics.neutronics import crossSectionGroupManager from armi.physics.neutronics.const import CONF_CROSS_SECTION from armi.physics.neutronics.crossSectionGroupManager import ( @@ -401,7 +402,6 @@ def test_ComponentAverageRepBlock(self): ("Assemblies not in the core should still have XS groups, see _getMissingBlueprintBlocks()"), ) - class TestBlockCollCompAvg1DCyl(unittest.TestCase): """Test Block collection component averages for 1D cylinder.""" @@ -585,6 +585,52 @@ def test_ComponentAverageDuctHet1DCylinder(self): f"{nuc} temperature should be different from {compTemp} for component {c}", ) + def test_compAverageNegativeVolume(self): + """Test creation of a representative block when negative-volume components are present. + + .. test:: Create representative blocks using a volume-weighted averaging. + :id: T_ARMI_XSGM_CREATE_REPR_BLOCKS2 + :tests: R_ARMI_XSGM_CREATE_REPR_BLOCKS + """ + + xsgm = self.o.getInterface("xsGroups") + + xsgm.interactBOL() + + # Check that the correct defaults are propagated after the interactBOL + # from the cross section group manager is called. + xsOpt = self.o.cs[CONF_CROSS_SECTION]["UA"] + self.assertEqual(xsOpt.blockRepresentation, "ComponentAverage1DCylinder") + + annularFuel = self.r.core.getAssemblies(Flags.FUEL | Flags.ANNULAR, includeBolAssems=True)[0] + + # set temperatures of each gap component before performing average + # give the gaps some mass (material is arbitrary) + for i, b in enumerate(annularFuel.getBlocks(Flags.FUEL)): + gaps = b.getComponents(Flags.GAP) + for c in gaps: + c.temperatureInC = 500.0 + 100.0 * i + c.material = Water() + c.setNumberDensities({"H": 0.5, "O": 0.25}) + # make some of the negative-volume gaps have positive volume by adjusting liner temperature + liner = b.getComponent(Flags.LINER | Flags.OUTER) + liner.setTemperature(500.0 + 100.0 * i) + + xsgm.createRepresentativeBlocks() + xsgm.updateNuclideTemperatures() + + reprBlock = xsgm.representativeBlocks["UA"] + for c in reprBlock.getComponents(Flags.GAP): + if c.getName() == "gap3": + self.assertAlmostEqual(c.temperatureInC, 700.0) + elif c.getName() == "gap4": + self.assertLess(c.temperatureInC, 600.0) + self.assertGreater(c.temperatureInC, 500.0) + else: + self.assertAlmostEqual(c.temperatureInC, 600.0) + outerLiner = reprBlock.getComponent(Flags.LINER | Flags.OUTER) + self.assertAlmostEqual(outerLiner.temperatureInC, 600.0) + def test_checkComponentConsistency(self): xsgm = self.o.getInterface("xsGroups") xsgm.interactBOL() From c4594b50ba44d7f390f62affc722e86d704b24a3 Mon Sep 17 00:00:00 2001 From: Michael Jarrett Date: Thu, 27 Aug 2026 20:37:20 +0000 Subject: [PATCH 5/9] Ruff format. --- armi/physics/neutronics/crossSectionGroupManager.py | 8 ++++++-- armi/physics/neutronics/tests/test_crossSectionManager.py | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/armi/physics/neutronics/crossSectionGroupManager.py b/armi/physics/neutronics/crossSectionGroupManager.py index 9184793b6..66173f9bc 100644 --- a/armi/physics/neutronics/crossSectionGroupManager.py +++ b/armi/physics/neutronics/crossSectionGroupManager.py @@ -451,13 +451,17 @@ def _getAverageComponentTemperature(self, compIndex): weights = np.array([self.getWeight(b) / b.getHeight() for b in blocks]) weights /= weights.sum() # normalize by total weight components = [sorted(b.getComponents())[compIndex] for b in blocks] - weightedAvgComponentMass = sum(w * c.getMass() if c.getMass() > 0.0 else 0.0 for w, c in zip(weights, components)) + weightedAvgComponentMass = sum( + w * c.getMass() if c.getMass() > 0.0 else 0.0 for w, c in zip(weights, components) + ) if weightedAvgComponentMass == 0.0: # if there is no component mass (e.g., gap), do a regular average return np.mean(np.array([c.temperatureInC for c in components])) else: return ( - weights.dot(np.array([c.temperatureInC * c.getMass() if c.getMass() > 0.0 else 0.0 for c in components])) + weights.dot( + np.array([c.temperatureInC * c.getMass() if c.getMass() > 0.0 else 0.0 for c in components]) + ) / weightedAvgComponentMass ) diff --git a/armi/physics/neutronics/tests/test_crossSectionManager.py b/armi/physics/neutronics/tests/test_crossSectionManager.py index 2d61d49e4..91846720f 100644 --- a/armi/physics/neutronics/tests/test_crossSectionManager.py +++ b/armi/physics/neutronics/tests/test_crossSectionManager.py @@ -402,6 +402,7 @@ def test_ComponentAverageRepBlock(self): ("Assemblies not in the core should still have XS groups, see _getMissingBlueprintBlocks()"), ) + class TestBlockCollCompAvg1DCyl(unittest.TestCase): """Test Block collection component averages for 1D cylinder.""" @@ -592,7 +593,7 @@ def test_compAverageNegativeVolume(self): :id: T_ARMI_XSGM_CREATE_REPR_BLOCKS2 :tests: R_ARMI_XSGM_CREATE_REPR_BLOCKS """ - + xsgm = self.o.getInterface("xsGroups") xsgm.interactBOL() From a5cb900adf795114025ab3c0a98ef4017ec09153 Mon Sep 17 00:00:00 2001 From: Michael Jarrett Date: Thu, 27 Aug 2026 21:14:23 +0000 Subject: [PATCH 6/9] Ruff linting. --- armi/physics/neutronics/tests/test_crossSectionManager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/armi/physics/neutronics/tests/test_crossSectionManager.py b/armi/physics/neutronics/tests/test_crossSectionManager.py index 91846720f..815cf3a3a 100644 --- a/armi/physics/neutronics/tests/test_crossSectionManager.py +++ b/armi/physics/neutronics/tests/test_crossSectionManager.py @@ -593,7 +593,6 @@ def test_compAverageNegativeVolume(self): :id: T_ARMI_XSGM_CREATE_REPR_BLOCKS2 :tests: R_ARMI_XSGM_CREATE_REPR_BLOCKS """ - xsgm = self.o.getInterface("xsGroups") xsgm.interactBOL() From 866a5225229b577048d111dadd9f50f3763fd82e Mon Sep 17 00:00:00 2001 From: Michael Jarrett Date: Thu, 27 Aug 2026 23:45:38 +0000 Subject: [PATCH 7/9] Annotate the unit test some more. --- .../neutronics/tests/test_crossSectionManager.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/armi/physics/neutronics/tests/test_crossSectionManager.py b/armi/physics/neutronics/tests/test_crossSectionManager.py index 815cf3a3a..eeb5275af 100644 --- a/armi/physics/neutronics/tests/test_crossSectionManager.py +++ b/armi/physics/neutronics/tests/test_crossSectionManager.py @@ -594,16 +594,9 @@ def test_compAverageNegativeVolume(self): :tests: R_ARMI_XSGM_CREATE_REPR_BLOCKS """ xsgm = self.o.getInterface("xsGroups") - xsgm.interactBOL() - # Check that the correct defaults are propagated after the interactBOL - # from the cross section group manager is called. - xsOpt = self.o.cs[CONF_CROSS_SECTION]["UA"] - self.assertEqual(xsOpt.blockRepresentation, "ComponentAverage1DCylinder") - annularFuel = self.r.core.getAssemblies(Flags.FUEL | Flags.ANNULAR, includeBolAssems=True)[0] - # set temperatures of each gap component before performing average # give the gaps some mass (material is arbitrary) for i, b in enumerate(annularFuel.getBlocks(Flags.FUEL)): @@ -622,11 +615,14 @@ def test_compAverageNegativeVolume(self): reprBlock = xsgm.representativeBlocks["UA"] for c in reprBlock.getComponents(Flags.GAP): if c.getName() == "gap3": + # gap3 temp is 700 C because the 500 C and 600 C components have negative volume self.assertAlmostEqual(c.temperatureInC, 700.0) elif c.getName() == "gap4": + # gap4 temp is between 500 C and 600 C because the 700C component has negative volume self.assertLess(c.temperatureInC, 600.0) self.assertGreater(c.temperatureInC, 500.0) else: + # other gap components all have positive volume self.assertAlmostEqual(c.temperatureInC, 600.0) outerLiner = reprBlock.getComponent(Flags.LINER | Flags.OUTER) self.assertAlmostEqual(outerLiner.temperatureInC, 600.0) From 69f24a59c07c1270a7b60698f6c21ca2d05d4eef Mon Sep 17 00:00:00 2001 From: Michael Jarrett Date: Fri, 28 Aug 2026 21:47:00 +0000 Subject: [PATCH 8/9] Optimize performance. --- armi/physics/neutronics/crossSectionGroupManager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/armi/physics/neutronics/crossSectionGroupManager.py b/armi/physics/neutronics/crossSectionGroupManager.py index 66173f9bc..42e5d5b08 100644 --- a/armi/physics/neutronics/crossSectionGroupManager.py +++ b/armi/physics/neutronics/crossSectionGroupManager.py @@ -451,8 +451,9 @@ def _getAverageComponentTemperature(self, compIndex): weights = np.array([self.getWeight(b) / b.getHeight() for b in blocks]) weights /= weights.sum() # normalize by total weight components = [sorted(b.getComponents())[compIndex] for b in blocks] + masses = [c.getMass() for c in components] weightedAvgComponentMass = sum( - w * c.getMass() if c.getMass() > 0.0 else 0.0 for w, c in zip(weights, components) + w * mass if mass > 0.0 else 0.0 for w, mass in zip(weights, masses) ) if weightedAvgComponentMass == 0.0: # if there is no component mass (e.g., gap), do a regular average @@ -460,7 +461,7 @@ def _getAverageComponentTemperature(self, compIndex): else: return ( weights.dot( - np.array([c.temperatureInC * c.getMass() if c.getMass() > 0.0 else 0.0 for c in components]) + np.array([c.temperatureInC * mass if mass > 0.0 else 0.0 for c, mass in zip(components, masses)]) ) / weightedAvgComponentMass ) From 9c2f215fb2a33d5bba71de4131ed3244c563cdec Mon Sep 17 00:00:00 2001 From: Michael Jarrett Date: Fri, 28 Aug 2026 21:49:50 +0000 Subject: [PATCH 9/9] Slight refactor. --- armi/physics/neutronics/crossSectionGroupManager.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/armi/physics/neutronics/crossSectionGroupManager.py b/armi/physics/neutronics/crossSectionGroupManager.py index 42e5d5b08..ba27ca1ea 100644 --- a/armi/physics/neutronics/crossSectionGroupManager.py +++ b/armi/physics/neutronics/crossSectionGroupManager.py @@ -451,18 +451,14 @@ def _getAverageComponentTemperature(self, compIndex): weights = np.array([self.getWeight(b) / b.getHeight() for b in blocks]) weights /= weights.sum() # normalize by total weight components = [sorted(b.getComponents())[compIndex] for b in blocks] - masses = [c.getMass() for c in components] - weightedAvgComponentMass = sum( - w * mass if mass > 0.0 else 0.0 for w, mass in zip(weights, masses) - ) + weightedMass = [w * c.getMass() for w, c in zip(weights, components)] + weightedAvgComponentMass = sum(m for m in weightedMass if m > 0.0) if weightedAvgComponentMass == 0.0: # if there is no component mass (e.g., gap), do a regular average return np.mean(np.array([c.temperatureInC for c in components])) else: return ( - weights.dot( - np.array([c.temperatureInC * mass if mass > 0.0 else 0.0 for c, mass in zip(components, masses)]) - ) + sum([c.temperatureInC * mass if mass > 0.0 else 0.0 for c, mass in zip(components, weightedMass)]) / weightedAvgComponentMass )