Repository: terrapower/armi

Issue:
Is coveralls.io broken?
Coveralls.io has been (intermittently) broken for about the last week. And still is. This has impacted my ability to work in ARMI.

In the [past](https://github.com/terrapower/armi/issues/552) we have talked about switching from coveralls.io to codecov.io. Maybe ti is time for that.  

I was really hoping this would not be necessary, because I'm too busy to make this change.  But a full week of essentially complete outage is just too much.

**The Problem**

* You can click on our high-level [coverage report](https://coveralls.io/github/terrapower/armi?branch=main), and for most of the past week that link has been active.
* But if you click on any given file, we do not have line-by-line code coverage. This is the main feature I want from coveralls.io, so the system appears to just be fully broken:
  * [latticePhysicsInterface.py](https://coveralls.io/builds/69247730/source?filename=armi%2Fphysics%2Fneutronics%2FlatticePhysics%2FlatticePhysicsInterface.py)
  * [isotopicDepletionInterface.py](https://coveralls.io/builds/69247730/source?filename=armi%2Fphysics%2Fneutronics%2FisotopicDepletion%2FisotopicDepletionInterface.py)
  * [mpiActions.py](https://coveralls.io/builds/69247730/source?filename=armi%2FmpiActions.py)

Due diligence, coveralls.io does have a [troubleshooting page](https://docs.coveralls.io/common-issues-and-troubleshooting).

Discussion:

Patch:
diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml
index 006512de..3dd65147 100644
--- a/.github/workflows/coverage.yaml
+++ b/.github/workflows/coverage.yaml
@@ -35,7 +35,7 @@ jobs:
           mpiexec -n 2 --use-hwthread-cpus coverage run --rcfile=pyproject.toml -m pytest --cov=armi --cov-config=pyproject.toml --cov-report=lcov --cov-append --ignore=venv armi/tests/test_mpiParameters.py || true
           coverage combine --rcfile=pyproject.toml --keep -a
       - name: Publish to coveralls.io
-        uses: coverallsapp/github-action@v1.1.2
+        uses: coverallsapp/github-action@v2
         with:
           github-token: ${{ secrets.GITHUB_TOKEN }}
-          path-to-lcov: coverage.lcov
+          file: coverage.lcov
diff --git a/README.rst b/README.rst
index 8f6bb1fd..636ba0ae 100644
--- a/README.rst
+++ b/README.rst
@@ -60,8 +60,7 @@ found in [#touranarmi]_.
 
 Quick start
 -----------
-Before starting, you need to have `Python <https://www.python.org/downloads/>`_ 3.9+ on
-Windows or Linux.
+Before starting, you need to have `Python <https://www.python.org/downloads/>`_ 3.9+.
 
 Get the ARMI code, install the prerequisites, and fire up the launcher with the following
 commands. You probably want to do this in a virtual environment as described in the `Installation
@@ -70,7 +69,7 @@ dependencies could conflict with your system dependencies.
 
 First, upgrade your version of pip::
 
-    $ pip install pip>=22.1
+    $ pip install -U pip>=22.1
 
 Now clone and install ARMI::
 
diff --git a/armi/bookkeeping/db/database3.py b/armi/bookkeeping/db/database3.py
index f1f35d57..871ff9eb 100644
--- a/armi/bookkeeping/db/database3.py
+++ b/armi/bookkeeping/db/database3.py
@@ -72,9 +72,8 @@ from armi.reactor.assemblies import Assembly
 from armi.reactor.blocks import Block
 from armi.reactor.components import Component
 from armi.reactor.composites import ArmiObject
-from armi.reactor.flags import Flags
 from armi.reactor.parameters import parameterCollections
-from armi.reactor.reactors import Core
+from armi.reactor.reactors import Core, Reactor
 from armi.settings.fwSettings.globalSettings import CONF_SORT_REACTOR
 from armi.utils import getNodesPerCycle
 from armi.utils.textProcessors import resolveMarkupInclusions
@@ -854,11 +853,7 @@ class Database3:
             comp.add(child)
 
         if isinstance(comp, Core):
-            # TODO: This is also an issue related to geoms and which core is "The Core".
-            # We only have a good geom for the main core, so can't do process loading on
-            # the SFP, etc.
-            if comp.hasFlags(Flags.CORE):
-                comp.processLoading(cs, dbLoad=True)
+            comp.processLoading(cs, dbLoad=True)
         elif isinstance(comp, Assembly):
             comp.calculateZCoords()
 
@@ -1311,8 +1306,7 @@ class Database3:
         Returns
         -------
         dict
-            Dictionary ArmiObject (input): dict of str/list pairs containing ((cycle,
-            node), value).
+            Dictionary ArmiObject (input): dict of str/list pairs containing ((cycle, node), value).
         """
         histData: Histories = {
             c: collections.defaultdict(collections.OrderedDict) for c in comps
@@ -1329,8 +1323,7 @@ class Database3:
             if "layout" not in h5TimeNodeGroup:
                 # Layout hasn't been written for this time step, so whatever is in there
                 # didn't come from the DatabaseInterface. Probably because it's the
-                # current time step and something has created the group to store aux
-                # data
+                # current time step and something has created the group to store aux data
                 continue
 
             cycle = h5TimeNodeGroup.attrs["cycle"]
@@ -1419,7 +1412,7 @@ class Database3:
 
                         histData[c][paramName][cycle, timeNode] = val
 
-        r = comps[0].getAncestorWithFlags(Flags.REACTOR)
+        r = comps[0].getAncestor(lambda c: isinstance(c, Reactor))
         cycleNode = r.p.cycle, r.p.timeNode
         for c, paramHistories in histData.items():
             for paramName, hist in paramHistories.items():
diff --git a/armi/reactor/assemblyLists.py b/armi/reactor/assemblyLists.py
index 0abb974f..1355ad81 100644
--- a/armi/reactor/assemblyLists.py
+++ b/armi/reactor/assemblyLists.py
@@ -201,7 +201,6 @@ class SpentFuelPool(AssemblyList):
         ----------
         assem : Assembly
             The Assembly to add to the list
-
         loc : LocationBase, optional
             If provided, the assembly is inserted at that location. If it is not
             provided, the locator on the Assembly object will be used. If the
diff --git a/armi/reactor/components/__init__.py b/armi/reactor/components/__init__.py
index 3fcbd768..92eef7b8 100644
--- a/armi/reactor/components/__init__.py
+++ b/armi/reactor/components/__init__.py
@@ -158,7 +158,7 @@ class UnshapedComponent(Component):
         Parameters
         ----------
         cold : bool, optional
-            Ignored for this component
+            If True, compute the area with as-input dimensions, instead of thermally-expanded.
         """
         coldArea = self.p.area
         if cold:
@@ -173,6 +173,13 @@ class UnshapedComponent(Component):
         This is the smallest it can possibly be. Since this is used to determine
         the outer component, it will never be allowed to be the outer one.
 
+        Parameters
+        ----------
+        Tc : float
+            Ignored for this component
+        cold : bool, optional
+            If True, compute the area with as-input dimensions, instead of thermally-expanded.
+
         Notes
         -----
         Tc is not used in this method for this particular component.
@@ -451,8 +458,17 @@ class DerivedShape(UnshapedComponent):
         Parameters
         ----------
         cold : bool, optional
-            Ignored for this component
+            If True, compute the area with as-input dimensions, instead of thermally-expanded.
         """
+        if cold:
+            # At cold temp, the DerivedShape has the area of the parent minus the other siblings
+            parentArea = self.parent.getArea()
+            # NOTE: the assumption is there is only one DerivedShape in each Component
+            siblings = sum(
+                [c.getArea(cold=True) for c in self.parent if type(c) != DerivedShape]
+            )
+            return parentArea - siblings
+
         if self.parent.derivedMustUpdate:
             self.computeVolume()
 
diff --git a/armi/reactor/flags.py b/armi/reactor/flags.py
index 6366432a..14bcff7e 100644
--- a/armi/reactor/flags.py
+++ b/armi/reactor/flags.py
@@ -205,9 +205,6 @@ class Flags(Flag):
     MEDIUM = auto()
     LOW = auto()
 
-    CORE = auto()
-    REACTOR = auto()
-
     # general kinds of assemblies or blocks
     MATERIAL = auto()
     FUEL = auto()
diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py
index 3553fb9b..0b1def1a 100644
--- a/armi/reactor/reactors.py
+++ b/armi/reactor/reactors.py
@@ -98,7 +98,6 @@ class Reactor(composites.Composite):
         self.spatialLocator = None
         self.p.maxAssemNum = 0
         self.p.cycle = 0
-        self.p.flags |= Flags.REACTOR
         self.core = None
         self.sfp = None
         self.blueprints = blueprints
@@ -123,7 +122,7 @@ class Reactor(composites.Composite):
 
     def add(self, container):
         composites.Composite.add(self, container)
-        cores = self.getChildrenWithFlags(Flags.CORE)
+        cores = [c for c in self.getChildren(deep=True) if isinstance(c, Core)]
         if cores:
             if len(cores) != 1:
                 raise ValueError(
@@ -251,10 +250,9 @@ class Core(composites.Composite):
         :implements: R_ARMI_R_CORE
 
         A :py:class:`Core <armi.reactor.reactors.Core>` object is typically a child of a
-        :py:class:`Reactor <armi.reactor.reactors.Reactor>` object. A Reactor can contain multiple
-        objects of the Core type. The instance attribute name ``r.core`` is reserved for the object
-        representating the active core. A reactor may also have a spent fuel pool instance
-        attribute, ``r.sfp``\ , which is also of type :py:class:`core <armi.reactor.reactors.Core>`.
+        :py:class:`Reactor <armi.reactor.reactors.Reactor>` object. A Reactor should only contain
+        one object of the Core type. The instance attribute name ``r.core`` is reserved for the
+        object representating the active core.
 
         Most of the operations to retrieve information from the ARMI reactor data model are mediated
         through Core objects. For example,
@@ -266,7 +264,6 @@ class Core(composites.Composite):
     params : dict
         Core-level parameters are scalar values that have time dependence. Examples are keff,
         maxPercentBu, etc.
-
     assemblies : list
         List of assembly objects that are currently in the core
     """
@@ -283,7 +280,6 @@ class Core(composites.Composite):
             Name of the object. Flags will inherit from this.
         """
         composites.Composite.__init__(self, name)
-        self.p.flags = Flags.fromStringIgnoreErrors(name)
         self.assembliesByName = {}
         self.circularRingList = {}
         self.blocksByName = {}  # lookup tables
diff --git a/doc/release/0.4.rst b/doc/release/0.4.rst
index 7ede4100..69169c07 100644
--- a/doc/release/0.4.rst
+++ b/doc/release/0.4.rst
@@ -14,16 +14,19 @@ New Features
 
 API Changes
 -----------
+#. Removing flags ``CORE`` and ``REACTOR``. (`PR#1835 <https://github.com/terrapower/armi/pull/1835>`_)
 #. TBD
 
 Bug Fixes
 ---------
+#. Fixed ``DerivedShape.getArea`` for ``cold=True``. (`PR#1831 <https://github.com/terrapower/armi/pull/1831>`_)
 #. Fixed error parsing command line integers in ``ReportsEntryPoint``. (`PR#1824 <https://github.com/terrapower/armi/pull/1824>`_)
 #. TBD
 
 Quality Work
 ------------
 #. Removing code that causes a deprecation warning (converting ``axialUnitGrid`` to ``AxialGrid.fromNCells``) (`PR#1809 <https://github.com/terrapower/armi/pull/1809>`_)
+#. TBD
 
 Changes that Affect Requirements
 --------------------------------


Test patch:
diff --git a/armi/physics/neutronics/tests/test_crossSectionTable.py b/armi/physics/neutronics/tests/test_crossSectionTable.py
index b69593a0..8dfc878b 100644
--- a/armi/physics/neutronics/tests/test_crossSectionTable.py
+++ b/armi/physics/neutronics/tests/test_crossSectionTable.py
@@ -20,7 +20,6 @@ from armi.physics.neutronics.isotopicDepletion import (
     isotopicDepletionInterface as idi,
 )
 from armi.physics.neutronics.latticePhysics import ORDER
-from armi.reactor.flags import Flags
 from armi.reactor.tests.test_blocks import loadTestBlock
 from armi.reactor.tests.test_reactors import loadTestReactor
 from armi.settings import Settings
@@ -37,7 +36,7 @@ class TestCrossSectionTable(unittest.TestCase):
         """
         obj = loadTestBlock()
         obj.p.mgFlux = range(33)
-        core = obj.getAncestorWithFlags(Flags.CORE)
+        core = obj.parent.parent
         core.lib = isotxs.readBinary(ISOAA_PATH)
         table = crossSectionTable.makeReactionRateTable(obj)
 
diff --git a/armi/reactor/tests/test_assemblies.py b/armi/reactor/tests/test_assemblies.py
index 248aa743..9bde09ff 100644
--- a/armi/reactor/tests/test_assemblies.py
+++ b/armi/reactor/tests/test_assemblies.py
@@ -605,8 +605,8 @@ class Assembly_TestCase(unittest.TestCase):
 
         for refBlock, curBlock in zip(self.assembly, assembly2):
             numNucs = 0
-            for nuc in self.assembly.getAncestorWithFlags(
-                Flags.REACTOR
+            for nuc in self.assembly.getAncestor(
+                lambda c: isinstance(c, reactors.Reactor)
             ).blueprints.allNuclidesInProblem:
                 numNucs += 1
                 # Block level density
diff --git a/armi/reactor/tests/test_components.py b/armi/reactor/tests/test_components.py
index 4b57f422..fc6569bb 100644
--- a/armi/reactor/tests/test_components.py
+++ b/armi/reactor/tests/test_components.py
@@ -17,6 +17,7 @@ import copy
 import math
 import unittest
 
+from armi.materials import air, alloy200
 from armi.materials.material import Material
 from armi.reactor import components
 from armi.reactor import flags
@@ -44,7 +45,7 @@ from armi.reactor.components import (
     ComponentType,
 )
 from armi.reactor.components import materials
-from armi.materials import air, alloy200
+from armi.reactor.tests.test_reactors import loadTestReactor
 
 
 class TestComponentFactory(unittest.TestCase):
@@ -482,6 +483,31 @@ class TestDerivedShape(TestShapedComponent):
         self.assertAlmostEqual(c.computeVolume(), 1386.5232044586771)
 
 
+class TestDerivedShapeGetArea(unittest.TestCase):
+    def test_getAreaColdTrue(self):
+        """Prove that the DerivedShape.getArea() works at cold=True."""
+        # load one-block test reactor
+        _o, r = loadTestReactor(
+            inputFileName="smallestTestReactor/armiRunSmallest.yaml"
+        )
+        b = r.core[0][0]
+
+        # ensure there is a DerivedShape in this Block
+        shapes = set([type(c) for c in b])
+        self.assertIn(Circle, shapes)
+        self.assertIn(DerivedShape, shapes)
+        self.assertIn(Helix, shapes)
+        self.assertIn(Hexagon, shapes)
+
+        # prove that getArea works on the block level
+        self.assertAlmostEqual(b.getArea(cold=True), b.getArea(cold=False), delta=1e-10)
+
+        # prove that getArea preserves the sum of all the areas, even if there is a DerivedShape
+        totalAreaCold = sum([c.getArea(cold=True) for c in b])
+        totalAreaHot = sum([c.getArea(cold=False) for c in b])
+        self.assertAlmostEqual(totalAreaCold, totalAreaHot, delta=1e-10)
+
+
 class TestCircle(TestShapedComponent):
     """Test circle shaped component."""