Repository: terrapower/armi
Pull request #2114 (2025-04-07 16:51:00)

Allowing component area to be queried at arbitrary temp
## What is the change? Why is it being made?

Currently component area can only be queried at input (cold) temperature or at hot temperature. This PR will allow component area to be calculated at an arbitrary temperature. 


## SCR Information

<!-- MANDATORY: uncomment one-and-only-one of these -->
Change Type: features
<!-- Change Type: fixes -->
<!-- Change Type: trivial -->
<!-- Change Type: docs -->

<!-- MANDATORY: Describe the change in one sentence -->
One-Sentence Description: Allow calculation of component area at arbitrary temperature

<!-- MANDATORY: Describe any impact on the requirements, all on one line -->
One-line Impact on Requirements: NA


---

## Checklist

<!--
    The pull request author should check the box if the condition is met OR if it does not apply.
-->

- [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.
- [x] The [documentation](https://terrapower.github.io/armi/developer/tooling.html#document-it) is still up-to-date in the `doc` folder.
- [x] The code style follows [good practices](https://terrapower.github.io/armi/developer/standards_and_practices.html).
- [x] The dependencies are still up-to-date in `pyproject.toml`.


alexhjames: @john-science I'm done fiddling with this PR, it's good for you to review whenever you get a chance. 
john-science: @alexhjames @keckler Sorry, the reason I haven't gotten further on this PR yet is because I am trying to decide why it is necessary after the last PR on this exact topic: https://github.com/terrapower/armi/pull/1831

Does anyone understand the distinction?
alexhjames: @john-science I replied on the issue (https://github.com/terrapower/armi/issues/2113#issuecomment-2784471591) with a detailed explanation. There isn't actually a bug in the code despite it appearing so. This PR is mainly focused on adding a Tc argument to `getArea`. It does also refactor the derived shape logic slightly to make it less circuitous. 

Patch:
diff --git a/armi/reactor/components/__init__.py b/armi/reactor/components/__init__.py
index 8193a998..73f04c8e 100644
--- a/armi/reactor/components/__init__.py
+++ b/armi/reactor/components/__init__.py
@@ -151,7 +151,7 @@ class UnshapedComponent(Component):
         )
         self._linkAndStoreDimensions(components, modArea=modArea)
 
-    def getComponentArea(self, cold=False):
+    def getComponentArea(self, cold=False, Tc=None):
         """
         Get the area of this component in cm^2.
 
@@ -159,12 +159,20 @@ class UnshapedComponent(Component):
         ----------
         cold : bool, optional
             If True, compute the area with as-input dimensions, instead of thermally-expanded.
+        Tc : float, optional
+            Temperature in C to compute the area at
         """
+        if cold and Tc is not None:
+            raise ValueError(
+                f"Cannot compute component area at {Tc} and cold dimensions simultaneously."
+            )
         coldArea = self.p.area
         if cold:
             return coldArea
+        if Tc is None:
+            Tc = self.temperatureInC
 
-        return self.getThermalExpansionFactor(self.temperatureInC) ** 2 * coldArea
+        return self.getThermalExpansionFactor(Tc) ** 2 * coldArea
 
     def getBoundingCircleOuterDiameter(self, Tc=None, cold=False):
         """
@@ -257,7 +265,7 @@ class UnshapedVolumetricComponent(UnshapedComponent):
         )
         self._linkAndStoreDimensions(components, op=op, userDefinedVolume=volume)
 
-    def getComponentArea(self, cold=False):
+    def getComponentArea(self, cold=False, Tc=None):
         return self.getVolume() / self.parent.getHeight()
 
     def getComponentVolume(self):
@@ -464,7 +472,7 @@ class DerivedShape(UnshapedComponent):
         vol = UnshapedComponent.getVolume(self)
         return vol
 
-    def getComponentArea(self, cold=False):
+    def getComponentArea(self, cold=False, Tc=None):
         """
         Get the area of this component in cm^2.
 
@@ -472,16 +480,32 @@ class DerivedShape(UnshapedComponent):
         ----------
         cold : bool, optional
             If True, compute the area with as-input dimensions, instead of thermally-expanded.
+        Tc : float, optional
+            Temperature in C to compute the area at
         """
+        if cold and Tc is not None:
+            raise ValueError(
+                f"Cannot compute component area at {Tc} and cold dimensions simultaneously."
+            )
+
         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
+            parentArea = self.parent.getMaxArea()
+            # NOTE: Here we assume there is one-and-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 Tc is not None:
+            # The DerivedShape has the area of the parent minus the other siblings
+            parentArea = self.parent.getMaxArea()
+            # NOTE: Here we assume there is one-and-only-one DerivedShape in each Component
+            siblings = sum(
+                [c.getArea(Tc=Tc) for c in self.parent if type(c) != DerivedShape]
+            )
+            return parentArea - siblings
+
         if self.parent.derivedMustUpdate:
             self.computeVolume()
 
diff --git a/armi/reactor/components/basicShapes.py b/armi/reactor/components/basicShapes.py
index 2edc20a9..6517e006 100644
--- a/armi/reactor/components/basicShapes.py
+++ b/armi/reactor/components/basicShapes.py
@@ -76,11 +76,11 @@ class Circle(ShapedComponent):
     def getCircleInnerDiameter(self, Tc=None, cold=False):
         return min(self.getDimension("id", Tc, cold), self.getDimension("od", Tc, cold))
 
-    def getComponentArea(self, cold=False):
+    def getComponentArea(self, cold=False, Tc=None):
         """Computes the area for the circle component in cm^2."""
-        idiam = self.getDimension("id", cold=cold)
-        od = self.getDimension("od", cold=cold)
-        mult = self.getDimension("mult", cold=cold)
+        idiam = self.getDimension("id", cold=cold, Tc=Tc)
+        od = self.getDimension("od", cold=cold, Tc=Tc)
+        mult = self.getDimension("mult", cold=cold, Tc=Tc)
         area = math.pi * (od**2 - idiam**2) / 4.0
         area *= mult
         return area
@@ -148,7 +148,7 @@ class Hexagon(ShapedComponent):
         sideLength = self.getDimension("ip", Tc, cold) / math.sqrt(3)
         return 2.0 * sideLength
 
-    def getComponentArea(self, cold=False):
+    def getComponentArea(self, cold=False, Tc=None):
         """
         Computes the area for the hexagon component in cm^2.
 
@@ -156,8 +156,8 @@ class Hexagon(ShapedComponent):
         -----
         http://www3.wolframalpha.com/input/?i=hexagon
         """
-        op = self.getDimension("op", cold=cold)
-        ip = self.getDimension("ip", cold=cold)
+        op = self.getDimension("op", cold=cold, Tc=Tc)
+        ip = self.getDimension("ip", cold=cold, Tc=Tc)
         mult = self.getDimension("mult")
         area = math.sqrt(3.0) / 2.0 * (op**2 - ip**2)
         area *= mult
@@ -249,12 +249,12 @@ class Rectangle(ShapedComponent):
         widthI = self.getDimension("widthInner", Tc, cold=cold)
         return math.sqrt(widthI**2 + lengthI**2)
 
-    def getComponentArea(self, cold=False):
+    def getComponentArea(self, cold=False, Tc=None):
         """Computes the area of the rectangle in cm^2."""
-        lengthO = self.getDimension("lengthOuter", cold=cold)
-        widthO = self.getDimension("widthOuter", cold=cold)
-        lengthI = self.getDimension("lengthInner", cold=cold)
-        widthI = self.getDimension("widthInner", cold=cold)
+        lengthO = self.getDimension("lengthOuter", cold=cold, Tc=Tc)
+        widthO = self.getDimension("widthOuter", cold=cold, Tc=Tc)
+        lengthI = self.getDimension("lengthInner", cold=cold, Tc=Tc)
+        widthI = self.getDimension("widthInner", cold=cold, Tc=Tc)
         mult = self.getDimension("mult")
         area = mult * (lengthO * widthO - lengthI * widthI)
         return area
@@ -325,10 +325,10 @@ class SolidRectangle(Rectangle):
         self.p.lengthInner = 0
         self.p.widthInner = 0
 
-    def getComponentArea(self, cold=False):
+    def getComponentArea(self, cold=False, Tc=None):
         """Computes the area of the solid rectangle in cm^2."""
-        lengthO = self.getDimension("lengthOuter", cold=cold)
-        widthO = self.getDimension("widthOuter", cold=cold)
+        lengthO = self.getDimension("lengthOuter", cold=cold, Tc=Tc)
+        widthO = self.getDimension("widthOuter", cold=cold, Tc=Tc)
         mult = self.getDimension("mult")
         area = mult * (lengthO * widthO)
         return area
@@ -383,10 +383,10 @@ class Square(Rectangle):
             modArea=modArea,
         )
 
-    def getComponentArea(self, cold=False):
+    def getComponentArea(self, cold=False, Tc=None):
         """Computes the area of the square in cm^2."""
-        widthO = self.getDimension("widthOuter", cold=cold)
-        widthI = self.getDimension("widthInner", cold=cold)
+        widthO = self.getDimension("widthOuter", cold=cold, Tc=Tc)
+        widthI = self.getDimension("widthInner", cold=cold, Tc=Tc)
         mult = self.getDimension("mult")
         area = mult * (widthO * widthO - widthI * widthI)
         return area
@@ -467,10 +467,10 @@ class Triangle(ShapedComponent):
             components, base=base, height=height, mult=mult, modArea=modArea
         )
 
-    def getComponentArea(self, cold=False):
+    def getComponentArea(self, cold=False, Tc=None):
         """Computes the area of the triangle in cm^2."""
-        base = self.getDimension("base", cold=cold)
-        height = self.getDimension("height", cold=cold)
+        base = self.getDimension("base", cold=cold, Tc=Tc)
+        height = self.getDimension("height", cold=cold, Tc=Tc)
         mult = self.getDimension("mult")
         area = mult * base * height / 2.0
         return area
diff --git a/armi/reactor/components/complexShapes.py b/armi/reactor/components/complexShapes.py
index 247447ab..7eefab9a 100644
--- a/armi/reactor/components/complexShapes.py
+++ b/armi/reactor/components/complexShapes.py
@@ -65,11 +65,11 @@ class HoledHexagon(basicShapes.Hexagon):
             components, op=op, holeOD=holeOD, nHoles=nHoles, mult=mult, modArea=modArea
         )
 
-    def getComponentArea(self, cold=False):
+    def getComponentArea(self, cold=False, Tc=None):
         r"""Computes the area for the hexagon with n number of circular holes in cm^2."""
-        op = self.getDimension("op", cold=cold)
-        holeOD = self.getDimension("holeOD", cold=cold)
-        nHoles = self.getDimension("nHoles", cold=cold)
+        op = self.getDimension("op", cold=cold, Tc=Tc)
+        holeOD = self.getDimension("holeOD", cold=cold, Tc=Tc)
+        nHoles = self.getDimension("nHoles", cold=cold, Tc=Tc)
         mult = self.getDimension("mult")
         hexArea = math.sqrt(3.0) / 2.0 * (op**2)
         circularArea = nHoles * math.pi * ((holeOD / 2.0) ** 2)
@@ -125,10 +125,10 @@ class HexHoledCircle(basicShapes.Circle):
             components, od=od, holeOP=holeOP, mult=mult, modArea=modArea
         )
 
-    def getComponentArea(self, cold=False):
+    def getComponentArea(self, cold=False, Tc=None):
         r"""Computes the area for the circle with one hexagonal hole."""
-        od = self.getDimension("od", cold=cold)
-        holeOP = self.getDimension("holeOP", cold=cold)
+        od = self.getDimension("od", cold=cold, Tc=Tc)
+        holeOP = self.getDimension("holeOP", cold=cold, Tc=Tc)
         mult = self.getDimension("mult")
         hexArea = math.sqrt(3.0) / 2.0 * (holeOP**2)
         circularArea = math.pi * ((od / 2.0) ** 2)
@@ -181,12 +181,12 @@ class HoledRectangle(basicShapes.Rectangle):
             modArea=modArea,
         )
 
-    def getComponentArea(self, cold=False):
+    def getComponentArea(self, cold=False, Tc=None):
         r"""Computes the area (in cm^2) for the the rectangle with one hole in it."""
-        length = self.getDimension("lengthOuter", cold=cold)
-        width = self.getDimension("widthOuter", cold=cold)
+        length = self.getDimension("lengthOuter", cold=cold, Tc=Tc)
+        width = self.getDimension("widthOuter", cold=cold, Tc=Tc)
         rectangleArea = length * width
-        holeOD = self.getDimension("holeOD", cold=cold)
+        holeOD = self.getDimension("holeOD", cold=cold, Tc=Tc)
         circularArea = math.pi * ((holeOD / 2.0) ** 2)
         mult = self.getDimension("mult")
         area = mult * (rectangleArea - circularArea)
@@ -243,11 +243,11 @@ class HoledSquare(basicShapes.Square):
             components, widthOuter=widthOuter, holeOD=holeOD, mult=mult, modArea=modArea
         )
 
-    def getComponentArea(self, cold=False):
+    def getComponentArea(self, cold=False, Tc=None):
         r"""Computes the area (in cm^2) for the the square with one hole in it."""
-        width = self.getDimension("widthOuter", cold=cold)
+        width = self.getDimension("widthOuter", cold=cold, Tc=Tc)
         rectangleArea = width**2
-        holeOD = self.getDimension("holeOD", cold=cold)
+        holeOD = self.getDimension("holeOD", cold=cold, Tc=Tc)
         circularArea = math.pi * ((holeOD / 2.0) ** 2)
         mult = self.getDimension("mult")
         area = mult * (rectangleArea - circularArea)
@@ -343,12 +343,12 @@ class Helix(ShapedComponent):
             "od", Tc, cold
         )
 
-    def getComponentArea(self, cold=False):
+    def getComponentArea(self, cold=False, Tc=None):
         """Computes the area for the helix in cm^2."""
-        ap = self.getDimension("axialPitch", cold=cold)
-        hd = self.getDimension("helixDiameter", cold=cold)
-        id = self.getDimension("id", cold=cold)
-        od = self.getDimension("od", cold=cold)
+        ap = self.getDimension("axialPitch", cold=cold, Tc=Tc)
+        hd = self.getDimension("helixDiameter", cold=cold, Tc=Tc)
+        id = self.getDimension("id", cold=cold, Tc=Tc)
+        od = self.getDimension("od", cold=cold, Tc=Tc)
         mult = self.getDimension("mult")
         c = ap / (2.0 * math.pi)
         helixFactor = math.sqrt((hd / 2.0) ** 2 + c**2) / c
diff --git a/armi/reactor/components/component.py b/armi/reactor/components/component.py
index 326f4d5c..b1b561e8 100644
--- a/armi/reactor/components/component.py
+++ b/armi/reactor/components/component.py
@@ -481,7 +481,7 @@ class Component(composites.Composite, metaclass=ComponentType):
             # material, not a lumpedFissionProductCompatable material
             pass
 
-    def getArea(self, cold=False):
+    def getArea(self, cold=False, Tc=None):
         """
         Get the area of a Component in cm^2.
 
@@ -495,13 +495,13 @@ class Component(composites.Composite, metaclass=ComponentType):
         --------
         block.getVolumeFractions: component coolant is typically the "leftover" and is calculated and set here
         """
-        area = self.getComponentArea(cold=cold)
+        area = self.getComponentArea(cold=cold, Tc=Tc)
         if self.p.get("modArea", None):
             comp, arg = self.p.modArea
             if arg == "sub":
-                area -= comp.getComponentArea(cold=cold)
+                area -= comp.getComponentArea(cold=cold, Tc=Tc)
             elif arg == "add":
-                area += comp.getComponentArea(cold=cold)
+                area += comp.getComponentArea(cold=cold, Tc=Tc)
             else:
                 raise ValueError("Option {} does not exist".format(arg))
 
@@ -617,7 +617,7 @@ class Component(composites.Composite, metaclass=ComponentType):
         """Returns True if the component material is a solid."""
         return not isinstance(self.material, material.Fluid)
 
-    def getComponentArea(self, cold=False):
+    def getComponentArea(self, cold=False, Tc=None):
         """
         Get the area of this component in cm^2.
 
@@ -625,6 +625,8 @@ class Component(composites.Composite, metaclass=ComponentType):
         ----------
         cold : bool, optional
             Compute the area with as-input dimensions instead of thermally-expanded
+        Tc : float, optional
+            Temperature to compute the area at
         """
         raise NotImplementedError
 
diff --git a/armi/reactor/components/volumetricShapes.py b/armi/reactor/components/volumetricShapes.py
index ab8d96a9..95808ab1 100644
--- a/armi/reactor/components/volumetricShapes.py
+++ b/armi/reactor/components/volumetricShapes.py
@@ -62,10 +62,14 @@ class Sphere(ShapedComponent):
         """Abstract bounding circle method that should be overwritten by each shape subclass."""
         return self.getDimension("od")
 
-    def getComponentArea(self, cold=False):
+    def getComponentArea(self, cold=False, Tc=None):
         """Compute an average area over the height."""
         from armi.reactor.blocks import Block  # avoid circular import
 
+        if Tc is not None:
+            raise NotImplementedError(
+                f"Cannot calculate area at specified temperature: {Tc}"
+            )
         block = self.getAncestor(lambda c: isinstance(c, Block))
         return self.getComponentVolume(cold) / block.getHeight()
         # raise NotImplementedError("Cannot compute area of a sphere component.")
@@ -131,7 +135,7 @@ class Cube(ShapedComponent):
             modArea=modArea,
         )
 
-    def getComponentArea(self, cold=False):
+    def getComponentArea(self, cold=False, Tc=None):
         raise NotImplementedError("Cannot compute area of a cube component.")
 
     def getComponentVolume(self):
@@ -211,7 +215,11 @@ class RadialSegment(ShapedComponent):
             outer_theta=outer_theta,
         )
 
-    def getComponentArea(self, refVolume=None, refHeight=None, cold=False):
+    def getComponentArea(self, refVolume=None, refHeight=None, cold=False, Tc=None):
+        if Tc is not None:
+            raise NotImplementedError(
+                f"Cannot calculate area at specified temperature: {Tc}"
+            )
         if refHeight:
             return (
                 (self.getDimension("height", cold=cold) / refHeight)
@@ -346,7 +354,11 @@ class DifferentialRadialSegment(RadialSegment):
             + self.getDimension("azimuthal_differential"),
         )
 
-    def getComponentArea(self, refVolume=None, refHeight=None, cold=False):
+    def getComponentArea(self, refVolume=None, refHeight=None, cold=False, Tc=None):
+        if Tc is not None:
+            raise NotImplementedError(
+                f"Cannot calculate area at specified temperature: {Tc}"
+            )
         self.updateDims()
         return RadialSegment.getComponentArea(
             self, refVolume=None, refHeight=None, cold=False


Test patch:
diff --git a/armi/reactor/components/tests/__init__.py b/armi/reactor/components/tests/__init__.py
new file mode 100644
index 00000000..9bd2ab59
--- /dev/null
+++ b/armi/reactor/components/tests/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2025 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/reactor/components/tests/test_basicShapes.py b/armi/reactor/components/tests/test_basicShapes.py
new file mode 100644
index 00000000..beb71ce4
--- /dev/null
+++ b/armi/reactor/components/tests/test_basicShapes.py
@@ -0,0 +1,197 @@
+# Copyright 2025 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 testing file for basic shapes."""
+import math
+import unittest
+
+from armi.materials import resolveMaterialClassByName
+from armi.reactor.components.basicShapes import (
+    Circle,
+    Hexagon,
+    Rectangle,
+    SolidRectangle,
+    Square,
+    Triangle,
+)
+
+
+class TestBasicShapes(unittest.TestCase):
+    """Class for testing basic shapes."""
+
+    @classmethod
+    def setUpClass(cls):
+        cls.material = resolveMaterialClassByName("HT9")()
+
+    def test_circleArea(self):
+        od = 2.0
+        id = 1.5
+        comp = Circle(
+            "Test", material=self.material, Tinput=20, Thot=300, od=od, id=id, mult=2
+        )
+
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=True), math.pi * (od**2 / 4 - id**2 / 4) * 2
+        )
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=True), comp.getComponentArea(Tc=20.0)
+        )
+
+        odHot = comp.getDimension("od")
+        idHot = comp.getDimension("id")
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=False),
+            math.pi * (odHot**2 / 4 - idHot**2 / 4) * 2,
+        )
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=False), comp.getComponentArea(Tc=300)
+        )
+
+    def test_hexagonArea(self):
+        op = 2.0
+        ip = 1.5
+        comp = Hexagon(
+            "Test", material=self.material, Tinput=20, Thot=300, op=op, ip=ip, mult=2
+        )
+
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=True), math.sqrt(3.0) * (op**2 - ip**2)
+        )
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=True), comp.getComponentArea(Tc=20.0)
+        )
+
+        opHot = comp.getDimension("op")
+        ipHot = comp.getDimension("ip")
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=False),
+            math.sqrt(3.0) * (opHot**2 - ipHot**2),
+        )
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=False), comp.getComponentArea(Tc=300)
+        )
+
+    def test_rectangleArea(self):
+        lo = 2.0
+        li = 1.5
+        wo = 2.5
+        wi = 1.25
+        comp = Rectangle(
+            "Test",
+            material=self.material,
+            Tinput=20,
+            Thot=300,
+            lengthOuter=lo,
+            lengthInner=li,
+            widthOuter=wo,
+            widthInner=wi,
+            mult=2,
+        )
+
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=True), 2 * (lo * wo - li * wi)
+        )
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=True), comp.getComponentArea(Tc=20.0)
+        )
+
+        loHot = comp.getDimension("lengthOuter")
+        liHot = comp.getDimension("lengthInner")
+        woHot = comp.getDimension("widthOuter")
+        wiHot = comp.getDimension("widthInner")
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=False), 2 * (loHot * woHot - liHot * wiHot)
+        )
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=False), comp.getComponentArea(Tc=300)
+        )
+
+    def test_solidRectangleArea(self):
+        lo = 2.0
+        wo = 2.5
+        comp = SolidRectangle(
+            "Test",
+            material=self.material,
+            Tinput=20,
+            Thot=300,
+            lengthOuter=lo,
+            widthOuter=wo,
+            mult=2,
+        )
+
+        self.assertAlmostEqual(comp.getComponentArea(cold=True), 2 * lo * wo)
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=True), comp.getComponentArea(Tc=20.0)
+        )
+
+        loHot = comp.getDimension("lengthOuter")
+        woHot = comp.getDimension("widthOuter")
+        self.assertAlmostEqual(comp.getComponentArea(cold=False), 2 * loHot * woHot)
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=False), comp.getComponentArea(Tc=300)
+        )
+
+    def test_squareArea(self):
+        wo = 2.5
+        wi = 1.25
+        comp = Square(
+            "Test",
+            material=self.material,
+            Tinput=20,
+            Thot=300,
+            widthOuter=wo,
+            widthInner=wi,
+            mult=2,
+        )
+
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=True), 2 * (wo**2 - wi**2)
+        )
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=True), comp.getComponentArea(Tc=20.0)
+        )
+
+        woHot = comp.getDimension("widthOuter")
+        wiHot = comp.getDimension("widthInner")
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=False), 2 * (woHot**2 - wiHot**2)
+        )
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=False), comp.getComponentArea(Tc=300)
+        )
+
+    def test_triangleArea(self):
+        base = 2.5
+        height = 1.25
+        comp = Triangle(
+            "Test",
+            material=self.material,
+            Tinput=20,
+            Thot=300,
+            base=base,
+            height=height,
+            mult=2,
+        )
+
+        self.assertAlmostEqual(comp.getComponentArea(cold=True), base * height)
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=True), comp.getComponentArea(Tc=20.0)
+        )
+
+        baseHot = comp.getDimension("base")
+        heightHot = comp.getDimension("height")
+        self.assertAlmostEqual(comp.getComponentArea(cold=False), baseHot * heightHot)
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=False), comp.getComponentArea(Tc=300)
+        )
diff --git a/armi/reactor/components/tests/test_complexShapes.py b/armi/reactor/components/tests/test_complexShapes.py
new file mode 100644
index 00000000..796cfb15
--- /dev/null
+++ b/armi/reactor/components/tests/test_complexShapes.py
@@ -0,0 +1,174 @@
+# Copyright 2025 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 testing file for basic shapes."""
+import math
+import unittest
+
+from armi.materials import resolveMaterialClassByName
+from armi.reactor.components.complexShapes import (
+    HexHoledCircle,
+    HoledHexagon,
+    HoledRectangle,
+    HoledSquare,
+)
+
+
+class TestComplexShapes(unittest.TestCase):
+    """Class for testing complex shapes."""
+
+    @classmethod
+    def setUpClass(cls):
+        cls.material = resolveMaterialClassByName("HT9")()
+
+    @staticmethod
+    def circArea(d):
+        return math.pi * (d / 2) ** 2
+
+    @staticmethod
+    def hexArea(op):
+        return math.sqrt(3.0) / 2.0 * op**2
+
+    @staticmethod
+    def rectArea(l, w):
+        return l * w
+
+    def test_holedHexagon(self):
+        op = 2.0
+        holeOD = 0.5
+        nHoles = 2
+        comp = HoledHexagon(
+            "Test",
+            material=self.material,
+            Tinput=20,
+            Thot=300,
+            op=op,
+            holeOD=holeOD,
+            nHoles=nHoles,
+            mult=2,
+        )
+
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=True),
+            (self.hexArea(op) - nHoles * self.circArea(holeOD)) * 2,
+        )
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=True), comp.getComponentArea(Tc=20.0)
+        )
+
+        opHot = comp.getDimension("op")
+        holeODHot = comp.getDimension("holeOD")
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=False),
+            (self.hexArea(opHot) - nHoles * self.circArea(holeODHot)) * 2,
+        )
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=False), comp.getComponentArea(Tc=300)
+        )
+
+    def test_holedRectangle(self):
+        lo = 2.0
+        wo = 3.0
+        holeOD = 0.5
+        comp = HoledRectangle(
+            "Test",
+            material=self.material,
+            Tinput=20,
+            Thot=300,
+            lengthOuter=lo,
+            widthOuter=wo,
+            holeOD=holeOD,
+            mult=2,
+        )
+
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=True),
+            (self.rectArea(lo, wo) - self.circArea(holeOD)) * 2,
+        )
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=True), comp.getComponentArea(Tc=20.0)
+        )
+
+        loHot = comp.getDimension("lengthOuter")
+        woHot = comp.getDimension("widthOuter")
+        holeODHot = comp.getDimension("holeOD")
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=False),
+            (self.rectArea(loHot, woHot) - self.circArea(holeODHot)) * 2,
+        )
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=False), comp.getComponentArea(Tc=300)
+        )
+
+    def test_holedSquare(self):
+        wo = 3.0
+        holeOD = 0.5
+        comp = HoledSquare(
+            "Test",
+            material=self.material,
+            Tinput=20,
+            Thot=300,
+            widthOuter=wo,
+            holeOD=holeOD,
+            mult=2,
+        )
+
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=True),
+            (self.rectArea(wo, wo) - self.circArea(holeOD)) * 2,
+        )
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=True), comp.getComponentArea(Tc=20.0)
+        )
+
+        woHot = comp.getDimension("widthOuter")
+        holeODHot = comp.getDimension("holeOD")
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=False),
+            (self.rectArea(woHot, woHot) - self.circArea(holeODHot)) * 2,
+        )
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=False), comp.getComponentArea(Tc=300)
+        )
+
+    def test_hexHoledCircle(self):
+        od = 3.0
+        holeOP = 0.5
+        comp = HexHoledCircle(
+            "Test",
+            material=self.material,
+            Tinput=20,
+            Thot=300,
+            od=od,
+            holeOP=holeOP,
+            mult=2,
+        )
+
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=True),
+            (self.circArea(od) - self.hexArea(holeOP)) * 2,
+        )
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=True), comp.getComponentArea(Tc=20.0)
+        )
+
+        odHot = comp.getDimension("od")
+        holeOPHot = comp.getDimension("holeOP")
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=False),
+            (self.circArea(odHot) - self.hexArea(holeOPHot)) * 2,
+        )
+        self.assertAlmostEqual(
+            comp.getComponentArea(cold=False), comp.getComponentArea(Tc=300)
+        )
diff --git a/armi/reactor/tests/test_components.py b/armi/reactor/tests/test_components.py
index 83746210..e1645472 100644
--- a/armi/reactor/tests/test_components.py
+++ b/armi/reactor/tests/test_components.py
@@ -368,6 +368,12 @@ class TestUnshapedComponent(TestGeneralComponents):
             ** 2,
         )
 
+        # Passing temperature directly
+        self.assertEqual(
+            self.component.getComponentArea(cold=False),
+            self.component.getComponentArea(Tc=self.component.temperatureInC),
+        )
+
         # show that area expansion is consistent with the density change in the material
         hotDensity = self.component.density()
         hotArea = self.component.getArea()
@@ -587,6 +593,29 @@ class TestDerivedShapeGetArea(unittest.TestCase):
         totalAreaHot = sum([c.getArea(cold=False) for c in b])
         self.assertAlmostEqual(totalAreaCold, totalAreaHot, delta=1e-10)
 
+    def test_getAreaTemp(self):
+        """Prove that the DerivedShape.getArea() works for an arbitrary temperature."""
+        # load one-block test reactor
+        _o, r = loadTestReactor(
+            inputFileName="smallestTestReactor/armiRunSmallest.yaml"
+        )
+        b = r.core[0][0]
+        b.clearCache()
+
+        # 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)
+
+        blockArea = b.getMaxArea()
+        compArea = sum([c.getArea(Tc=300) for c in b if type(c) != DerivedShape])
+
+        comp = [c for c in b if type(c) == DerivedShape][0]
+
+        self.assertAlmostEqual(blockArea - compArea, comp.getComponentArea(Tc=300))
+
 
 class TestComponentSort(unittest.TestCase):
     def setUp(self):