Repository: terrapower/armi
Pull request #1298 (2023-06-09 12:02:22)

undo setting ndense to 0 for xs gen; move feature to xs gen
## Description

Undo #1257 since it impacts memory and database. Add features to lattice physics

---

## Checklist

<!--
    You (the pull requester) should put an `x` in the boxes below you have completed.
    If you're unsure about any of them, don't hesitate to ask. We're here to help!
    Learn what a "good PR" looks like here: https://terrapower.github.io/armi/developer/tooling.html#good-pull-requests
-->

- [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 important changes.
- [x] The documentation is still up-to-date in the `doc` folder.
- [x] The dependencies are still up-to-date in `setup.py`.



Patch:
diff --git a/armi/nuclearDataIO/xsNuclides.py b/armi/nuclearDataIO/xsNuclides.py
index facc56c6..be79103e 100644
--- a/armi/nuclearDataIO/xsNuclides.py
+++ b/armi/nuclearDataIO/xsNuclides.py
@@ -243,6 +243,8 @@ def plotScatterMatrix(scatterMatrix, scatterTypeLabel="", fName=None):
     else:
         pyplot.show()
 
+    pyplot.close()
+
 
 def plotCompareScatterMatrix(scatterMatrix1, scatterMatrix2, fName=None):
     """Compares scatter matrices graphically between libraries."""
@@ -260,3 +262,5 @@ def plotCompareScatterMatrix(scatterMatrix1, scatterMatrix2, fName=None):
         pyplot.savefig(fName)
     else:
         pyplot.show()
+
+    pyplot.close()
diff --git a/armi/physics/neutronics/fissionProductModel/fissionProductModel.py b/armi/physics/neutronics/fissionProductModel/fissionProductModel.py
index 840126f4..ca57fe50 100644
--- a/armi/physics/neutronics/fissionProductModel/fissionProductModel.py
+++ b/armi/physics/neutronics/fissionProductModel/fissionProductModel.py
@@ -104,7 +104,7 @@ from armi.physics.neutronics.fissionProductModel.fissionProductModelSettings imp
     CONF_FP_MODEL,
     CONF_MAKE_ALL_BLOCK_LFPS_INDEPENDENT,
 )
-from armi.settings.fwSettings.globalSettings import CONF_DETAILED_AXIAL_EXPANSION
+
 
 NUM_FISSION_PRODUCTS_PER_LFP = 2.0
 
@@ -125,10 +125,6 @@ class FissionProductModel(interfaces.Interface):
     def __init__(self, r, cs):
         interfaces.Interface.__init__(self, r, cs)
         self._globalLFPs = lumpedFissionProduct.lumpedFissionProductFactory(self.cs)
-        # If detailed axial expansion is active, mapping between blocks occurs on uniform mesh
-        # and this can cause blocks to have isotopes that they don't have cross sections for.
-        # Fix this by adding all isotopes to all blocks so they are present it lattice physics.
-        self.allBlocksNeedAllNucs = self.cs[CONF_DETAILED_AXIAL_EXPANSION]
 
     @property
     def _explicitFissionProducts(self):
@@ -156,10 +152,7 @@ class FissionProductModel(interfaces.Interface):
         products be consistent across all blocks, even if fission products are
         not generated when the block is depleted.
         """
-        if self.getInterface("mcnp") or self.allBlocksNeedAllNucs:
-            return None
-        else:
-            return Flags.FUEL
+        return None if self.getInterface("mcnp") is not None else Flags.FUEL
 
     def interactBOL(self):
         interfaces.Interface.interactBOL(self)
@@ -168,31 +161,15 @@ class FissionProductModel(interfaces.Interface):
         else:
             self.setAllBlockLFPs()
 
-    def _getComponentToInitDensities(self, b):
-        """Get the component to initialize ndens keys on."""
-        # add the isotopics to the smallest solid since that is usually the most "interesting"
-        # sorted() calls getBoundingCircleOuterDiameter under the hood
-        solidsOrderedBySize = sorted(c for c in b if c.containsSolidMaterial())
-        if solidsOrderedBySize:
-            return solidsOrderedBySize[0]
-        else:
-            # no solids, so just add to smallest component
-            return sorted(c for c in b)[0]
-
     def setAllComponentFissionProducts(self):
         """
-        Initialize all nuclides for each ``DEPLETABLE`` component in the core, or all blocks if detailedAxialExpansion is enabled.
+        Initialize all nuclides for each ``DEPLETABLE`` component in the core.
 
         Notes
         -----
         This should be called when explicit fission product modeling is enabled to
         ensure that all isotopes are initialized on the depletable components within
-        the reactor data model so that cross sections are created during depletion.
-
-        When detailedAxialExpansion is also enabled, all regions will have fission/activation
-        products added to avoid missing cross sections during mesh conversion (since converted
-        blocks only have one xsID but may have isotopes from multiple blocks with different IDs.)
-        Setting density to zero here enables small number densities is XS generation.
+        the reactor data model so that there is some density as a starting point.
 
         When explicit fission products are enabled and the user has not already included
         all fission products in the blueprints (in ``nuclideFlags``), the ``fpModelLibrary`` setting is used
@@ -210,28 +187,15 @@ class FissionProductModel(interfaces.Interface):
         """
         for b in self.r.core.getBlocks(includeAll=True):
             b.setLumpedFissionProducts(None)
-            self._initializeIsoDensities(b)
-
-    def _initializeIsoDensities(self, b):
-        """
-        Initialize isotope densities to include all on depletable components.
-
-        Notes
-        -----
-        In non-uniform reactor cases all blocks will get all isotopes so that the
-        global flux solve does not have any missing cross sections.
-        """
-        compsToAddIso = b.getComponents(Flags.DEPLETABLE)
-        if self.allBlocksNeedAllNucs and not compsToAddIso:
-            compsToAddIso = [self._getComponentToInitDensities(b)]
-        for c in compsToAddIso:
-            # Add all isotopes in problem at 0.0 density
-            updatedNDens = c.getNumberDensities()
-            for nuc in self.r.blueprints.allNuclidesInProblem:
-                if nuc in updatedNDens:
-                    continue
-                updatedNDens[nuc] = 0.0
-            c.updateNumberDensities(updatedNDens)
+            for c in b.getComponents(Flags.DEPLETABLE):
+                # Add all isotopes in problem at 0.0 density
+                updatedNDens = c.getNumberDensities()
+                # self.r.blueprints.allNuclidesInProblem contains ~everything in ENDF if _explicitFissionProducts
+                for nuc in self.r.blueprints.allNuclidesInProblem:
+                    if nuc in updatedNDens:
+                        continue
+                    updatedNDens[nuc] = 0.0
+                c.updateNumberDensities(updatedNDens)
 
     def setAllBlockLFPs(self):
         """
@@ -247,8 +211,6 @@ class FissionProductModel(interfaces.Interface):
             else:
                 independentLFPs = self.getGlobalLumpedFissionProducts().duplicate()
                 b.setLumpedFissionProducts(independentLFPs)
-            if self.allBlocksNeedAllNucs:
-                self._initializeIsoDensities(b)
 
     def getGlobalLumpedFissionProducts(self):
         r"""
diff --git a/armi/physics/neutronics/latticePhysics/latticePhysicsWriter.py b/armi/physics/neutronics/latticePhysics/latticePhysicsWriter.py
index bb2cd23d..2504322e 100644
--- a/armi/physics/neutronics/latticePhysics/latticePhysicsWriter.py
+++ b/armi/physics/neutronics/latticePhysics/latticePhysicsWriter.py
@@ -25,6 +25,7 @@ import math
 import collections
 
 import numpy
+import ordered_set
 
 from armi import runLog
 from armi import interfaces
@@ -42,6 +43,7 @@ from armi.physics.neutronics.settings import (
     CONF_MINIMUM_NUCLIDE_DENSITY,
 )
 from armi.physics.neutronics.settings import CONF_GEN_XS
+from armi.settings.fwSettings.globalSettings import CONF_DETAILED_AXIAL_EXPANSION
 
 
 # number of decimal places to round temperatures to in _groupNuclidesByTemperature
@@ -51,7 +53,7 @@ _NUCLIDE_VALUES_TEMPERATURE_INDEX = 1
 
 
 @warn_when_root
-def NuclideNameFoundMultipleTimes(nuclideName):
+def nuclideNameFoundMultipleTimes(nuclideName):
     return "Nuclide `{}' was found multiple times.".format(nuclideName)
 
 
@@ -241,11 +243,19 @@ class LatticePhysicsWriter(interfaces.InputWriter):
         # on the components will already contain all the nuclides required to be
         # modeled by the lattice physics writer. Otherwise, assume that `allNuclidesInProblem`
         # should be modeled.
-        nuclides = (
-            sorted(objNuclides)
-            if self.explicitFissionProducts
-            else self.r.blueprints.allNuclidesInProblem
-        )
+        if self.explicitFissionProducts:
+            # If detailed axial expansion is active, mapping between blocks occurs on uniform mesh
+            # and this can cause blocks to have isotopes that they don't have cross sections for.
+            # Fix this by adding all isotopes so they are present in lattice physics.
+            if self.cs[CONF_DETAILED_AXIAL_EXPANSION]:
+                nuclides = self.r.blueprints.allNuclidesInProblem
+            else:
+                nuclides = ordered_set.OrderedSet(sorted(objNuclides))
+        else:
+            nuclides = self.r.blueprints.allNuclidesInProblem
+
+        nuclides = nuclides.union(self.r.blueprints.nucsToForceInXsGen)
+
         numDensities = subjectObject.getNuclideNumberDensities(nuclides)
 
         for nucName, dens in zip(nuclides, numDensities):
@@ -262,7 +272,7 @@ class LatticePhysicsWriter(interfaces.InputWriter):
 
             density = max(dens, self.minimumNuclideDensity)
             if nuc in nucDensities:
-                NuclideNameFoundMultipleTimes(nucName)
+                nuclideNameFoundMultipleTimes(nucName)
                 dens, nucTemperatureInC, nucCategory = nucDensities[nuc]
                 density = dens + density
                 nucDensities[nuc] = (density, nucTemperatureInC, nucCategory)
diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py
index 3ed609c5..8ba84d49 100644
--- a/armi/reactor/blocks.py
+++ b/armi/reactor/blocks.py
@@ -27,19 +27,20 @@ import math
 
 import numpy
 
+from armi import nuclideBases
 from armi import runLog
 from armi.bookkeeping import report
-from armi import nuclideBases
 from armi.physics.neutronics import GAMMA
 from armi.physics.neutronics import NEUTRON
-from armi.reactor.components import basicShapes
 from armi.reactor import blockParameters
 from armi.reactor import components
-from armi.reactor.components.basicShapes import Hexagon, Circle
 from armi.reactor import composites
 from armi.reactor import geometry
 from armi.reactor import grids
 from armi.reactor import parameters
+from armi.reactor.components import basicShapes
+from armi.reactor.components.basicShapes import Hexagon, Circle
+from armi.reactor.components.complexShapes import Helix
 from armi.reactor.flags import Flags
 from armi.reactor.parameters import ParamLocation
 from armi.utils import densityTools
@@ -800,8 +801,13 @@ class Block(composites.Composite):
             bolBlock = self
 
         hmDens = bolBlock.getHMDens()  # total homogenized heavy metal number density
-        self.p.molesHmBOL = self.getHMMoles()
         self.p.nHMAtBOL = hmDens
+
+        self.p.molesHmBOL = self.getHMMoles()
+        self.p.puFrac = (
+            self.getPuMoles() / self.p.molesHmBOL if self.p.molesHmBOL > 0.0 else 0.0
+        )
+
         try:
             # non-pinned reactors (or ones without cladding) will not use smear density
             self.p.smearDensity = self.getSmearDensity()
@@ -810,14 +816,12 @@ class Block(composites.Composite):
         self.p.enrichmentBOL = self.getFissileMassEnrich()
         massHmBOL = 0.0
         sf = self.getSymmetryFactor()
-        for child in self.iterComponents():
-            child.p.massHmBOL = child.getHMMass() * sf  # scale to full block
-            massHmBOL += child.p.massHmBOL
-            self.p.puFrac = (
-                self.getPuMoles() / self.p.molesHmBOL
-                if self.p.molesHmBOL > 0.0
-                else 0.0
-            )
+        for child in self:
+            hmMass = child.getHMMass() * sf
+            massHmBOL += hmMass
+            # Components have a massHmBOL parameter but not every composite will
+            if isinstance(child, components.Component):
+                child.p.massHmBOL = hmMass
         self.p.massHmBOL = massHmBOL
         return hmDens
 
@@ -1249,7 +1253,7 @@ class Block(composites.Composite):
         """
         maxDim = -float("inf")
         largestComponent = None
-        for c in self.iterComponents():
+        for c in self:
             try:
                 dimVal = c.getDimension(dimension)
             except parameters.ParameterError:
@@ -2211,11 +2215,19 @@ class HexBlock(Block):
                 6 * c.getDimension("ip") / math.sqrt(3) if c else 0.0
             )
 
-        # solid circle = od * pi
-        # NOTE: since these are pin components, multiply by the number of pins
+        # solid circle = NumPins * pi * (Comp Diam + Wire Diam)
         wettedPinPerimeter = 0.0
         for c in wettedPinComponents:
-            wettedPinPerimeter += c.getDimension("od") if c else 0.0
+            correctionFactor = 1.0
+            if isinstance(c, Helix):
+                # account for the helical wire wrap
+                correctionFactor = numpy.hypot(
+                    1.0,
+                    math.pi
+                    * c.getDimension("helixDiameter")
+                    / c.getDimension("axialPitch"),
+                )
+            wettedPinPerimeter += c.getDimension("od") * correctionFactor
         wettedPinPerimeter *= self.getNumPins() * math.pi
 
         # hollow circle = (id + od) * pi
diff --git a/armi/reactor/blueprints/__init__.py b/armi/reactor/blueprints/__init__.py
index 09aad757..2b104634 100644
--- a/armi/reactor/blueprints/__init__.py
+++ b/armi/reactor/blueprints/__init__.py
@@ -215,11 +215,12 @@ class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector):
         self.assemblies = {}
         self._prepped = False
         self._assembliesBySpecifier = {}
-        self.allNuclidesInProblem = (
-            ordered_set.OrderedSet()
-        )  # Better for performance since these are used for lookups
+
+        # Better for performance since these are used for lookups
+        self.allNuclidesInProblem = ordered_set.OrderedSet()
         self.activeNuclides = ordered_set.OrderedSet()
         self.inertNuclides = ordered_set.OrderedSet()
+        self.nucsToForceInXsGen = ordered_set.OrderedSet()
         self.elementsToExpand = []
         return self
 
@@ -393,46 +394,47 @@ class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector):
 
         actives = set()
         inerts = set()
-        undefBurnChainActiveNuclides = set()
-        if self.nuclideFlags is None:
-            self.nuclideFlags = isotopicOptions.genDefaultNucFlags()
 
-        isotopicOptions.autoUpdateNuclideFlags(cs, self.nuclideFlags)
+        nuclideFlags = self.nuclideFlags or isotopicOptions.genDefaultNucFlags()
 
+        nucsToForceInXsGen = set()
+        # just expanding flags now. ndense gets expanded in comp blueprints
         self.elementsToExpand = []
-        for nucFlag in self.nuclideFlags:
+        for nucFlag in nuclideFlags:
             # this returns any nuclides that are flagged specifically for expansion by input
-            expandedElements = nucFlag.fileAsActiveOrInert(
-                actives, inerts, undefBurnChainActiveNuclides
+            (
+                expandedElements,
+                undefBurnChainActiveNuclides,
+            ) = nucFlag.fileAsActiveOrInert(
+                actives,
+                inerts,
             )
             self.elementsToExpand.extend(expandedElements)
 
         inerts -= actives
         self.customIsotopics = self.customIsotopics or isotopicOptions.CustomIsotopics()
-        (
-            elementalsToKeep,
-            expansions,
-        ) = isotopicOptions.autoSelectElementsToKeepFromSettings(cs)
+        eleKeep, eleExpand = isotopicOptions.eleExpandInfoBasedOnCodeENDF(cs)
 
         # Flag all elementals for expansion unless they've been flagged otherwise by
         # user input or automatic lattice/datalib rules.
-        for elemental in nuclideBases.instances:
-            if not isinstance(elemental, nuclideBases.NaturalNuclideBase):
+        for nucBase in nuclideBases.instances:
+            isAlreadyIsotopic = not isinstance(nucBase, nuclideBases.NaturalNuclideBase)
+            if isAlreadyIsotopic:
                 # `elemental` may be a NaturalNuclideBase or a NuclideBase
-                # skip all NuclideBases
+                # skip all NuclideBases (isotopics)
                 continue
 
-            if elemental in elementalsToKeep:
+            # we now know its an elemental
+            elemental = nucBase
+            if elemental in eleKeep:
                 continue
 
             if elemental.name in actives:
                 currentSet = actives
-                actives.remove(elemental.name)
             elif elemental.name in inerts:
                 currentSet = inerts
-                inerts.remove(elemental.name)
             else:
-                # This was not specified in the nuclide flags at all.
+                # This was not specified in the nuclide flags at all as burn or xs.
                 # If a material with this in its composition is brought in
                 # it's nice from a user perspective to allow it.
                 # But current behavior is that all nuclides in problem
@@ -442,20 +444,17 @@ class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector):
             self.elementsToExpand.append(elemental.element)
 
             if (
-                elemental.name in self.nuclideFlags
-                and self.nuclideFlags[elemental.name].expandTo
+                elemental.name in nuclideFlags
+                and nuclideFlags[elemental.element.symbol].expandTo
             ):
-                # user-input has precedence
+                # user-input expandTo has precedence
                 newNuclides = [
                     nuclideBases.byName[nn]
-                    for nn in self.nuclideFlags[elemental.element.symbol].expandTo
+                    for nn in nuclideFlags[elemental.element.symbol].expandTo
                 ]
-            elif (
-                elemental in expansions
-                and elemental.element.symbol in self.nuclideFlags
-            ):
-                # code-specific expansion required
-                newNuclides = expansions[elemental]
+            elif elemental in eleExpand and elemental.element.symbol in nuclideFlags:
+                # code-specific expansion required based on code and ENDF
+                newNuclides = eleExpand[elemental]
                 # overlay code details onto nuclideFlags for other parts of the code
                 # that will use them.
                 # TODO: would be better if nuclideFlags did this upon reading s.t.
@@ -465,16 +464,25 @@ class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector):
                 # This must be updated because the operative expansion code just uses the flags
                 #
                 # Also, if this element is not in nuclideFlags at all, we just don't add it
-                self.nuclideFlags[elemental.element.symbol].expandTo = [
+                nuclideFlags[elemental.element.symbol].expandTo = [
                     nb.name for nb in newNuclides
                 ]
             else:
                 # expand to all possible natural isotopics
                 newNuclides = elemental.element.getNaturalIsotopics()
 
+            # remove the elemental and add the isotopic
+            currentSet.remove(elemental.name)
             for nb in newNuclides:
                 currentSet.add(nb.name)
 
+        # force everything asked for in xsGen
+        nucsToForceInXsGen = ordered_set.OrderedSet(sorted(actives.union(inerts)))
+
+        # add all detailed isotopes in ENDF if requested
+        isotopicOptions.autoUpdateNuclideFlags(cs, nuclideFlags, inerts)
+        self.nuclideFlags = nuclideFlags
+
         if self.elementsToExpand:
             runLog.info(
                 "Will expand {} elementals to have natural isotopics".format(
@@ -487,6 +495,7 @@ class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector):
         self.allNuclidesInProblem = ordered_set.OrderedSet(
             sorted(actives.union(inerts))
         )
+        self.nucsToForceInXsGen = ordered_set.OrderedSet(sorted(nucsToForceInXsGen))
 
         # Inform user which nuclides are truncating the burn chain.
         if undefBurnChainActiveNuclides and nuclideBases.burnChainImposed:
diff --git a/armi/reactor/blueprints/isotopicOptions.py b/armi/reactor/blueprints/isotopicOptions.py
index f9163b1c..ac38e924 100644
--- a/armi/reactor/blueprints/isotopicOptions.py
+++ b/armi/reactor/blueprints/isotopicOptions.py
@@ -107,9 +107,7 @@ class NuclideFlag(yamlize.Object):
             self.nuclideName, self.burn, self.xs
         )
 
-    def fileAsActiveOrInert(
-        self, activeSet, inertSet, undefinedBurnChainActiveNuclides
-    ):
+    def fileAsActiveOrInert(self, activeSet, inertSet):
         """
         Given a nuclide or element name, file it as either active or inert.
 
@@ -117,6 +115,7 @@ class NuclideFlag(yamlize.Object):
         rather than the NaturalNuclideBase, as the NaturalNuclideBase will never
         occur in such a problem.
         """
+        undefBurnChainActiveNuclides = set()
         nb = nuclideBases.byName[self.nuclideName]
         if self.expandTo:
             nucBases = [nuclideBases.byName[nn] for nn in self.expandTo]
@@ -129,11 +128,11 @@ class NuclideFlag(yamlize.Object):
             if self.burn:
                 if not nuc.trans and not nuc.decays:
                     # DUMPs and LFPs usually
-                    undefinedBurnChainActiveNuclides.add(nuc.name)
+                    undefBurnChainActiveNuclides.add(nuc.name)
                 activeSet.add(nuc.name)
             if self.xs:
                 inertSet.add(nuc.name)
-        return expanded
+        return expanded, undefBurnChainActiveNuclides
 
 
 class NuclideFlags(yamlize.KeyedList):
@@ -454,9 +453,9 @@ def getDefaultNuclideFlags():
     return nuclideFlags
 
 
-def autoSelectElementsToKeepFromSettings(cs):
+def eleExpandInfoBasedOnCodeENDF(cs):
     """
-    Intelligently choose elements to expand based on settings.
+    Intelligently choose elements to expand based on code and ENDF version.
 
     If settings point to a particular code and library and we know
     that combo requires certain elementals to be expanded, we
@@ -566,7 +565,7 @@ def genDefaultNucFlags():
     return flags
 
 
-def autoUpdateNuclideFlags(cs, nuclideFlags):
+def autoUpdateNuclideFlags(cs, nuclideFlags, inerts):
     """
     This function is responsible for examining the fission product model treatment
     that is selected by the user and adding a set of nuclides to the `nuclideFlags`
@@ -592,8 +591,9 @@ def autoUpdateNuclideFlags(cs, nuclideFlags):
             nuc = nb.name
             if nuc in nuclideFlags or elements.byZ[nb.z] in nuclideFlags:
                 continue
-            nuclideFlag = NuclideFlag(nuc, burn=False, xs=True, expandTo=[])
-            nuclideFlags[nuc] = nuclideFlag
+            nuclideFlags[nuc] = NuclideFlag(nuc, burn=False, xs=True, expandTo=[])
+            # inert since burn is False
+            inerts.add(nuc)
 
 
 def getAllNuclideBasesByLibrary(cs):
diff --git a/armi/reactor/components/component.py b/armi/reactor/components/component.py
index 02b431d7..90cbf110 100644
--- a/armi/reactor/components/component.py
+++ b/armi/reactor/components/component.py
@@ -28,6 +28,7 @@ from armi import runLog
 from armi.bookkeeping import report
 from armi.reactor import composites
 from armi.reactor import parameters
+from armi.reactor import flags
 from armi.reactor.components import componentParameters
 from armi.utils import densityTools
 from armi.utils.units import C_TO_K
@@ -1187,6 +1188,10 @@ class Component(composites.Composite, metaclass=ComponentType):
             "Please implement if this component type can be a pitch defining component."
         )
 
+    def getFuelMass(self) -> float:
+        """Return the mass in grams if this is a fueled component"""
+        return self.getMass() if self.hasFlags(flags.Flags.FUEL) else 0.0
+
 
 class ShapedComponent(Component):
     """A component with well-defined dimensions."""
diff --git a/armi/reactor/composites.py b/armi/reactor/composites.py
index e21aeeaf..cd4c844a 100644
--- a/armi/reactor/composites.py
+++ b/armi/reactor/composites.py
@@ -31,6 +31,7 @@ See Also: :doc:`/developer/index`.
 """
 import collections
 import itertools
+import operator
 import timeit
 from typing import Dict, Optional, Type, Tuple, List, Union
 
@@ -882,9 +883,7 @@ class ArmiObject(metaclass=CompositeModelType):
         mass : float
             The mass in grams.
         """
-        return sum(
-            [c.getMass(nuclideNames=nuclideNames) for c in self.iterComponents()]
-        )
+        return sum([c.getMass(nuclideNames=nuclideNames) for c in self])
 
     def getMassFrac(self, nucName):
         """
@@ -1235,7 +1234,7 @@ class ArmiObject(metaclass=CompositeModelType):
         volumes = numpy.array(
             [
                 c.getVolume() / (c.parent.getSymmetryFactor() if c.parent else 1.0)
-                for c in self.iterComponents()
+                for c in self
             ]
         )  # c x 1
         totalVol = volumes.sum()
@@ -1244,7 +1243,7 @@ class ArmiObject(metaclass=CompositeModelType):
             return [0.0] * len(nucNames)
 
         densListForEachComp = []
-        for c in self.iterComponents():
+        for c in self:
             numberDensityDict = c.getNumberDensities()
             densListForEachComp.append(
                 [numberDensityDict.get(nuc, 0.0) for nuc in nucNames]
@@ -2123,8 +2122,8 @@ class ArmiObject(metaclass=CompositeModelType):
         return mass
 
     def getFuelMass(self):
-        """Returns mass of fuel in grams."""
-        return sum([fuel.getMass() for fuel in self.iterComponents(Flags.FUEL)], 0.0)
+        """returns mass of fuel in grams."""
+        return sum((c.getFuelMass() for c in self))
 
     def constituentReport(self):
         """A print out of some pertinent constituent information."""
@@ -3213,12 +3212,8 @@ class Composite(ArmiObject):
 
         Used to roughly approximate relative size vs. other objects
         """
-        return sum(
-            [
-                comp.getBoundingCircleOuterDiameter(Tc, cold)
-                for comp in self.iterComponents()
-            ]
-        )
+        getter = operator.methodcaller("getBoundingCircleOuterDiameter", Tc, cold)
+        return sum(map(getter, self))
 
 
 class StateRetainer:
diff --git a/armi/utils/codeTiming.py b/armi/utils/codeTiming.py
index d852a3bf..c849b03f 100644
--- a/armi/utils/codeTiming.py
+++ b/armi/utils/codeTiming.py
@@ -306,6 +306,7 @@ class MasterTimer:
         # done
         filename = base_file_name + ".code-timeline.png"
         plt.savefig(filename)
+        plt.close()
         return os.path.join(os.getcwd(), filename)
 
 
diff --git a/armi/utils/plotting.py b/armi/utils/plotting.py
index 8a718f4d..b4ae11da 100644
--- a/armi/utils/plotting.py
+++ b/armi/utils/plotting.py
@@ -1089,6 +1089,7 @@ def plotBlockFlux(core, fName=None, bList=None, peak=False, adjoint=False, bList
 
     if fName:
         plt.savefig(fName)
+        plt.close()
         report.setData(
             "Flux Plot {}".format(os.path.split(fName)[1]),
             os.path.abspath(fName),
@@ -1439,6 +1440,7 @@ def plotBlockDiagram(block, fName, cold, cmapName="RdYlBu", materialList=None):
     ax.spines["bottom"].set_visible(False)
     ax.margins(0)
     plt.savefig(fName, format="svg", **pltKwargs)
+    plt.close()
 
     return os.path.abspath(fName)
 
@@ -1585,3 +1587,5 @@ def plotNucXs(
         plt.savefig(fName)
     elif not noShow:
         plt.show()
+
+    plt.close()
diff --git a/armi/utils/reportPlotting.py b/armi/utils/reportPlotting.py
index 9ae6438e..edb4a81e 100644
--- a/armi/utils/reportPlotting.py
+++ b/armi/utils/reportPlotting.py
@@ -441,6 +441,7 @@ def plotCoreOverviewRadar(reactors, reactorNames=None):
         size="large",
     )
     plt.savefig("reactor_comparison.png")
+    plt.close()
 
 
 def _getNeutronicVals(r):
diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst
index ed0d928d..cd36edfc 100644
--- a/doc/release/0.2.rst
+++ b/doc/release/0.2.rst
@@ -10,11 +10,13 @@ What's new in ARMI
 ------------------
 #. Calculate weighted-average percent burnup of BlockCollections. (`PR#1265 <https://github.com/terrapower/armi/pull/1265>`_)
 #. Added ``Composite.sort()`` to allow the user to recursively sort any piece of the ``Reactor``. (`PR#1280 <https://github.com/terrapower/armi/pull/1280>`_)
+#  Revert "when using non-uniform mesh, detailed fission/activation products have cross sections generated to avoid blocks without xs data" and move capability to lattice physics (`PR#1280 <https://github.com/terrapower/armi/pull/1298>`_)
 #. TBD
 
 Bug fixes
 ---------
 #. Changed units.FLOAT_DIMENSION_DECIMALS from 10 to 8 (`PR#1183 <https://github.com/terrapower/armi/pull/1183>`_)
+#. Improved ``HexBlock.getWettedPerimeter()`` to include wire. (`PR#1299 <https://github.com/terrapower/armi/pull/1299>`_)
 #. 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>`_)
 #. Sort components on representativeBlock for consistency check. (`PR#1275 <https://github.com/terrapower/armi/pull/1275>`_)


Test patch:
diff --git a/armi/nuclearDataIO/tests/test_xsCollections.py b/armi/nuclearDataIO/tests/test_xsCollections.py
index 7dffb245..0ac7af37 100644
--- a/armi/nuclearDataIO/tests/test_xsCollections.py
+++ b/armi/nuclearDataIO/tests/test_xsCollections.py
@@ -14,6 +14,7 @@
 
 """Module that tests methods within xsCollections."""
 # pylint: disable=missing-function-docstring,missing-class-docstring,protected-access,invalid-name,no-self-use,no-method-argument,import-outside-toplevel
+import os
 import unittest
 
 from armi import settings
@@ -21,6 +22,8 @@ from armi.reactor.blocks import HexBlock
 from armi.nuclearDataIO import isotxs
 from armi.nuclearDataIO import xsCollections
 from armi.tests import ISOAA_PATH
+from armi.utils.directoryChangers import TemporaryDirectoryChanger
+from armi.utils.plotting import plotNucXs
 
 
 class TestXsCollections(unittest.TestCase):
@@ -65,6 +68,16 @@ class TestXsCollections(unittest.TestCase):
             (nuc.micros.elasticScatter[0, 0] + 2.0 * nuc.micros.n2nScatter[0, 0]),
         )
 
+    def test_plotNucXs(self):
+        """
+        Testing this plotting method here because we need a XS library
+        to run the test.
+        """
+        fName = "test_plotNucXs.png"
+        with TemporaryDirectoryChanger():
+            plotNucXs(self.microLib, "U235AA", "fission", fName=fName)
+            self.assertTrue(os.path.exists(fName))
+
     def test_createMacrosFromMicros(self):
         self.assertEqual(self.mc.minimumNuclideDensity, 1e-13)
         self.mc.createMacrosFromMicros(self.microLib, self.block)
diff --git a/armi/nuclearDataIO/tests/test_xsLibraries.py b/armi/nuclearDataIO/tests/test_xsLibraries.py
index 61facffc..9c8c46c4 100644
--- a/armi/nuclearDataIO/tests/test_xsLibraries.py
+++ b/armi/nuclearDataIO/tests/test_xsLibraries.py
@@ -287,7 +287,7 @@ class Test_GetISOTXSFilesInWorkingDirectory(unittest.TestCase):
         self.assertEqual(set(), container & set(shouldNotBeThere))
 
 
-# LOOK OUT, THIS GETS DELETED LATER ON SO IT DOESN'T RUN... IT IS AN ABSTRACT CLASS!!
+# NOTE: This is just a base class, so it isn't run directly.
 class TestXSlibraryMerging(unittest.TestCase, TempFileMixin):
     """A shared class that defines tests that should be true for all IsotxsLibrary merging."""
 
diff --git a/armi/physics/neutronics/fissionProductModel/tests/test_fissionProductModel.py b/armi/physics/neutronics/fissionProductModel/tests/test_fissionProductModel.py
index 98a03a47..180c6646 100644
--- a/armi/physics/neutronics/fissionProductModel/tests/test_fissionProductModel.py
+++ b/armi/physics/neutronics/fissionProductModel/tests/test_fissionProductModel.py
@@ -93,15 +93,6 @@ class TestFissionProductModelLumpedFissionProducts(unittest.TestCase):
         else:
             self.assertTrue(False, "All blocks have all nuclides!")
 
-        # now check if detailed axial expansion that all blocks have ALL nuclides
-        fpModel.allBlocksNeedAllNucs = True
-        fpModel.interactBOL()
-        for b in r.core.getBlocks():
-            nuclideList = b.getNuclides()
-            for nuc in r.blueprints.allNuclidesInProblem:
-                self.assertIn(nuc, nuclideList)
-            self.assertTrue(b._lumpedFissionProducts is not None)
-
 
 class TestFissionProductModelExplicitMC2Library(unittest.TestCase):
     """
@@ -170,15 +161,6 @@ class TestFissionProductModelExplicitMC2Library(unittest.TestCase):
             else:
                 self.assertLess(len(b.getNuclides()), len(nuclideBases.byMcc3Id))
 
-        # now check if detailed axial expansion that all blocks have detailed nuclides
-        self.fpModel.allBlocksNeedAllNucs = True
-
-        self.fpModel.interactBOL()
-        for b in self.r.core.getBlocks():
-            nuclideList = b.getNuclides()
-            for nb in nuclideBases.byMcc3Id.values():
-                self.assertIn(nb.name, nuclideList)
-
 
 if __name__ == "__main__":
     # import sys;sys.argv = ['', 'Test.testName']
diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py
index 1b079f8a..667d7f8c 100644
--- a/armi/reactor/tests/test_blocks.py
+++ b/armi/reactor/tests/test_blocks.py
@@ -747,7 +747,7 @@ class Block_TestCase(unittest.TestCase):
         self.assertAlmostEqual(cur, ref, places=places)
 
     def test_replaceBlockWithBlock(self):
-        r"""Tests conservation of mass flag in replaceBlockWithBlock."""
+        """Tests conservation of mass flag in replaceBlockWithBlock."""
         block = self.block
         ductBlock = block.__class__("duct")
         ductBlock.add(block.getComponent(Flags.COOLANT, exact=True))
@@ -771,13 +771,24 @@ class Block_TestCase(unittest.TestCase):
 
     def test_getWettedPerimeter(self):
         cur = self.block.getWettedPerimeter()
+
+        wire = self.block.getComponent(Flags.WIRE)
+        correctionFactor = numpy.hypot(
+            1.0,
+            math.pi
+            * wire.getDimension("helixDiameter")
+            / wire.getDimension("axialPitch"),
+        )
+        wireDiameter = wire.getDimension("od") * correctionFactor
+
         ref = math.pi * (
-            self.block.getDim(Flags.CLAD, "od") + self.block.getDim(Flags.WIRE, "od")
+            self.block.getDim(Flags.CLAD, "od") + wireDiameter
         ) * self.block.getDim(Flags.CLAD, "mult") + 6 * self.block.getDim(
             Flags.DUCT, "ip"
         ) / math.sqrt(
             3
         )
+
         self.assertAlmostEqual(cur, ref)
 
     def test_getFlowAreaPerPin(self):
diff --git a/armi/reactor/tests/test_components.py b/armi/reactor/tests/test_components.py
index c9155e3c..3af22b1c 100644
--- a/armi/reactor/tests/test_components.py
+++ b/armi/reactor/tests/test_components.py
@@ -22,6 +22,7 @@ import unittest
 
 from armi.materials.material import Material
 from armi.reactor import components
+from armi.reactor import flags
 from armi.reactor.components import (
     Component,
     UnshapedComponent,
@@ -516,6 +517,13 @@ class TestCircle(TestShapedComponent):
         self.component.changeNDensByFactor(3.0)
         self.assertEqual(self.component.getNumberDensity("NA23"), 3.0)
 
+    def test_fuelMass(self):
+        nominalMass = self.component.getMass()
+        self.component.p.flags = flags.Flags.FUEL
+        self.assertEqual(self.component.getFuelMass(), nominalMass)
+        self.component.p.flags = flags.Flags.MODERATOR
+        self.assertEqual(self.component.getFuelMass(), 0.0)
+
 
 class TestComponentExpansion(unittest.TestCase):
     tCold = 20