Repository: terrapower/armi

Issue:
GlobalFluxInterface has a silent, undocumented, default tight coupling
I'm working on an internal tight coupling workflow that should have converged, but didn't. After some digging, I found the `GlobalFluxInterface` sets an undocumented default tight coupler, even if you don't request to converge on keff

https://github.com/terrapower/armi/blob/24e36fe447914b1824f7bca01fdf0a648602b357/armi/physics/neutronics/globalFlux/globalFluxInterface.py#L65-L77

I understand why a default tight coupler for global flux makes sense. In most cases, this is probably what you want to do. But the behavior isn't documented, at least not in the [tight coupling docs](https://terrapower.github.io/armi/user/physics_coupling.html#tight-coupling), which led to my confusion. And it's not included in any logging so the log file wasn't _super_ helpful.

The thing that tipped me off was the addition of the `keff` convergence table in addition to the field I cared about. So there was _something_ about converging on keff, but I had no idea where it came from

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/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/doc/release/0.4.rst b/doc/release/0.4.rst
index 7773c5e6..69169c07 100644
--- a/doc/release/0.4.rst
+++ b/doc/release/0.4.rst
@@ -19,6 +19,7 @@ API Changes
 
 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
 
diff --git a/doc/user/physics_coupling.rst b/doc/user/physics_coupling.rst
index 15872a38..b9d29db9 100644
--- a/doc/user/physics_coupling.rst
+++ b/doc/user/physics_coupling.rst
@@ -76,3 +76,12 @@ The total convergence of the power distribution is finally measured through the
 
 .. math::
     \epsilon = \| \xi \|_{\inf} = \max \xi.
+
+
+The Global Flux Interface
+-------------------------
+The :py:class:`Global Flux Interface <armi.physics.neutronics.globalFlux.globalFluxInterface.GlobalFluxInterface>`
+class will attempt to set its own ``TightCoupler`` based on ``keff``. To see the specifics, see:
+:py:meth:`_setTightCouplingDefaults <armi.physics.neutronics.globalFlux.globalFluxInterface.GlobalFluxInterface._setTightCouplingDefaults>`.
+If you want to change the tight coupling performance of the ``GlobalFluxInterface``, it would be
+easiest to just sublass the interface and over-write the `_setTightCouplingDefaults` method.


Test patch:
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."""
 
diff --git a/armi/tests/zpprTest.yaml b/armi/tests/zpprTest.yaml
index 2e54d554..4a597f2c 100644
--- a/armi/tests/zpprTest.yaml
+++ b/armi/tests/zpprTest.yaml
@@ -29,7 +29,6 @@ settings:
       meshSubdivisionsPerCm: 10
   cycleLength: 365.25
   economics: false
-  epsCyclic: 1.0
   epsEig: 1e-10
   genXS: Neutron
   geomFile: zpprTestGeom.xml