Repository: terrapower/armi

Issue:
Add reactor time to stdout summaries
@opotowsky, @john-science - As I am looking at the open PR, I'm wondering now if we should also add anything about the reactors current "time" in the simulation as well. Cycle and Time node are great, but deciphering the cycle and node relationship to time step and cycle length could be helpful to users. @keckler I'm curious if this would helpful for you in multi cycle benchmarks like FFTF and EBR-II.

_Originally posted by @jakehader in https://github.com/terrapower/armi/issues/1748#issuecomment-2210954513_
            

Discussion:

Patch:
diff --git a/armi/operators/operator.py b/armi/operators/operator.py
index 3ff2c62b..759ddbfa 100644
--- a/armi/operators/operator.py
+++ b/armi/operators/operator.py
@@ -567,17 +567,21 @@ class Operator:
         """
         if interactionName == "Coupled":
             cycleNodeInfo = (
-                f" - timestep: cycle {self.r.p.cycle}, node {self.r.p.timeNode}"
-                f" - iteration {self.r.core.p.coupledIteration}"
+                f" - timestep: cycle {self.r.p.cycle}, node {self.r.p.timeNode}, "
+                f"year {'{0:.2f}'.format(self.r.p.time)} - iteration "
+                f"{self.r.core.p.coupledIteration}"
             )
         elif interactionName in ("BOC", "EOC"):
             cycleNodeInfo = f" - timestep: cycle {self.r.p.cycle}"
+            # - timestep: cycle 2
         elif interactionName in ("Init", "BOL", "EOL"):
             cycleNodeInfo = ""
         else:
             cycleNodeInfo = (
-                f" - timestep: cycle {self.r.p.cycle}, node {self.r.p.timeNode}"
+                f" - timestep: cycle {self.r.p.cycle}, node {self.r.p.timeNode}, "
+                f"year {'{0:.2f}'.format(self.r.p.time)}"
             )
+
         return cycleNodeInfo
 
     def _debugDB(self, interactionName, interfaceName, statePointIndex=0):
diff --git a/armi/reactor/converters/blockConverters.py b/armi/reactor/converters/blockConverters.py
index 62f7873f..a74491d8 100644
--- a/armi/reactor/converters/blockConverters.py
+++ b/armi/reactor/converters/blockConverters.py
@@ -311,7 +311,8 @@ class MultipleComponentMerger(BlockConverter):
                 soluteName, self.solventName, minID=self.specifiedMinID
             )
         solvent = self._sourceBlock.getComponentByName(self.solventName)
-        BlockConverter._verifyExpansion(self, self.soluteNames, solvent)
+        if solvent.__class__ is not components.DerivedShape:
+            BlockConverter._verifyExpansion(self, self.soluteNames, solvent)
         return self._sourceBlock
 
 
diff --git a/doc/release/0.3.rst b/doc/release/0.3.rst
index 1371e01d..49faedb3 100644
--- a/doc/release/0.3.rst
+++ b/doc/release/0.3.rst
@@ -20,6 +20,7 @@ New Features
 API Changes
 -----------
 #. Replacing the concrete material with a better reference. (`PR#1717 <https://github.com/terrapower/armi/pull/1717>`_)
+#. Adding more detailed time information to logging. (`PR#1796 <https://github.com/terrapower/armi/pull/1796>`_)
 #. Renaming ``structuredgrid.py`` to camelCase. (`PR#1650 <https://github.com/terrapower/armi/pull/1650>`_)
 #. Removing unused argument from ``Block.coords()``. (`PR#1651 <https://github.com/terrapower/armi/pull/1651>`_)
 #. Removing unused method ``HexGrid.allPositionsInThird()``. (`PR#1655 <https://github.com/terrapower/armi/pull/1655>`_)


Test patch:
diff --git a/armi/bookkeeping/db/tests/test_databaseInterface.py b/armi/bookkeeping/db/tests/test_databaseInterface.py
index 50cb7b4d..7b106b27 100644
--- a/armi/bookkeeping/db/tests/test_databaseInterface.py
+++ b/armi/bookkeeping/db/tests/test_databaseInterface.py
@@ -350,7 +350,8 @@ class TestDatabaseReading(unittest.TestCase):
         # than the original input file. This allows settings to be
         # changed in memory like this and survive for testing.
         newSettings = {"verbosity": "extra"}
-        newSettings["nCycles"] = 2
+        cls.nCycles = 2
+        newSettings["nCycles"] = cls.nCycles
         newSettings["burnSteps"] = 2
         o, r = loadTestReactor(customSettings=newSettings)
         reduceTestReactorRings(r, o.cs, 3)
@@ -510,6 +511,18 @@ class TestDatabaseReading(unittest.TestCase):
 
         assert_allclose(numDensVec1, numDensVec2)
 
+    def test_timesteps(self):
+        with Database3(self.dbName, "r") as db:
+            # build time steps in the DB file
+            timesteps = []
+            for cycle in range(self.nCycles):
+                for bStep in range(3):
+                    timesteps.append(f"/c0{cycle}n0{bStep}")
+            timesteps.append("/c01n02EOL")
+
+            # verify the timesteps are correct, including the EOL
+            self.assertEqual(list(db.keys()), timesteps)
+
 
 class TestBadName(unittest.TestCase):
     def test_badDBName(self):
diff --git a/armi/cases/tests/test_cases.py b/armi/cases/tests/test_cases.py
index 4c44689b..b04c9fcd 100644
--- a/armi/cases/tests/test_cases.py
+++ b/armi/cases/tests/test_cases.py
@@ -234,7 +234,7 @@ class TestArmiCase(unittest.TestCase):
                 self.assertIn("Triggering BOL Event", mock.getStdout())
                 self.assertIn("xsGroups", mock.getStdout())
                 self.assertIn(
-                    "Completed EveryNode - timestep: cycle 0, node 0 Event",
+                    "Completed EveryNode - timestep: cycle 0, node 0, year 0.00 Event",
                     mock.getStdout(),
                 )
 
diff --git a/armi/operators/tests/test_operators.py b/armi/operators/tests/test_operators.py
index 6a0638bd..9b29dd48 100644
--- a/armi/operators/tests/test_operators.py
+++ b/armi/operators/tests/test_operators.py
@@ -207,15 +207,28 @@ class OperatorTests(unittest.TestCase):
         self.assertIn("history", interfaceNames)
         self.assertNotIn("xsGroups", interfaceNames)
 
-        # Test EOL
-        interfaces = self.o.getActiveInterfaces("EOL")
-        self.assertEqual(interfaces[-1].name, "main")
-
         # Test Coupled
         interfaces = self.o.getActiveInterfaces("Coupled")
         for test, ref in zip(interfaces, self.activeInterfaces):
             self.assertEqual(test.name, ref.name)
 
+        # Test EOL
+        interfaces = self.o.getActiveInterfaces("EOL")
+        self.assertEqual(interfaces[-1].name, "main")
+
+        # Test excludedInterfaceNames
+        excludedInterfaceNames = ["fissionProducts", "fuelHandler", "xsGroups"]
+        interfaces = self.o.getActiveInterfaces(
+            "EOL", excludedInterfaceNames=excludedInterfaceNames
+        )
+        interfaceNames = [ii.name for ii in interfaces]
+        self.assertIn("history", interfaceNames)
+        self.assertIn("main", interfaceNames)
+        self.assertIn("snapshot", interfaceNames)
+        self.assertNotIn("fissionProducts", interfaceNames)
+        self.assertNotIn("fuelHandler", interfaceNames)
+        self.assertNotIn("xsGroups", interfaceNames)
+
     def test_loadStateError(self):
         """The ``loadTestReactor()`` test tool does not have any history in the DB to load from."""
         # a first, simple test that this method fails correctly
@@ -551,6 +564,7 @@ class TestInterfaceAndEventHeaders(unittest.TestCase):
         )
         cls.r.p.cycle = 0
         cls.r.p.timeNode = 1
+        cls.r.p.time = 11.01
         cls.r.core.p.coupledIteration = 7
 
     def test_expandCycleAndTimeNodeArgs_Empty(self):
@@ -572,7 +586,8 @@ class TestInterfaceAndEventHeaders(unittest.TestCase):
         """When cycleNodeInfo should return the cycle and node."""
         self.assertEqual(
             self.o._expandCycleAndTimeNodeArgs(interactionName="EveryNode"),
-            f" - timestep: cycle {self.r.p.cycle}, node {self.r.p.timeNode}",
+            f" - timestep: cycle {self.r.p.cycle}, node {self.r.p.timeNode}, "
+            f"year {'{0:.2f}'.format(self.r.p.time)}",
         )
 
     def test_expandCycleAndTimeNodeArgs_Coupled(self):
@@ -580,7 +595,7 @@ class TestInterfaceAndEventHeaders(unittest.TestCase):
         self.assertEqual(
             self.o._expandCycleAndTimeNodeArgs(interactionName="Coupled"),
             (
-                f" - timestep: cycle {self.r.p.cycle}, node {self.r.p.timeNode} "
-                f"- iteration {self.r.core.p.coupledIteration}"
+                f" - timestep: cycle {self.r.p.cycle}, node {self.r.p.timeNode}, year "
+                f"{'{0:.2f}'.format(self.r.p.time)} - iteration {self.r.core.p.coupledIteration}"
             ),
         )
diff --git a/armi/reactor/converters/tests/test_blockConverter.py b/armi/reactor/converters/tests/test_blockConverter.py
index 9218a73f..95402aa9 100644
--- a/armi/reactor/converters/tests/test_blockConverter.py
+++ b/armi/reactor/converters/tests/test_blockConverter.py
@@ -81,6 +81,22 @@ class TestBlockConverter(unittest.TestCase):
         self.assertNotIn(soluteName, convertedBlock.getComponentNames())
         self._checkAreaAndComposition(block, convertedBlock)
 
+    def test_dissolveMultiple(self):
+        """Test dissolving multiple components into another."""
+        self._test_dissolve_multi(loadTestBlock(), ["wire", "clad"], "coolant")
+        self._test_dissolve_multi(
+            loadTestBlock(), ["inner liner", "outer liner"], "clad"
+        )
+
+    def _test_dissolve_multi(self, block, soluteNames, solventName):
+        converter = blockConverters.MultipleComponentMerger(
+            block, soluteNames, solventName
+        )
+        convertedBlock = converter.convert()
+        for soluteName in soluteNames:
+            self.assertNotIn(soluteName, convertedBlock.getComponentNames())
+        self._checkAreaAndComposition(block, convertedBlock)
+
     def test_build_NthRing(self):
         """Test building of one ring."""
         RING = 6