Repository: terrapower/armi

Issue:
B4C defaults to a 0.9 TD fraction
This isn't really a _Problem_, but it is unexpected, inconsistent, and potentially troublesome. 

The B4C class has a built-in non-unity theoretical density fraction that is applied by default when the material is constructed:
https://github.com/terrapower/armi/blob/37f547940501d5e55978cac2fdba37e10fbae0ff/armi/materials/b4c.py#L117-L145

This non-unity fraction is used whenever the material density is queried:
https://github.com/terrapower/armi/blob/37f547940501d5e55978cac2fdba37e10fbae0ff/armi/materials/b4c.py#L160-L178

There is _also_ the ability to specify the theoretical density fraction using the `TD_frac` material modification, which is a good thing:
https://github.com/terrapower/armi/blob/37f547940501d5e55978cac2fdba37e10fbae0ff/armi/materials/b4c.py#L31-L52

The bad part about that is that all other material classes return a fully-dense material by default. If you want a version of the material with lower density, generally you should use the associated material modification `TD_frac`. But for B4C, you actually need to use `TD_frac` to get a full-density material. So this means that the B4C class is inconsistent with our other materials, but it is not documented as such.

I did want to change this as part of #1114 , but since it would be an API-breaking change and it proved a bit more challenging than expected, I decided to just report it here for future work instead. B4C, being as ubiquitous as it is, is used allllllll over our repos, and this could be tricky to implement a change.

Discussion:
john-science: Sorry, @keckler .  All your `TD_frac` PRs in ARMI (and elsewhere) need to have some conflicts resolved due to another Materials PR going into ARMI (and other downstream repos) this morning.

In most cases, it will be quite easy.  We removed all `Parameter`s from ARMI `Material`s. But we just moved all the ones that were still used to class attributes, so you can probably fix 99% of your errors by going from `mat.p.thing` to `mat.thing`.

Sorry for the hassle!

keckler: Done!

Patch:
diff --git a/armi/materials/b4c.py b/armi/materials/b4c.py
index 585dd1f9..1a5ec47d 100644
--- a/armi/materials/b4c.py
+++ b/armi/materials/b4c.py
@@ -12,7 +12,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""Boron carbide; a very typical reactor control material."""
+"""
+Boron carbide; a very typical reactor control material.
+
+Note that this material defaults to a theoretical density fraction of 0.9, reflecting
+the difficulty of producing B4C at 100% theoretical density in real life. To get
+different fraction, use the `TD_frac` material modification in your assembly definition.
+"""
 from armi import runLog
 from armi.materials import material
 from armi.nucDirectory import nuclideBases
diff --git a/armi/physics/neutronics/crossSectionGroupManager.py b/armi/physics/neutronics/crossSectionGroupManager.py
index cccf6b81..6d5ea9fd 100644
--- a/armi/physics/neutronics/crossSectionGroupManager.py
+++ b/armi/physics/neutronics/crossSectionGroupManager.py
@@ -610,8 +610,26 @@ class CrossSectionGroupManager(interfaces.Interface):
         self.clearRepresentativeBlocks()
 
     def interactCoupled(self, iteration):
-        """Update XS groups on each physics coupling iteration to get latest temperatures."""
-        self.interactBOC(cycle=None)
+        """Update XS groups on each physics coupling iteration to get latest temperatures.
+
+        Notes
+        -----
+        This coupling iteration is limited to when the time node is equal to zero. This is
+        assumed to be reasonable for most applications as 1) microscopic cross section changes with burn-up
+        are deemed to be less significant compared to convergence on the temperature state, and 2) temperature
+        distributions are not expected to dramatically change for time steps > 0.
+
+        .. warning::
+
+            The latter assumptions are design and application-specific and a subclass should be
+            considered when violated.
+
+        See Also
+        --------
+        :py:meth:`Assembly <armi.physics.neutronics.latticePhysics.latticePhysics.LatticePhysicsInterface.interactCoupled>`
+        """
+        if self.r.p.timeNode == 0:
+            self.interactBOC(cycle=None)
 
     def clearRepresentativeBlocks(self):
         """Clear the representative blocks."""
diff --git a/armi/physics/neutronics/parameters.py b/armi/physics/neutronics/parameters.py
index 97a2669c..2d7b8b0e 100644
--- a/armi/physics/neutronics/parameters.py
+++ b/armi/physics/neutronics/parameters.py
@@ -263,7 +263,10 @@ def _getNeutronicsBlockParams():
             ),
             location=ParamLocation.AVERAGE,
             default=0.0,
-            categories=[parameters.Category.detailedAxialExpansion],
+            categories=[
+                parameters.Category.detailedAxialExpansion,
+                parameters.Category.neutronics,
+            ],
         )
 
         def linPowByPin(self, value):
diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst
index e8599460..555a3f6e 100644
--- a/doc/release/0.2.rst
+++ b/doc/release/0.2.rst
@@ -40,6 +40,7 @@ Bug fixes
 #. Fixed a bug where the material namespace order for test_axialExpansionChanger.py was persisting beyond the tests. (`PR#1046 https://github.com/terrapower/armi/pull/1046`_)
 #. A bug was fixed in `PR#1022 <https://github.com/terrapower/armi/pull/1022>`_ where the gaseous fission products were not being removed from the core directly, but instead the fission yields within the lumped fission products were being adjusted.
 #. Fixed a bug where non-fuel depletable components were not being initialized with all nuclides with the ``explicitFissionProducts`` model (`PR#1067 https://github.com/terrapower/armi/pull/1067`_)
+#. Bug fix to ensure consistency between cross section group manager and lattice physics interface for tight coupling. (`PR#1118 <https://github.com/terrapower/armi/pull/1118>`_)
 
 ARMI v0.2.5
 ===========


Test patch:
diff --git a/armi/physics/neutronics/tests/test_cross_section_manager.py b/armi/physics/neutronics/tests/test_cross_section_manager.py
index d6558230..c530818d 100644
--- a/armi/physics/neutronics/tests/test_cross_section_manager.py
+++ b/armi/physics/neutronics/tests/test_cross_section_manager.py
@@ -348,6 +348,16 @@ class Test_CrossSectionGroupManager(unittest.TestCase):
             newReprBlock.getNumberDensities(), oldReprBlock.getNumberDensities()
         )
 
+    def test_interactCoupled(self):
+        # ensure that representativeBlocks remains empty if timeNode == 1
+        self.blockList[0].r.p.timeNode = 1
+        self.csm.interactCoupled(iteration=0)
+        self.assertFalse(self.csm.representativeBlocks)
+        # ensure that representativeBlocks get populated if timeNode == 0
+        self.blockList[0].r.p.timeNode = 0
+        self.csm.interactCoupled(iteration=0)
+        self.assertTrue(self.csm.representativeBlocks)
+
 
 class TestXSNumberConverters(unittest.TestCase):
     def test_conversion(self):
diff --git a/armi/reactor/converters/tests/test_uniformMesh.py b/armi/reactor/converters/tests/test_uniformMesh.py
index 89fbf562..fe773bc0 100644
--- a/armi/reactor/converters/tests/test_uniformMesh.py
+++ b/armi/reactor/converters/tests/test_uniformMesh.py
@@ -424,12 +424,14 @@ class TestGammaUniformMesh(unittest.TestCase):
             b.p.fastFlux = 2.0
             b.p.flux = 5.0
             b.p.power = 5.0
+            b.p.linPow = 2.0
 
-        # set original parameters on pre-mapped core with non-uniform assemblies
+        # set new parameters on core with uniform assemblies (emulate a physics kernel)
         self.converter.convert(self.r)
         for b in self.converter.convReactor.core.getBlocks():
             b.p.powerGamma = 0.5
             b.p.powerNeutron = 0.5
+            b.p.linPow = 10.0
             b.p.power = b.p.powerGamma + b.p.powerNeutron
 
         # check integral and density params
@@ -458,6 +460,9 @@ class TestGammaUniformMesh(unittest.TestCase):
             self.assertNotEqual(b.p.powerNeutron, 0.5)
             self.assertNotEqual(b.p.power, 1.0)
 
+            # has updated value
+            self.assertAlmostEqual(b.p.linPow, 10.0)
+
         # equal because these are mapped
         for expectedPower, expectedGammaPower, a in zip(
             assemblyPowers, assemblyGammaPowers, self.r.core