Repository: terrapower/armi
Pull request #1858 (2024-09-06 11:05:57)

Allow merging a solute with zero area
## What is the change?

The block converter `mergeComponents` was implemented to not allow merging any components that had zero area. This should be loosened to allow for merging a zero area solute into a non-zero area solvent. 

## Why is the change being made?

There are many situations where we don't care about modeling zero-area components (for example, a gap that has completely closed) and it is helpful to be able to merge these components into another component so that they don't appear on the block.

---

## Checklist

<!--
    You (the pull requester) should put an `x` in the boxes below you have completed.

    (If a checkbox doesn't apply to your PR, check it anyway.)
-->

- [x] This PR has only [one purpose or idea](https://terrapower.github.io/armi/developer/tooling.html#one-idea-one-pr).
- [x] [Tests](https://terrapower.github.io/armi/developer/tooling.html#test-it) have been added/updated to verify any new/changed code.

<!-- 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 -->

- [ ] The [release notes](https://terrapower.github.io/armi/developer/tooling.html#add-release-notes) have been updated if necessary.
- [ ] The [documentation](https://terrapower.github.io/armi/developer/tooling.html#document-it) is still up-to-date in the `doc` folder.
- [x] The dependencies are still up-to-date in `pyproject.toml`.


Patch:
diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml
new file mode 100644
index 00000000..cc016738
--- /dev/null
+++ b/.github/workflows/stale.yaml
@@ -0,0 +1,27 @@
+# This workflow warns and then closes PRs that have had no activity for a specified amount of time.
+#
+# You can adjust the behavior by modifying this file.
+# For more information, see: https://github.com/actions/stale
+name: Mark Stale PRs
+
+on:
+  schedule:
+  # once a day at 3:14 AM
+  - cron: '14 3 * * *'
+
+permissions:
+  pull-requests: write
+
+jobs:
+  stale:
+    runs-on: ubuntu-24.04
+    steps:
+      - uses: actions/stale@v8
+        with:
+          repo-token: ${{ secrets.GITHUB_TOKEN }}
+          stale-pr-message: "This pull request has been automatically marked as stale because it has not had any activity in the last 100 days. It will be closed in 7 days if no further activity occurs. Thank you for your contributions."
+          stale-pr-label: "stale"
+          days-before-pr-stale: 100
+          days-before-pr-close: 7
+          days-before-issue-stale: -1
+          operations-per-run: 100
\ No newline at end of file
diff --git a/armi/physics/thermalHydraulics/settings.py b/armi/physics/thermalHydraulics/settings.py
index a8df91ed..3975a35e 100644
--- a/armi/physics/thermalHydraulics/settings.py
+++ b/armi/physics/thermalHydraulics/settings.py
@@ -34,7 +34,7 @@ def defineSettings():
         ),
         setting.Setting(
             CONF_TH_KERNEL,
-            default=False,
+            default="",
             label="Thermal Hydraulics Kernel",
             description="Name of primary T/H solver in this run",
         ),
diff --git a/armi/reactor/converters/blockConverters.py b/armi/reactor/converters/blockConverters.py
index 77a0fa6c..d78d1c48 100644
--- a/armi/reactor/converters/blockConverters.py
+++ b/armi/reactor/converters/blockConverters.py
@@ -16,17 +16,15 @@
 import copy
 import math
 
-import numpy as np
 import matplotlib
 import matplotlib.pyplot as plt
-from matplotlib.patches import Wedge
+import numpy as np
 from matplotlib.collections import PatchCollection
+from matplotlib.patches import Wedge
 
-from armi.reactor import blocks
-from armi.reactor import grids
-from armi.reactor import components
-from armi.reactor.flags import Flags
 from armi import runLog
+from armi.reactor import blocks, components, grids
+from armi.reactor.flags import Flags
 
 SIN60 = math.sin(math.radians(60.0))
 
@@ -141,12 +139,15 @@ class BlockConverter:
                 "Components are not of compatible shape to be merged "
                 "solute: {}, solvent: {}".format(solute, solvent)
             )
-        if solute.getArea() <= 0 or solvent.getArea() <= 0:
+        if solute.getArea() < 0:
             raise ValueError(
-                "Cannot merge components if either have negative area. "
-                "{} area: {}, {} area : {}".format(
-                    solute, solvent, solute.getArea(), solvent.getArea()
-                )
+                "Cannot merge solute with negative area into a solvent. "
+                "{} area: {}".format(solute, solute.getArea())
+            )
+        if solvent.getArea() <= 0:
+            raise ValueError(
+                "Cannot merge into a solvent with negative or 0 area. "
+                "{} area: {}".format(solvent, solvent.getArea())
             )
 
     def restablishLinks(self, solute, solvent, soluteLinks):
diff --git a/doc/release/0.4.rst b/doc/release/0.4.rst
index 2b69760a..7696b0d3 100644
--- a/doc/release/0.4.rst
+++ b/doc/release/0.4.rst
@@ -11,6 +11,7 @@ New Features
 #. ARMI now supports Python 3.12. (`PR#1813 <https://github.com/terrapower/armi/pull/1813>`_)
 #. Removing the ``tabulate`` dependency by ingesting it to ``armi.utils.tabulate``. (`PR#1811 <https://github.com/terrapower/armi/pull/1811>`_)
 #. Adding ``--skip-inspection`` flag to ``CompareCases`` CLI. (`PR#1842 <https://github.com/terrapower/armi/pull/1842>`_)
+#. Allow merging a component with zero area into another component (`PR#1858 <https://github.com/terrapower/armi/pull/1858>`_)
 #. TBD
 
 API Changes
@@ -25,6 +26,7 @@ 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>`_)
+#. Changed data type of ``thKernel`` setting from ``bool`` to ``str`` in ``ThermalHydraulicsPlugin``. (`PR#1855 <https://github.com/terrapower/armi/pull/1855>`_)
 #. TBD
 
 Quality Work


Test patch:
diff --git a/armi/physics/thermalHydraulics/tests/__init__.py b/armi/physics/thermalHydraulics/tests/__init__.py
new file mode 100644
index 00000000..84776603
--- /dev/null
+++ b/armi/physics/thermalHydraulics/tests/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2024 TerraPower, LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/armi/physics/thermalHydraulics/tests/test_thermalHydraulicsPlugin.py b/armi/physics/thermalHydraulics/tests/test_thermalHydraulicsPlugin.py
new file mode 100644
index 00000000..3c9938ab
--- /dev/null
+++ b/armi/physics/thermalHydraulics/tests/test_thermalHydraulicsPlugin.py
@@ -0,0 +1,41 @@
+# Copyright 2024 TerraPower, LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Unit tests for the thermal hydraulics plugin."""
+from armi.physics import thermalHydraulics
+from armi.physics.thermalHydraulics.settings import CONF_DO_TH, CONF_TH_KERNEL
+from armi.settings import caseSettings
+from armi.tests.test_plugins import TestPlugin
+
+
+class TestThermalHydraulicsPlugin(TestPlugin):
+    plugin = thermalHydraulics.ThermalHydraulicsPlugin
+
+    def test_thermalHydraulicsSettingsLoaded(self):
+        """Test that the thermal hydraulics case settings are loaded."""
+        cs = caseSettings.Settings()
+
+        self.assertIn(CONF_DO_TH, cs)
+        self.assertIn(CONF_TH_KERNEL, cs)
+
+    def test_thermalHydraulicsSettingsSet(self):
+        """Test that the thermal hydraulics case settings are applied correctly."""
+        cs = caseSettings.Settings()
+        thKernelName = "testKernel"
+
+        cs[CONF_DO_TH] = True
+        cs[CONF_TH_KERNEL] = thKernelName
+
+        self.assertTrue(cs[CONF_DO_TH])
+        self.assertEqual(cs[CONF_TH_KERNEL], thKernelName)
diff --git a/armi/reactor/converters/tests/test_blockConverter.py b/armi/reactor/converters/tests/test_blockConverter.py
index a48f1a79..5cb72f3d 100644
--- a/armi/reactor/converters/tests/test_blockConverter.py
+++ b/armi/reactor/converters/tests/test_blockConverter.py
@@ -18,18 +18,63 @@ import unittest
 
 import numpy as np
 
+from armi.physics.neutronics.isotopicDepletion.isotopicDepletionInterface import (
+    isDepletable,
+)
+from armi.reactor import blocks, components, grids
 from armi.reactor.converters import blockConverters
-from armi.reactor import blocks
-from armi.reactor import components
 from armi.reactor.flags import Flags
 from armi.reactor.tests.test_blocks import loadTestBlock
-from armi.reactor.tests.test_reactors import loadTestReactor, TEST_ROOT
+from armi.reactor.tests.test_reactors import TEST_ROOT, loadTestReactor
 from armi.utils import hexagon
-from armi.reactor import grids
 from armi.utils.directoryChangers import TemporaryDirectoryChanger
-from armi.physics.neutronics.isotopicDepletion.isotopicDepletionInterface import (
-    isDepletable,
-)
+
+
+def buildSimpleFuelBlockNegativeArea():
+    """
+    Return a simple block containing fuel, clad, duct, and coolant.
+
+    The block has a negative-area gap between fuel and cladding for testing.
+    """
+    b = blocks.HexBlock("fuel", height=10.0)
+
+    fuelDims = {"Tinput": 25, "Thot": 600, "od": 0.76, "id": 0.00, "mult": 127.0}
+    cladDims = {"Tinput": 25, "Thot": 600, "od": 0.80, "id": 0.76, "mult": 127.0}
+    ductDims = {"Tinput": 25, "Thot": 600, "op": 16, "ip": 15.3, "mult": 1.0}
+    intercoolantDims = {
+        "Tinput": 400,
+        "Thot": 400,
+        "op": 17.0,
+        "ip": ductDims["op"],
+        "mult": 1.0,
+    }
+    coolDims = {"Tinput": 25.0, "Thot": 400}
+
+    fuel = components.Circle("fuel", "UZr", **fuelDims)
+    clad = components.Circle("clad", "HT9", **cladDims)
+    gapDims = {
+        "Tinput": 25,
+        "Thot": 600,
+        "od": "clad.id",
+        "id": "fuel.od",
+        "mult": 127.0,
+    }
+    gapDims["components"] = {"fuel": fuel, "clad": clad}
+    gap = components.Circle("gap", "Void", **gapDims)
+    duct = components.Hexagon("duct", "HT9", **ductDims)
+    coolant = components.DerivedShape("coolant", "Sodium", **coolDims)
+    intercoolant = components.Hexagon("intercoolant", "Sodium", **intercoolantDims)
+
+    b.add(fuel)
+    b.add(gap)
+    b.add(clad)
+    b.add(duct)
+    b.add(coolant)
+    b.add(intercoolant)
+
+    b.getVolumeFractions()
+
+    return b
 
 
 class TestBlockConverter(unittest.TestCase):
@@ -88,6 +133,25 @@ class TestBlockConverter(unittest.TestCase):
             loadTestBlock(), ["inner liner", "outer liner"], "clad"
         )
 
+    def test_dissolveZeroArea(self):
+        """Test dissolving a zero-area component into another."""
+        self._test_dissolve(loadTestBlock(), "gap2", "outer liner")
+
+    def test_dissolveIntoZeroArea(self):
+        """Test dissolving a component into a zero-area solvent (raises ValueError)."""
+        with self.assertRaises(ValueError):
+            self._test_dissolve(loadTestBlock(), "outer liner", "gap2")
+
+    def test_dissolveNegativeArea(self):
+        """Test dissolving a zero-area component into another."""
+        with self.assertRaises(ValueError):
+            self._test_dissolve(buildSimpleFuelBlockNegativeArea(), "gap", "clad")
+
+    def test_dissolveIntoNegativeArea(self):
+        """Test dissolving a zero-area component into another."""
+        with self.assertRaises(ValueError):
+            self._test_dissolve(buildSimpleFuelBlockNegativeArea(), "clad", "gap")
+
     def _test_dissolve_multi(self, block, soluteNames, solventName):
         converter = blockConverters.MultipleComponentMerger(
             block, soluteNames, solventName
diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py
index 56d28bc9..abd6d547 100644
--- a/armi/reactor/tests/test_blocks.py
+++ b/armi/reactor/tests/test_blocks.py
@@ -13,32 +13,31 @@
 # limitations under the License.
 """Tests blocks.py."""
 import copy
+import io
 import math
 import os
 import unittest
 from unittest.mock import MagicMock, patch
-import io
 
 import numpy as np
 from numpy.testing import assert_allclose
 
 from armi import materials, runLog, settings, tests
-from armi.reactor import blueprints
-from armi.reactor.components import basicShapes, complexShapes
 from armi.nucDirectory import nucDir, nuclideBases
+from armi.nuclearDataIO import xsCollections
 from armi.nuclearDataIO.cccc import isotxs
-from armi.physics.neutronics import NEUTRON, GAMMA
+from armi.physics.neutronics import GAMMA, NEUTRON
 from armi.physics.neutronics.settings import (
     CONF_LOADING_FILE,
     CONF_XS_KERNEL,
 )
-from armi.reactor import blocks, components, geometry, grids
+from armi.reactor import blocks, blueprints, components, geometry, grids
+from armi.reactor.components import basicShapes, complexShapes
 from armi.reactor.flags import Flags
 from armi.reactor.tests.test_assemblies import makeTestAssembly
 from armi.tests import ISOAA_PATH, TEST_ROOT
 from armi.utils import hexagon, units
 from armi.utils.units import MOLES_PER_CC_TO_ATOMS_PER_BARN_CM
-from armi.nuclearDataIO import xsCollections
 
 NUM_PINS_IN_TEST_BLOCK = 217