Repository: terrapower/armi

Issue:
Fix wetted perimeter for hexagonal inner ducts
In `HexBlock.getWettedPerimeter()`, we assume that `Flags.INNER` `Flags.DUCT`s are circular:

https://github.com/terrapower/armi/blob/d0f2ef81401c635328e72467ae5ab9eea9000e12/armi/reactor/blocks.py#L2494-L2495

But they could be hexagonal.

So we could add another component type variable like

```python
wettedHollowHexagonComponentsInner = (Flags.INNER | Flags.DUCT | Flags.SECONDARY)
```

with a wetted perimeter of:

```python
6 * (ip + op) / sqrt(3)
```

We could also change:

```python
wettedHollowCircleComponentFlags = (Flags.INNER | Flags.DUCT | Flags.PRIMARY)
```

At least potentially. Or we could try to get the geometries/shapes of the component, and directly check if it is hexagonal or circular.

Discussion:

Patch:
diff --git a/armi/_bootstrap.py b/armi/_bootstrap.py
index 9a568fd0..e00317b7 100644
--- a/armi/_bootstrap.py
+++ b/armi/_bootstrap.py
@@ -13,21 +13,6 @@
 # limitations under the License.
 
 """Code that needs to be executed before most ARMI components are safe to import."""
-
-import sys
-
-# This is a courtesy, to help people who accidently run ARMI with an old version of Python.
-if (
-    sys.version_info.major < 3
-    or sys.version_info.major == 3
-    and sys.version_info.minor < 7
-):
-    raise RuntimeError(
-        "ARMI highly recommends using Python 3.9 or 3.11. Are you sure you are using the "
-        f"correct interpreter?\nYou are using: {sys.executable}"
-    )
-
-
 from armi.nucDirectory import nuclideBases  # noqa: E402
 
 # Nuclide bases get built explicitly here to have better determinism
diff --git a/armi/bookkeeping/historyTracker.py b/armi/bookkeeping/historyTracker.py
index c25f6eb0..995bbb5e 100644
--- a/armi/bookkeeping/historyTracker.py
+++ b/armi/bookkeeping/historyTracker.py
@@ -139,9 +139,6 @@ class HistoryTrackerInterface(interfaces.Interface):
         self.xsHistory = {}
         self._preloadedBlockHistory = None
 
-        msg = "The HistoryTrackerInterface is deprecated, and will be removed."
-        runLog.warning(msg)
-
     def interactBOL(self):
         self.addDetailAssembliesBOL()
 
diff --git a/armi/reactor/assemblies.py b/armi/reactor/assemblies.py
index 28574e17..49395faf 100644
--- a/armi/reactor/assemblies.py
+++ b/armi/reactor/assemblies.py
@@ -1219,9 +1219,8 @@ class Assembly(composites.Composite):
 
         Parameters
         ----------
-        rad: float
+        rad : float
             number (in radians) specifying the angle of counter clockwise rotation
-
         """
         for b in self:
             b.rotate(rad)
@@ -1230,6 +1229,21 @@ class Assembly(composites.Composite):
         grid = self.parent.spatialGrid
         return grid.overlapsWhichSymmetryLine(self.spatialLocator.getCompleteIndices())
 
+    def orientBlocks(self, parentSpatialGrid):
+        """Add special grids to the blocks inside this Assembly, respecting their orientation.
+
+        Parameters
+        ----------
+        parentSpatialGrid : Grid
+            Spatial Grid of the parent of this Assembly (probably a system-level grid).
+        """
+        for b in self:
+            if b.spatialGrid is None:
+                try:
+                    b.autoCreateSpatialGrids(parentSpatialGrid)
+                except (ValueError, NotImplementedError) as e:
+                    runLog.warning(str(e), single=True)
+
 
 class HexAssembly(Assembly):
     """An assembly that is hexagonal in cross-section."""
diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py
index 10dc8b57..55613edd 100644
--- a/armi/reactor/blocks.py
+++ b/armi/reactor/blocks.py
@@ -293,14 +293,19 @@ class Block(composites.Composite):
 
         return smearDensity
 
-    def autoCreateSpatialGrids(self):
+    def autoCreateSpatialGrids(self, systemSpatialGrid=None):
         """
         Creates a spatialGrid for a Block.
 
-        Blocks do not always have a spatialGrid from Blueprints, but, some Blocks can have their
-        spatialGrids inferred based on the multiplicty of their components.
-        This would add the ability to create a spatialGrid for a Block and give its children
-        the corresponding spatialLocators if certain conditions are met.
+        Blocks do not always have a spatialGrid from Blueprints, but some Blocks can have their
+        spatialGrids inferred based on the multiplicty of their components. This would add the
+        ability to create a spatialGrid for a Block and give its children the corresponding
+        spatialLocators if certain conditions are met.
+
+        Parameters
+        ----------
+        systemSpatialGrid : Grid, optional
+            Spatial Grid of the system-level parent of this Assembly that contains this Block.
 
         Raises
         ------
@@ -308,7 +313,8 @@ class Block(composites.Composite):
             If the multiplicities of the block are not only 1 or N or if generated ringNumber leads
             to more positions than necessary.
         """
-        raise NotImplementedError()
+        if self.spatialGrid is None:
+            self.spatialGrid = systemSpatialGrid
 
     def getMgFlux(self, adjoint=False, average=False, volume=None, gamma=False):
         """
@@ -2316,7 +2322,7 @@ class HexBlock(Block):
                     return 2.0
         return 1.0
 
-    def autoCreateSpatialGrids(self):
+    def autoCreateSpatialGrids(self, systemSpatialGrid=None):
         """
         Given a block without a spatialGrid, create a spatialGrid and give its children the
         corresponding spatialLocators (if it is a simple block).
@@ -2325,12 +2331,19 @@ class HexBlock(Block):
         to 1 or N but no other multiplicities. Also, this should only happen when N fits exactly
         into a given number of hex rings.  Otherwise, do not create a grid for this block.
 
+        Parameters
+        ----------
+        systemSpatialGrid : Grid, optional
+            Spatial Grid of the system-level parent of this Assembly that contains this Block.
+
         Notes
         -----
-        If the Block meets all the conditions, we gather all components to either be a
-        multiIndexLocation containing all of the pin positions, or the locator is the center (0,0).
+        When a hex grid has another hex grid nested inside it, the nested grid has the opposite
+        orientation (corners vs flats up). This method takes care of that.
 
-        Also, this only works on blocks that have 'flat side up'.
+        If components inside this block are multiplicity 1, they get a single locator at the center
+        of the grid cell. If the multiplicity is greater than 1, all the components are added to a
+        multiIndexLocation on the hex grid.
 
         Raises
         ------
@@ -2338,7 +2351,7 @@ class HexBlock(Block):
             If the multiplicities of the block are not only 1 or N or if generated ringNumber leads
             to more positions than necessary.
         """
-        # Check multiplicities...
+        # Check multiplicities
         mults = {c.getDimension("mult") for c in self.iterComponents()}
 
         if len(mults) != 2 or 1 not in mults:
@@ -2348,29 +2361,39 @@ class HexBlock(Block):
                 )
             )
 
-        ringNumber = hexagon.numRingsToHoldNumCells(self.getNumPins())
-        # For the below to work, there must not be multiple wire or multiple clad types.
-        # note that it's the pointed end of the cell hexes that are up (but the
-        # macro shape of the pins forms a hex with a flat top fitting in the assembly)
+        # build the grid, from pitch and orientation
+        if isinstance(systemSpatialGrid, grids.HexGrid):
+            cornersUp = not systemSpatialGrid.cornersUp
+        else:
+            cornersUp = False
+
         grid = grids.HexGrid.fromPitch(
-            self.getPinPitch(cold=True), numRings=0, cornersUp=True
+            self.getPinPitch(cold=True),
+            numRings=0,
+            armiObject=self,
+            cornersUp=cornersUp,
         )
-        spatialLocators = grids.MultiIndexLocation(grid=self.spatialGrid)
+
+        ringNumber = hexagon.numRingsToHoldNumCells(self.getNumPins())
         numLocations = 0
         for ring in range(ringNumber):
             numLocations = numLocations + hexagon.numPositionsInRing(ring + 1)
+
         if numLocations != self.getNumPins():
             raise ValueError(
-                "Cannot create spatialGrid, number of locations in rings{} not equal to pin number{}".format(
+                "Cannot create spatialGrid, number of locations in rings {} not equal to pin number {}".format(
                     numLocations, self.getNumPins()
                 )
             )
 
-        i = 0
+        # set the spatial position of the sub-block components
+        spatialLocators = grids.MultiIndexLocation(grid=grid)
         for ring in range(ringNumber):
             for pos in range(grid.getPositionsInRing(ring + 1)):
                 i, j = grid.getIndicesFromRingAndPos(ring + 1, pos + 1)
                 spatialLocators.append(grid[i, j, 0])
+
+        # finally, fill the spatial grid, and put the sub-block components on it
         if self.spatialGrid is None:
             self.spatialGrid = grid
             for c in self:
@@ -2486,13 +2509,10 @@ class HexBlock(Block):
         )
 
         # flags pertaining to circular pin components where the exterior of the circle is wetted
-        wettedPinComponentFlags = (
-            Flags.CLAD,
-            Flags.WIRE,
-        )
+        wettedPinComponentFlags = (Flags.CLAD, Flags.WIRE)
 
-        # flags pertaining to circular components where both the interior and exterior of the circle are wetted
-        wettedHollowCircleComponentFlags = (Flags.DUCT | Flags.INNER,)
+        # flags pertaining to components where both the interior and exterior are wetted
+        wettedHollowComponentFlags = (Flags.DUCT | Flags.INNER,)
 
         # obtain all wetted components based on type
         wettedHollowHexagonComponents = []
@@ -2506,9 +2526,13 @@ class HexBlock(Block):
             wettedPinComponents.append(c) if c else None
 
         wettedHollowCircleComponents = []
-        for flag in wettedHollowCircleComponentFlags:
+        wettedHollowHexComponents = []
+        for flag in wettedHollowComponentFlags:
             c = self.getComponent(flag, exact=True)
-            wettedHollowCircleComponents.append(c) if c else None
+            if isinstance(c, Hexagon):
+                wettedHollowHexComponents.append(c) if c else None
+            else:
+                wettedHollowCircleComponents.append(c) if c else None
 
         # calculate wetted perimeters according to their geometries
 
@@ -2542,10 +2566,19 @@ class HexBlock(Block):
             )
         wettedHollowCirclePerimeter *= math.pi
 
+        # hollow hexagon = 6 * (ip + op) / sqrt(3)
+        wettedHollowHexPerimeter = 0.0
+        for c in wettedHollowHexComponents:
+            wettedHollowHexPerimeter += (
+                c.getDimension("ip") + c.getDimension("op") if c else 0.0
+            )
+        wettedHollowHexPerimeter *= 6 / math.sqrt(3)
+
         return (
             wettedHollowHexagonPerimeter
             + wettedPinPerimeter
             + wettedHollowCirclePerimeter
+            + wettedHollowHexPerimeter
         )
 
     def getFlowArea(self):
diff --git a/armi/reactor/blueprints/blockBlueprint.py b/armi/reactor/blueprints/blockBlueprint.py
index f233fd58..692ae84a 100644
--- a/armi/reactor/blueprints/blockBlueprint.py
+++ b/armi/reactor/blueprints/blockBlueprint.py
@@ -22,13 +22,12 @@ import yamlize
 from armi import getPluginManagerOrFail, runLog
 from armi.materials.material import Material
 from armi.reactor import blocks
-from armi.reactor.composites import Composite
 from armi.reactor import parameters
-from armi.reactor.flags import Flags
 from armi.reactor.blueprints import componentBlueprint
 from armi.reactor.components.component import Component
+from armi.reactor.composites import Composite
 from armi.reactor.converters import blockConverters
-from armi.settings.fwSettings import globalSettings
+from armi.reactor.flags import Flags
 
 
 def _configureGeomOptions():
@@ -240,11 +239,6 @@ class BlockBlueprint(yamlize.KeyedList):
         b.verifyBlockDims()
         b.spatialGrid = spatialGrid
 
-        if b.spatialGrid is None and cs[globalSettings.CONF_BLOCK_AUTO_GRID]:
-            try:
-                b.autoCreateSpatialGrids()
-            except (ValueError, NotImplementedError) as e:
-                runLog.warning(str(e), single=True)
         return b
 
     def _getBlockwiseMaterialModifierOptions(
diff --git a/armi/reactor/cores.py b/armi/reactor/cores.py
index e8422514..00ce8d47 100644
--- a/armi/reactor/cores.py
+++ b/armi/reactor/cores.py
@@ -563,10 +563,12 @@ class Core(composites.Composite):
                 "".format(aName, self.assembliesByName[aName], a, self.r.p.maxAssemNum)
             )
             raise RuntimeError("Core already contains an assembly with the same name.")
+
         self.assembliesByName[aName] = a
         for b in a:
             self.blocksByName[b.getName()] = b
 
+        a.orientBlocks(parentSpatialGrid=self.spatialGrid)
         if self.geomType == geometry.GeomType.HEX:
             ring, _loc = self.spatialGrid.getRingPos(
                 a.spatialLocator.getCompleteIndices()
diff --git a/armi/reactor/grids/locations.py b/armi/reactor/grids/locations.py
index 03babf51..1d0320ee 100644
--- a/armi/reactor/grids/locations.py
+++ b/armi/reactor/grids/locations.py
@@ -289,7 +289,7 @@ class IndexLocation(LocationBase):
         """Return the coordinates of the center of the mesh cell here in cm."""
         if self.grid is None:
             raise ValueError(
-                "Cannot get local coordinates of {} because grid is None.".format(self)
+                f"Cannot get local coordinates of {self} because grid is None."
             )
         return self.grid.getCoordinates(self.indices, nativeCoords=nativeCoords)
 
diff --git a/armi/reactor/grids/structuredGrid.py b/armi/reactor/grids/structuredGrid.py
index 437bb5b6..07388338 100644
--- a/armi/reactor/grids/structuredGrid.py
+++ b/armi/reactor/grids/structuredGrid.py
@@ -17,13 +17,13 @@ from typing import Tuple, Union, List, Iterable, Optional, Sequence
 
 import numpy as np
 
+from armi.reactor.grids.grid import Grid
 from armi.reactor.grids.locations import (
     IJKType,
-    LocationBase,
     IndexLocation,
+    LocationBase,
     MultiIndexLocation,
 )
-from armi.reactor.grids.grid import Grid
 
 # data structure for database-serialization of grids
 GridParameters = collections.namedtuple(
diff --git a/armi/reactor/spentFuelPool.py b/armi/reactor/spentFuelPool.py
index 7ca9e3e8..666c646b 100644
--- a/armi/reactor/spentFuelPool.py
+++ b/armi/reactor/spentFuelPool.py
@@ -41,7 +41,7 @@ class SpentFuelPool(ExcoreStructure):
         Parameters
         ----------
         assem : Assembly
-            The Assembly to add to the list
+            The Assembly to add to the spent fuel pool
         loc : LocationBase, optional
             If provided, the assembly is inserted at this location.
             If it is not provided, the locator on the Assembly object will be used.
@@ -70,6 +70,9 @@ class SpentFuelPool(ExcoreStructure):
         else:
             loc = self._getNextLocation()
 
+        # orient the blocks to match this grid
+        assem.orientBlocks(parentSpatialGrid=self.spatialGrid)
+
         super().add(assem, loc)
 
     def getAssembly(self, name):
diff --git a/armi/settings/fwSettings/globalSettings.py b/armi/settings/fwSettings/globalSettings.py
index 43eb9209..cf9ed1dc 100644
--- a/armi/settings/fwSettings/globalSettings.py
+++ b/armi/settings/fwSettings/globalSettings.py
@@ -38,7 +38,6 @@ CONF_AVAILABILITY_FACTOR = "availabilityFactor"
 CONF_AVAILABILITY_FACTORS = "availabilityFactors"
 CONF_AXIAL_MESH_REFINEMENT_FACTOR = "axialMeshRefinementFactor"
 CONF_BETA = "beta"
-CONF_BLOCK_AUTO_GRID = "autoGenerateBlockGrids"
 CONF_BRANCH_VERBOSITY = "branchVerbosity"
 CONF_BU_GROUPS = "buGroups"
 CONF_BURN_CHAIN_FILE_NAME = "burnChainFileName"
@@ -820,15 +819,6 @@ def defineSettings() -> List[setting.Setting]:
                 "than from the framework."
             ),
         ),
-        # It may make sense to remove this setting when MILs become more stable.
-        setting.Setting(
-            CONF_BLOCK_AUTO_GRID,
-            default=True,
-            label="Auto-generate Block grids",
-            description="Should block blueprints attempt to auto-generate a spatial "
-            "grid upon construction? This feature makes heavy use of multi-index "
-            "locations, which are not yet universally supported.",
-        ),
         setting.Setting(
             CONF_CYCLES,
             default=[],
diff --git a/doc/release/0.4.rst b/doc/release/0.4.rst
index 770521b5..b6917e9b 100644
--- a/doc/release/0.4.rst
+++ b/doc/release/0.4.rst
@@ -41,6 +41,7 @@ API Changes
 #. Removing ``Assembly.doubleResolution()``. (`PR#1951 <https://github.com/terrapower/armi/pull/1951>`_)
 #. Removing ``assemblyLists.py`` and the ``AssemblyList`` class. (`PR#1891 <https://github.com/terrapower/armi/pull/1891>`_)
 #. Removing ``Assembly.rotatePins`` and ``Block.rotatePins``. Prefer ``Assembly.rotate`` and ``Block.rotate``. (`PR#1846 <https://github.com/terrapower/armi/1846>`_)
+#. Removing unused setting ``autoGenerateBlockGrids``. (`PR#1947 <https://github.com/terrapower/armi/pull/1947>`_)
 #. Transposing ``pinMgFluxes`` parameters so that leading dimension is pin index. (`PR#1937 <https://github.com/terrapower/armi/pull/1937>`_)
 #. ``Block.getPinCoordinates`` returns an ``(N, 3)`` array, rather than a length ``N`` list of three-length arrays. (`PR#1943 <https://github.com/terrapower/armi/pull/1943>`_)
 #. Removing ``globalFluxInterface.DoseResultsMapper`` class. (`PR#1952 <https://github.com/terrapower/armi/pull/1952>`_)
@@ -50,6 +51,7 @@ API Changes
 
 Bug Fixes
 ---------
+#. Fixed spatial grids of pins in Blocks on flats-up grids. (`PR#1947 <https://github.com/terrapower/armi/pull/1947>`_)
 #. 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>`_)
 #. Fixed ``PermissionError`` when using ``syncDbAfterWrite``. (`PR#1857 <https://github.com/terrapower/armi/pull/1857>`_)
@@ -60,14 +62,15 @@ Bug Fixes
 #. Material theoretical density is serialized to and read from database. (`PR#1852 <https://github.com/terrapower/armi/pull/1852>`_)
 #. Removed broken and unused column in ``summarizeMaterialData``. (`PR#1925 <https://github.com/terrapower/armi/pull/1925>`_)
 #. Fixed edge case in ``assemblyBlueprint._checkParamConsistency()``. (`PR#1928 <https://github.com/terrapower/armi/pull/1928>`_)
+#. Fixed wetted perimeter for hex inner ducts. (`PR#1985 <https://github.com/terrapower/armi/pull/1985>`_)
 #. TBD
 
 Quality Work
 ------------
 #. Removing deprecated code ``axialUnitGrid``. (`PR#1809 <https://github.com/terrapower/armi/pull/1809>`_)
 #. Refactoring ``axialExpansionChanger``. (`PR#1861 <https://github.com/terrapower/armi/pull/1861>`_)
+#. Raising a ValueError when database load fails. (`PR#1940 <https://github.com/terrapower/armi/pull/1940>`_)
 #. Changes to make axial expansion related classes more extensible. (`PR#1920 <https://github.com/terrapower/armi/pull/1920>`_)
-#. Raise a ValueError when database load fails. (`PR#1940 <https://github.com/terrapower/armi/pull/1940>`_)
 #. TBD
 
 Changes that Affect Requirements


Test patch:
diff --git a/armi/reactor/grids/tests/test_grids.py b/armi/reactor/grids/tests/test_grids.py
index 24cc6ccc..79502922 100644
--- a/armi/reactor/grids/tests/test_grids.py
+++ b/armi/reactor/grids/tests/test_grids.py
@@ -14,17 +14,17 @@
 
 """Tests for grids."""
 from io import BytesIO
-import math
 from random import randint
-import unittest
+import math
 import pickle
+import unittest
 
 import numpy as np
 from numpy.testing import assert_allclose, assert_array_equal
 
-from armi.utils import hexagon
 from armi.reactor import geometry
 from armi.reactor import grids
+from armi.utils import hexagon
 
 
 class MockLocator(grids.IndexLocation):
@@ -240,8 +240,9 @@ class TestGrid(unittest.TestCase):
 class TestHexGrid(unittest.TestCase):
     """A set of tests for the Hexagonal Grid."""
 
-    def test_positions(self):
-        grid = grids.HexGrid.fromPitch(1.0)
+    def test_getCoordinatesFlatsUp(self):
+        """Test getCoordinates() for flats up hex grids."""
+        grid = grids.HexGrid.fromPitch(1.0, cornersUp=False)
         self.assertAlmostEqual(grid.pitch, 1.0)
         side = 1.0 / math.sqrt(3)
         assert_allclose(grid.getCoordinates((0, 0, 0)), (0.0, 0.0, 0.0))
@@ -263,6 +264,88 @@ class TestHexGrid(unittest.TestCase):
         assert_allclose(grid.getCoordinates((1, 0, 0)), iDirection)
         assert_allclose(grid.getCoordinates((0, 1, 0)), jDirection)
 
+    def test_getCoordinatesCornersUp(self):
+        """Test getCoordinates() for corners up hex grids."""
+        grid = grids.HexGrid.fromPitch(1.0, cornersUp=True)
+        self.assertAlmostEqual(grid.pitch, 1.0)
+        side = 1.0 / math.sqrt(3)
+        assert_allclose(grid.getCoordinates((0, 0, 0)), (0.0, 0.0, 0.0))
+        assert_allclose(grid.getCoordinates((1, 0, 0)), (0.5, 1.5 * side, 0.0))
+        assert_allclose(grid.getCoordinates((-1, 0, 0)), (-0.5, -1.5 * side, 0.0))
+        assert_allclose(grid.getCoordinates((0, 1, 0)), (-0.5, 1.5 * side, 0.0))
+        assert_allclose(grid.getCoordinates((1, -1, 0)), (1, 0.0, 0.0))
+
+        unitSteps = grid.reduce()[0]
+        iDirection = tuple(direction[0] for direction in unitSteps)
+        jDirection = tuple(direction[1] for direction in unitSteps)
+        for directionVector in (iDirection, jDirection):
+            self.assertAlmostEqual(
+                (sum(val**2 for val in directionVector)) ** 0.5,
+                1.0,
+                msg=f"Direction vector {directionVector} should have "
+                "magnitude 1 for pitch 1.",
+            )
+        assert_allclose(grid.getCoordinates((1, 0, 0)), iDirection)
+        assert_allclose(grid.getCoordinates((0, 1, 0)), jDirection)
+
+    def test_getLocalCoordinatesHex(self):
+        """Test getLocalCoordinates() is different for corners up vs flats up hex grids."""
+        grid0 = grids.HexGrid.fromPitch(1.0, cornersUp=True)
+        grid1 = grids.HexGrid.fromPitch(1.0, cornersUp=False)
+        for i in range(3):
+            for j in range(3):
+                if i == 0 and j == 0:
+                    continue
+                coords0 = grid0[i, j, 0].getLocalCoordinates()
+                coords1 = grid1[i, j, 0].getLocalCoordinates()
+                self.assertNotEqual(coords0[0], coords1[0], msg=f"X @ ({i}, {j})")
+                self.assertNotEqual(coords0[1], coords1[1], msg=f"Y @ ({i}, {j})")
+                self.assertEqual(coords0[2], coords1[2], msg=f"Z @ ({i}, {j})")
+
+    def test_getLocalCoordinatesCornersUp(self):
+        """Test getLocalCoordinates() for corners up hex grids."""
+        # validate the first ring of a corners-up hex grid
+        grid = grids.HexGrid.fromPitch(1.0, cornersUp=True)
+        vals = []
+        for pos in range(grid.getPositionsInRing(2)):
+            i, j = grid.getIndicesFromRingAndPos(2, pos + 1)
+            vals.append(grid[i, j, 0].getLocalCoordinates())
+
+        # short in Y
+        maxY = max(v[1] for v in vals)
+        minY = min(v[1] for v in vals)
+        val = math.sqrt(3) / 2
+        self.assertAlmostEqual(maxY, val, delta=0.0001)
+        self.assertAlmostEqual(minY, -val, delta=0.0001)
+
+        # long in X
+        maxX = max(v[0] for v in vals)
+        minX = min(v[0] for v in vals)
+        self.assertAlmostEqual(maxX, 1)
+        self.assertAlmostEqual(minX, -1)
+
+    def test_getLocalCoordinatesFlatsUp(self):
+        """Test getLocalCoordinates() for flats up hex grids."""
+        # validate the first ring of a flats-up hex grid
+        grid = grids.HexGrid.fromPitch(1.0, cornersUp=False)
+        vals = []
+        for pos in range(grid.getPositionsInRing(2)):
+            i, j = grid.getIndicesFromRingAndPos(2, pos + 1)
+            vals.append(grid[i, j, 0].getLocalCoordinates())
+
+        # long in Y
+        maxY = max(v[1] for v in vals)
+        minY = min(v[1] for v in vals)
+        self.assertAlmostEqual(maxY, 1)
+        self.assertAlmostEqual(minY, -1)
+
+        # short in X
+        maxX = max(v[0] for v in vals)
+        minX = min(v[0] for v in vals)
+        val = math.sqrt(3) / 2
+        self.assertAlmostEqual(maxX, val, delta=0.0001)
+        self.assertAlmostEqual(minX, -val, delta=0.0001)
+
     def test_neighbors(self):
         grid = grids.HexGrid.fromPitch(1.0)
         neighbs = grid.getNeighboringCellIndices(0, 0, 0)
@@ -935,8 +1018,3 @@ class TestAxialGrid(unittest.TestCase):
             self.assertEqual(x, 0.0)
             self.assertEqual(y, 0.0)
             self.assertEqual(z, count + 0.5)
-
-
-if __name__ == "__main__":
-    # import sys;sys.argv = ["", "TestHexGrid.testPositions"]
-    unittest.main()
diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py
index e78b640f..07f7eb09 100644
--- a/armi/reactor/tests/test_blocks.py
+++ b/armi/reactor/tests/test_blocks.py
@@ -16,7 +16,9 @@ import copy
 import io
 import math
 import os
+import shutil
 import unittest
+from glob import glob
 from unittest.mock import MagicMock, patch
 
 import numpy as np
@@ -34,17 +36,19 @@ from armi.physics.neutronics.settings import (
 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_reactors import loadTestReactor
 from armi.reactor.tests.test_assemblies import makeTestAssembly
-from armi.tests import ISOAA_PATH, TEST_ROOT
+from armi.reactor.tests.test_reactors import loadTestReactor
+from armi.reactor.tests.test_reactors import TEST_ROOT
+from armi.tests import ISOAA_PATH
 from armi.utils import hexagon, units
+from armi.utils.directoryChangers import TemporaryDirectoryChanger
 from armi.utils.units import MOLES_PER_CC_TO_ATOMS_PER_BARN_CM
 
 NUM_PINS_IN_TEST_BLOCK = 217
 
 
 def buildSimpleFuelBlock():
-    """Return a simple block containing fuel, clad, duct, and coolant."""
+    """Return a simple hex block containing fuel, clad, duct, and coolant."""
     b = blocks.HexBlock("fuel", height=10.0)
 
     fuelDims = {"Tinput": 25.0, "Thot": 600, "od": 0.76, "id": 0.00, "mult": 127.0}
@@ -233,7 +237,7 @@ def loadTestBlock(cold=True) -> blocks.HexBlock:
 
     block.setHeight(16.0)
 
-    block.autoCreateSpatialGrids()
+    block.autoCreateSpatialGrids(r.core.spatialGrid)
     assembly.add(block)
     r.core.add(assembly)
     return block
@@ -812,8 +816,7 @@ class Block_TestCase(unittest.TestCase):
         self.assertEqual(block.p.height, refHeight)
 
     def test_getWettedPerimeter(self):
-        cur = self.block.getWettedPerimeter()
-
+        # calculate the reference value
         wire = self.block.getComponent(Flags.WIRE)
         correctionFactor = np.hypot(
             1.0,
@@ -821,16 +824,81 @@ class Block_TestCase(unittest.TestCase):
             * wire.getDimension("helixDiameter")
             / wire.getDimension("axialPitch"),
         )
-        wireDiameter = wire.getDimension("od") * correctionFactor
-
-        ref = math.pi * (
-            self.block.getDim(Flags.CLAD, "od") + wireDiameter
-        ) * self.block.getDim(Flags.CLAD, "mult") + 6 * self.block.getDim(
-            Flags.DUCT, "ip"
-        ) / math.sqrt(
-            3
-        )
+        wireDiam = wire.getDimension("od") * correctionFactor
+
+        ipDim = self.block.getDim(Flags.DUCT, "ip")
+        odDim = self.block.getDim(Flags.CLAD, "od")
+        mult = self.block.getDim(Flags.CLAD, "mult")
+        ref = math.pi * (odDim + wireDiam) * mult + 6 * ipDim / math.sqrt(3)
 
+        # test getWettedPerimeter
+        cur = self.block.getWettedPerimeter()
+        self.assertAlmostEqual(cur, ref)
+
+    def test_getWettedPerimeterCircularInnerDuct(self):
+        """Calculate the wetted perimeter for a HexBlock with circular inner duct."""
+        # build a test block with a Hex inner duct
+        fuelDims = {"Tinput": 400, "Thot": 400, "od": 0.76, "id": 0.00, "mult": 127.0}
+        cladDims = {"Tinput": 400, "Thot": 400, "od": 0.80, "id": 0.77, "mult": 127.0}
+        ductDims = {"Tinput": 400, "Thot": 400, "od": 16, "id": 15.3, "mult": 1.0}
+        intercoolantDims = {
+            "Tinput": 400,
+            "Thot": 400,
+            "od": 17.0,
+            "id": ductDims["od"],
+            "mult": 1.0,
+        }
+
+        fuel = components.Circle("fuel", "UZr", **fuelDims)
+        clad = components.Circle("clad", "HT9", **cladDims)
+        duct = components.Circle("inner duct", "HT9", **ductDims)
+        intercoolant = components.Circle("intercoolant", "Sodium", **intercoolantDims)
+
+        b = blocks.HexBlock("fuel", height=10.0)
+        b.add(fuel)
+        b.add(clad)
+        b.add(duct)
+        b.add(intercoolant)
+
+        # calculate the reference value
+        ref = (ductDims["id"] + ductDims["od"]) * math.pi
+        ref += b.getNumPins() * cladDims["od"] * math.pi
+
+        # test getWettedPerimeter
+        cur = b.getWettedPerimeter()
+        self.assertAlmostEqual(cur, ref)
+
+    def test_getWettedPerimeterHexInnerDuct(self):
+        """Calculate the wetted perimeter for a HexBlock with hexagonal inner duct."""
+        # build a test block with a Hex inner duct
+        fuelDims = {"Tinput": 400, "Thot": 400, "od": 0.76, "id": 0.00, "mult": 127.0}
+        cladDims = {"Tinput": 400, "Thot": 400, "od": 0.80, "id": 0.77, "mult": 127.0}
+        ductDims = {"Tinput": 400, "Thot": 400, "op": 16, "ip": 15.3, "mult": 1.0}
+        intercoolantDims = {
+            "Tinput": 400,
+            "Thot": 400,
+            "op": 17.0,
+            "ip": ductDims["op"],
+            "mult": 1.0,
+        }
+
+        fuel = components.Circle("fuel", "UZr", **fuelDims)
+        clad = components.Circle("clad", "HT9", **cladDims)
+        duct = components.Hexagon("inner duct", "HT9", **ductDims)
+        intercoolant = components.Hexagon("intercoolant", "Sodium", **intercoolantDims)
+
+        b = blocks.HexBlock("fuel", height=10.0)
+        b.add(fuel)
+        b.add(clad)
+        b.add(duct)
+        b.add(intercoolant)
+
+        # calculate the reference value
+        ref = 6 * (ductDims["ip"] + ductDims["op"]) / math.sqrt(3)
+        ref += b.getNumPins() * cladDims["od"] * math.pi
+
+        # test getWettedPerimeter
+        cur = b.getWettedPerimeter()
         self.assertAlmostEqual(cur, ref)
 
     def test_getFlowAreaPerPin(self):
@@ -1910,29 +1978,29 @@ class TestNegativeVolume(unittest.TestCase):
 class HexBlock_TestCase(unittest.TestCase):
     def setUp(self):
         _ = settings.Settings()
-        self.HexBlock = blocks.HexBlock("TestHexBlock")
+        self.hexBlock = blocks.HexBlock("TestHexBlock")
         hexDims = {"Tinput": 273.0, "Thot": 273.0, "op": 70.6, "ip": 70.0, "mult": 1.0}
         self.hexComponent = components.Hexagon("duct", "UZr", **hexDims)
-        self.HexBlock.add(self.hexComponent)
-        self.HexBlock.add(
+        self.hexBlock.add(self.hexComponent)
+        self.hexBlock.add(
             components.Circle(
                 "clad", "HT9", Tinput=273.0, Thot=273.0, od=0.1, mult=169.0
             )
         )
-        self.HexBlock.add(
+        self.hexBlock.add(
             components.Circle(
                 "wire", "HT9", Tinput=273.0, Thot=273.0, od=0.01, mult=169.0
             )
         )
-        self.HexBlock.add(
+        self.hexBlock.add(
             components.DerivedShape("coolant", "Sodium", Tinput=273.0, Thot=273.0)
         )
-        self.HexBlock.autoCreateSpatialGrids()
-        r = tests.getEmptyHexReactor()
+        self.r = tests.getEmptyHexReactor()
+        self.hexBlock.autoCreateSpatialGrids(self.r.core.spatialGrid)
         a = makeTestAssembly(1, 1)
-        a.add(self.HexBlock)
-        loc1 = r.core.spatialGrid[0, 1, 0]
-        r.core.add(a, loc1)
+        a.add(self.hexBlock)
+        loc1 = self.r.core.spatialGrid[0, 1, 0]
+        self.r.core.add(a, loc1)
 
     def test_getArea(self):
         """Test that we can correctly calculate the area of a hexagonal block.
@@ -1970,7 +2038,7 @@ class HexBlock_TestCase(unittest.TestCase):
             :id: T_ARMI_BLOCK_HEX1
             :tests: R_ARMI_BLOCK_HEX
         """
-        pitch_comp_type = self.HexBlock.PITCH_COMPONENT_TYPE[0]
+        pitch_comp_type = self.hexBlock.PITCH_COMPONENT_TYPE[0]
         self.assertEqual(pitch_comp_type.__name__, "Hexagon")
 
     def test_coords(self):
@@ -1981,17 +2049,17 @@ class HexBlock_TestCase(unittest.TestCase):
             :id: T_ARMI_BLOCK_POSI1
             :tests: R_ARMI_BLOCK_POSI
         """
-        core = self.HexBlock.core
-        a = self.HexBlock.parent
+        core = self.hexBlock.core
+        a = self.hexBlock.parent
         loc1 = core.spatialGrid[0, 1, 0]
         a.spatialLocator = loc1
-        x0, y0 = self.HexBlock.coords()
+        x0, y0 = self.hexBlock.coords()
         a.spatialLocator = core.spatialGrid[0, -1, 0]  # symmetric
-        x2, y2 = self.HexBlock.coords()
+        x2, y2 = self.hexBlock.coords()
         a.spatialLocator = loc1
-        self.HexBlock.p.displacementX = 0.01
-        self.HexBlock.p.displacementY = 0.02
-        x1, y1 = self.HexBlock.coords()
+        self.hexBlock.p.displacementX = 0.01
+        self.hexBlock.p.displacementY = 0.02
+        x1, y1 = self.hexBlock.coords()
 
         # make sure displacements are working
         self.assertAlmostEqual(x1 - x0, 1.0)
@@ -2002,7 +2070,7 @@ class HexBlock_TestCase(unittest.TestCase):
         self.assertAlmostEqual(y0, -y2)
 
     def test_getNumPins(self):
-        self.assertEqual(self.HexBlock.getNumPins(), 169)
+        self.assertEqual(self.hexBlock.getNumPins(), 169)
 
     def test_block_dims(self):
         """
@@ -2012,55 +2080,56 @@ class HexBlock_TestCase(unittest.TestCase):
             :id: T_ARMI_BLOCK_DIMS
             :tests: R_ARMI_BLOCK_DIMS
         """
-        self.assertAlmostEqual(4316.582, self.HexBlock.getVolume(), 3)
-        self.assertAlmostEqual(70.6, self.HexBlock.getPitch(), 1)
-        self.assertAlmostEqual(4316.582, self.HexBlock.getMaxArea(), 3)
+        self.assertAlmostEqual(4316.582, self.hexBlock.getVolume(), 3)
+        self.assertAlmostEqual(70.6, self.hexBlock.getPitch(), 1)
+        self.assertAlmostEqual(4316.582, self.hexBlock.getMaxArea(), 3)
 
-        self.assertEqual(70, self.HexBlock.getDuctIP())
-        self.assertEqual(70.6, self.HexBlock.getDuctOP())
+        self.assertEqual(70, self.hexBlock.getDuctIP())
+        self.assertEqual(70.6, self.hexBlock.getDuctOP())
 
-        self.assertAlmostEqual(34.273, self.HexBlock.getPinToDuctGap(), 3)
-        self.assertEqual(0.11, self.HexBlock.getPinPitch())
-        self.assertAlmostEqual(300.889, self.HexBlock.getWettedPerimeter(), 3)
-        self.assertAlmostEqual(4242.184, self.HexBlock.getFlowArea(), 3)
-        self.assertAlmostEqual(56.395, self.HexBlock.getHydraulicDiameter(), 3)
+        self.assertAlmostEqual(34.273, self.hexBlock.getPinToDuctGap(), 3)
+        self.assertEqual(0.11, self.hexBlock.getPinPitch())
+        self.assertAlmostEqual(300.889, self.hexBlock.getWettedPerimeter(), 3)
+        self.assertAlmostEqual(4242.184, self.hexBlock.getFlowArea(), 3)
+        self.assertAlmostEqual(56.395, self.hexBlock.getHydraulicDiameter(), 3)
 
     def test_symmetryFactor(self):
         # full hex
-        self.HexBlock.spatialLocator = self.HexBlock.core.spatialGrid[2, 0, 0]
-        self.HexBlock.clearCache()
-        self.assertEqual(1.0, self.HexBlock.getSymmetryFactor())
-        a0 = self.HexBlock.getArea()
-        v0 = self.HexBlock.getVolume()
-        m0 = self.HexBlock.getMass()
+        self.hexBlock.spatialLocator = self.hexBlock.core.spatialGrid[2, 0, 0]
+        self.hexBlock.clearCache()
+        self.assertEqual(1.0, self.hexBlock.getSymmetryFactor())
+        a0 = self.hexBlock.getArea()
+        v0 = self.hexBlock.getVolume()
+        m0 = self.hexBlock.getMass()
 
         # 1/3 symmetric
-        self.HexBlock.spatialLocator = self.HexBlock.core.spatialGrid[0, 0, 0]
-        self.HexBlock.clearCache()
-        self.assertEqual(3.0, self.HexBlock.getSymmetryFactor())
-        self.assertEqual(a0 / 3.0, self.HexBlock.getArea())
-        self.assertEqual(v0 / 3.0, self.HexBlock.getVolume())
-        self.assertAlmostEqual(m0 / 3.0, self.HexBlock.getMass())
+        self.hexBlock.spatialLocator = self.hexBlock.core.spatialGrid[0, 0, 0]
+        self.hexBlock.clearCache()
+        self.assertEqual(3.0, self.hexBlock.getSymmetryFactor())
+        self.assertEqual(a0 / 3.0, self.hexBlock.getArea())
+        self.assertEqual(v0 / 3.0, self.hexBlock.getVolume())
+        self.assertAlmostEqual(m0 / 3.0, self.hexBlock.getMass())
 
     def test_retainState(self):
         """Ensure retainState restores params and spatialGrids."""
-        self.HexBlock.spatialGrid = grids.HexGrid.fromPitch(1.0)
-        self.HexBlock.setType("intercoolant")
-        with self.HexBlock.retainState():
-            self.HexBlock.setType("fuel")
-            self.HexBlock.spatialGrid.changePitch(2.0)
-        self.assertAlmostEqual(self.HexBlock.spatialGrid.pitch, 1.0)
-        self.assertTrue(self.HexBlock.hasFlags(Flags.INTERCOOLANT))
+        self.hexBlock.spatialGrid = grids.HexGrid.fromPitch(1.0)
+        self.hexBlock.setType("intercoolant")
+        with self.hexBlock.retainState():
+            self.hexBlock.setType("fuel")
+            self.hexBlock.spatialGrid.changePitch(2.0)
+        self.assertAlmostEqual(self.hexBlock.spatialGrid.pitch, 1.0)
+        self.assertTrue(self.hexBlock.hasFlags(Flags.INTERCOOLANT))
 
     def test_getPinLocations(self):
         """Test pin locations can be obtained."""
-        locs = set(self.HexBlock.getPinLocations())
-        nPins = self.HexBlock.getNumPins()
+        locs = set(self.hexBlock.getPinLocations())
+        nPins = self.hexBlock.getNumPins()
         self.assertEqual(len(locs), nPins)
         for l in locs:
-            self.assertIs(l.grid, self.HexBlock.spatialGrid)
+            self.assertIs(l.grid, self.hexBlock.spatialGrid)
+
         # Check all clad components are represented
-        for c in self.HexBlock.getChildrenWithFlags(Flags.CLAD):
+        for c in self.hexBlock.getChildrenWithFlags(Flags.CLAD):
             if isinstance(c.spatialLocator, grids.MultiIndexLocation):
                 for l in c.spatialLocator:
                     locs.remove(l)
@@ -2073,25 +2142,26 @@ class HexBlock_TestCase(unittest.TestCase):
 
     def test_getPinCoordsAndLocsAgree(self):
         """Ensure consistency in ordering of pin locations and coordinates."""
-        locs = self.HexBlock.getPinLocations()
-        coords = self.HexBlock.getPinCoordinates()
+        locs = self.hexBlock.getPinLocations()
+        coords = self.hexBlock.getPinCoordinates()
         self.assertEqual(len(locs), len(coords))
         for loc, coord in zip(locs, coords):
             convertedCoords = loc.getLocalCoordinates()
             np.testing.assert_array_equal(coord, convertedCoords, err_msg=f"{loc=}")
 
     def test_getPinCoords(self):
-        blockPitch = self.HexBlock.getPitch()
-        pinPitch = self.HexBlock.getPinPitch()
-        nPins = self.HexBlock.getNumPins()
+        blockPitch = self.hexBlock.getPitch()
+        pinPitch = self.hexBlock.getPinPitch()
+        nPins = self.hexBlock.getNumPins()
         side = hexagon.side(blockPitch)
-        xyz = self.HexBlock.getPinCoordinates()
+        xyz = self.hexBlock.getPinCoordinates()
         x, y, z = xyz.T
-        self.assertAlmostEqual(
-            y[1], y[2]
-        )  # first two pins should be side by side on top.
-        self.assertNotAlmostEqual(x[1], x[2])
-        self.assertEqual(len(xyz), self.HexBlock.getNumPins())
+
+        # these two pins should be side by side
+        self.assertTrue(self.hexBlock.spatialGrid.cornersUp)
+        self.assertAlmostEqual(y[1], y[2])
+        self.assertAlmostEqual(x[1], -x[2])
+        self.assertEqual(len(xyz), self.hexBlock.getNumPins())
 
         # ensure all pins are within the proper bounds of a
         # flats-up oriented hex block
@@ -2189,17 +2259,17 @@ class HexBlock_TestCase(unittest.TestCase):
         self.assertAlmostEqual(sum(c.getArea() for c in hexBlock), hexTotalArea)
 
     def test_getDuctPitch(self):
-        ductIP = self.HexBlock.getDuctIP()
+        ductIP = self.hexBlock.getDuctIP()
         self.assertAlmostEqual(70.0, ductIP)
-        ductOP = self.HexBlock.getDuctOP()
+        ductOP = self.hexBlock.getDuctOP()
         self.assertAlmostEqual(70.6, ductOP)
 
     def test_getPinCenterFlatToFlat(self):
-        nRings = hexagon.numRingsToHoldNumCells(self.HexBlock.getNumPins())
-        pinPitch = self.HexBlock.getPinPitch()
+        nRings = hexagon.numRingsToHoldNumCells(self.hexBlock.getNumPins())
+        pinPitch = self.hexBlock.getPinPitch()
         pinCenterCornerToCorner = 2 * (nRings - 1) * pinPitch
         pinCenterFlatToFlat = math.sqrt(3.0) / 2.0 * pinCenterCornerToCorner
-        f2f = self.HexBlock.getPinCenterFlatToFlat()
+        f2f = self.hexBlock.getPinCenterFlatToFlat()
         self.assertAlmostEqual(pinCenterFlatToFlat, f2f)
 
     def test_gridCreation(self):
@@ -2209,10 +2279,10 @@ class HexBlock_TestCase(unittest.TestCase):
             :id: T_ARMI_GRID_MULT
             :tests: R_ARMI_GRID_MULT
         """
-        b = self.HexBlock
+        b = self.hexBlock
         # The block should have a spatial grid at construction,
         # since it has mults = 1 or 169 from setup
-        b.autoCreateSpatialGrids()
+        b.autoCreateSpatialGrids(self.r.core.spatialGrid)
         self.assertIsNotNone(b.spatialGrid)
         for c in b:
             if c.getDimension("mult", cold=True) == 169:
@@ -2257,7 +2327,7 @@ class HexBlock_TestCase(unittest.TestCase):
         b.add(duct)
         b.add(wire)
         with self.assertRaises(ValueError):
-            b.autoCreateSpatialGrids()
+            b.autoCreateSpatialGrids(self.r.core.spatialGrid)
         self.assertIsNone(b.spatialGrid)
 
     def test_gridNotCreatedMultipleMultiplicities(self):
@@ -2272,12 +2342,96 @@ class HexBlock_TestCase(unittest.TestCase):
         }
         # add a wire only some places in the block, so grid should not be created.
         wire = components.Helix("wire", "HT9", **wireDims)
-        self.HexBlock.add(wire)
-        self.HexBlock.spatialGrid = None  # clear existing
+        self.hexBlock.add(wire)
+        self.hexBlock.spatialGrid = None  # clear existing
         with self.assertRaises(ValueError):
-            self.HexBlock.autoCreateSpatialGrids()
+            self.hexBlock.autoCreateSpatialGrids(self.r.core.spatialGrid)
+
+        self.assertIsNone(self.hexBlock.spatialGrid)
 
-        self.assertIsNone(self.HexBlock.spatialGrid)
+
+class TestHexBlockOrientation(unittest.TestCase):
+    def setUp(self):
+        self.td = TemporaryDirectoryChanger()
+        self.td.__enter__()
+
+    def tearDown(self):
+        self.td.__exit__(None, None, None)
+
+    @staticmethod
+    def getLocalCoordinatesBlockBounds(b: blocks.HexBlock):
+        """Call getLocalCoordinates() for every Component in the Block and find the X/Y bounds."""
+        maxX = -111
+        minX = 999
+        maxY = -111
+        minY = 999
+        for comp in b:
+            locs = comp.spatialLocator
+            if not isinstance(locs, grids.MultiIndexLocation):
+                locs = [locs]
+
+            for loc in locs:
+                x, y, _ = loc.getLocalCoordinates()
+                if x > maxX:
+                    maxX = x
+                elif x < minX:
+                    minX = x
+
+                if y > maxY:
+                    maxY = y
+                elif y < minY:
+                    minY = y
+
+        return minX, maxX, minY, maxY
+
+    def test_validateReactorCornersUp(self):
+        """Validate the spatial grid for a corners up HexBlock and its children."""
+        # load a corners up reactor
+        _o, r = loadTestReactor(
+            os.path.join(TEST_ROOT, "smallestTestReactor"),
+            inputFileName="armiRunSmallest.yaml",
+        )
+
+        # grab a pinned fuel block, and verify it is flats up
+        b = r.core.getFirstBlock(Flags.FUEL)
+        self.assertTrue(r.core.spatialGrid.cornersUp)
+        self.assertFalse(b.spatialGrid.cornersUp)
+        self.assertNotEqual(r.core.spatialGrid.cornersUp, b.spatialGrid.cornersUp)
+
+        # for a flats up block-grid, the hex centroids should stretch more in Y than X
+        minX, maxX, minY, maxY = self.getLocalCoordinatesBlockBounds(b)
+        ratio = (maxY - minY) / (maxX - minX)
+        self.assertAlmostEqual(ratio, 2 / math.sqrt(3), delta=0.0001)
+
+    def test_validateReactorFlatsUp(self):
+        """Validate the spatial grid for a flats up HexBlock and its children."""
+        # copy the files over
+        inDir = os.path.join(TEST_ROOT, "smallestTestReactor")
+        for filePath in glob(os.path.join(inDir, "*.yaml")):
+            outPath = os.path.join(self.td.destination, os.path.basename(filePath))
+            shutil.copyfile(filePath, outPath)
+
+        # modify the reactor to make it flats up
+        testFile = os.path.join(self.td.destination, "refSmallestReactor.yaml")
+        txt = open(testFile, "r").read()
+        txt = txt.replace("geom: hex_corners_up", "geom: hex")
+        open(testFile, "w").write(txt)
+
+        # load a flats up reactor
+        _o, r = loadTestReactor(
+            self.td.destination, inputFileName="armiRunSmallest.yaml"
+        )
+
+        # grab a pinned fuel block, and verify it is corners up
+        b = r.core.getFirstBlock(Flags.FUEL)
+        self.assertFalse(r.core.spatialGrid.cornersUp)
+        self.assertTrue(b.spatialGrid.cornersUp)
+        self.assertNotEqual(r.core.spatialGrid.cornersUp, b.spatialGrid.cornersUp)
+
+        # for a corners up block-grid, the hex centroids should stretch more in X than Y
+        minX, maxX, minY, maxY = self.getLocalCoordinatesBlockBounds(b)
+        ratio = (maxX - minX) / (maxY - minY)
+        self.assertAlmostEqual(ratio, 2 / math.sqrt(3), delta=0.0001)
 
 
 class ThRZBlock_TestCase(unittest.TestCase):
@@ -2389,8 +2543,9 @@ class ThRZBlock_TestCase(unittest.TestCase):
     def test_getThetaRZGrid(self):
         """Since not applicable to ThetaRZ Grids."""
         b = self.ThRZBlock
-        with self.assertRaises(NotImplementedError):
-            b.autoCreateSpatialGrids()
+        self.assertIsNone(b.spatialGrid)
+        b.autoCreateSpatialGrids("FakeSpatilGrid")
+        self.assertIsNotNone(b.spatialGrid)
 
     def test_getWettedPerimeter(self):
         with self.assertRaises(NotImplementedError):
@@ -2493,8 +2648,9 @@ class CartesianBlock_TestCase(unittest.TestCase):
     def test_getCartesianGrid(self):
         """Since not applicable to Cartesian Grids."""
         b = self.cartesianBlock
-        with self.assertRaises(NotImplementedError):
-            b.autoCreateSpatialGrids()
+        self.assertIsNone(b.spatialGrid)
+        b.autoCreateSpatialGrids("FakeSpatialGrid")
+        self.assertIsNotNone(b.spatialGrid)
 
     def test_getWettedPerimeter(self):
         with self.assertRaises(NotImplementedError):
diff --git a/armi/utils/tests/test_plotting.py b/armi/utils/tests/test_plotting.py
index 2c3572cd..f9fe9f0f 100644
--- a/armi/utils/tests/test_plotting.py
+++ b/armi/utils/tests/test_plotting.py
@@ -106,7 +106,7 @@ class TestPlotting(unittest.TestCase):
     def test_plotHexBlock(self):
         with TemporaryDirectoryChanger():
             first_fuel_block = self.r.core.getFirstBlock(Flags.FUEL)
-            first_fuel_block.autoCreateSpatialGrids()
+            first_fuel_block.autoCreateSpatialGrids(self.r.core.spatialGrid)
             plotting.plotBlockDiagram(first_fuel_block, "blockDiagram23.svg", True)
             self.assertTrue(os.path.exists("blockDiagram23.svg"))