Repository: terrapower/armi
Pull request #2167 (2025-06-26 15:48:22)

Remove unused test code
## What is the change? Why is it being made?

I found myself searching for the `bondBOL` parameter and found it in this test file. This is an unknown thing to ARMI and I found that this entire class variable is totally unused. This PR removes it. 

## 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: Cleaning out unused test code.

<!-- 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`.



Patch:
diff --git a/armi/reactor/assemblies.py b/armi/reactor/assemblies.py
index fa8cee69..4c53c963 100644
--- a/armi/reactor/assemblies.py
+++ b/armi/reactor/assemblies.py
@@ -1195,6 +1195,12 @@ class Assembly(composites.Composite):
         rad : float
             number (in radians) specifying the angle of counter clockwise rotation
         """
+        # handle the orientation parameter
+        if self.p.orientation is None:
+            self.p.orientation = np.array([0.0, 0.0, 0.0])
+
+        self.p.orientation[2] += math.degrees(rad)
+
         for b in self:
             b.rotate(rad)
 
@@ -1230,9 +1236,9 @@ class HexAssembly(Assembly):
             :id: I_ARMI_ROTATE_HEX_ASSEM
             :implements: R_ARMI_ROTATE_HEX
 
-            This method loops through every ``Block`` in this ``HexAssembly`` and rotates
-            it by a given angle (in radians). The rotation angle is positive in the
-            counter-clockwise direction. To perform the ``Block`` rotation, the
+            This method loops through every ``Block`` in this ``HexAssembly`` and rotates it by a
+            given angle (in radians). The rotation angle is positive in the counter-clockwise
+            direction. To perform the ``Block`` rotation, the
             :meth:`armi.reactor.blocks.HexBlock.rotate` method is called.
 
         Parameters
diff --git a/armi/reactor/assemblyParameters.py b/armi/reactor/assemblyParameters.py
index c99fdc43..39c44024 100644
--- a/armi/reactor/assemblyParameters.py
+++ b/armi/reactor/assemblyParameters.py
@@ -30,7 +30,7 @@ def getAssemblyParameterDefinitions():
             units=units.DEGREES,
             description=(
                 "Triple representing rotations counterclockwise around each spatial axis. "
-                "For example, a hex assembly rotated by 1/6th has orientation (0,0,60.0)"
+                "For example, a hex assembly rotated by 1/6th has orientation (0, 0, 60.0)"
             ),
             default=None,
         )
@@ -52,17 +52,17 @@ def getAssemblyParameterDefinitions():
         pb.defParam(
             "chargeBu",
             units=units.PERCENT_FIMA,
-            description="Max block-average burnup in this assembly when it most recently"
-            " entered the core. If the assembly was discharged and then re-charged,"
-            " this value will only reflect the most recent charge.",
+            description="Max block-average burnup in this assembly when it most recently entered "
+            "the core. If the assembly was discharged and then re-charged, this value will only "
+            "reflect the most recent charge.",
         )
 
         pb.defParam(
             "chargeCycle",
             units=units.UNITLESS,
-            description="Cycle number that this assembly most recently entered the core."
-            " If the assembly was discharged and then re-charged, this value will only"
-            " reflect the most recent charge.",
+            description="Cycle number that this assembly most recently entered the core. If the "
+            "assembly was discharged and then re-charged, this value will only reflect the most "
+            "recent charge.",
         )
 
         pb.defParam(
@@ -132,9 +132,8 @@ def getAssemblyParameterDefinitions():
                 raise ValueError
             elif len(value) > 1000:
                 runLog.warning(
-                    "Strings stored in the `notes` parameter must be less than 1000"
-                    f" characters. Truncating the note starting with {value[0:15]}..."
-                    " at 1000 characters!"
+                    "Strings stored in the `notes` parameter must be less than 1000 characters. "
+                    f"Truncating the note starting with {value[0:15]}... at 1000 characters!"
                 )
                 self._p_notes = value[0:1000]
             else:
diff --git a/armi/reactor/blockParameters.py b/armi/reactor/blockParameters.py
index 754b3891..15d0ef55 100644
--- a/armi/reactor/blockParameters.py
+++ b/armi/reactor/blockParameters.py
@@ -32,7 +32,7 @@ def getBlockParameterDefinitions():
             units=units.DEGREES,
             description=(
                 "Triple representing rotations counterclockwise around each spatial axis. For "
-                "example, a hex assembly rotated by 1/6th has orientation (0,0,60.0)"
+                "example, a hex assembly rotated by 1/6th has orientation (0, 0, 60.0)"
             ),
             default=None,
         )
diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py
index 3f7248a9..e0922a7f 100644
--- a/armi/reactor/blocks.py
+++ b/armi/reactor/blocks.py
@@ -1843,7 +1843,9 @@ class HexBlock(Block):
         """
         rotNum = round((rad % (2 * math.pi)) / math.radians(60))
         self._rotateChildLocations(rad, rotNum)
-        self.p.orientation[2] += rotNum * 60
+        if self.p.orientation is None:
+            self.p.orientation = np.array([0.0, 0.0, 0.0])
+        self.p.orientation[2] += rotNum * 60.0
         self._rotateBoundaryParameters(rotNum)
         self._rotateDisplacement(rad)
 
@@ -1898,9 +1900,8 @@ class HexBlock(Block):
                     pass
                 else:
                     msg = (
-                        "No rotation method defined for spatial parameters that aren't "
-                        "defined once per hex edge/corner. No rotation performed "
-                        f"on {name}"
+                        "No rotation method defined for spatial parameters that aren't defined "
+                        f"once per hex edge/corner. No rotation performed on {name}"
                     )
                     runLog.warning(msg)
             elif isinstance(original, (int, float)):
diff --git a/armi/reactor/blueprints/__init__.py b/armi/reactor/blueprints/__init__.py
index d402edc0..bfcb92a8 100644
--- a/armi/reactor/blueprints/__init__.py
+++ b/armi/reactor/blueprints/__init__.py
@@ -65,6 +65,7 @@ text representations of input and objects in the code.
 """
 
 import copy
+import math
 import pathlib
 import traceback
 import typing
@@ -182,8 +183,8 @@ class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector):
     _resolveFunctions = []
 
     def __new__(cls):
-        # yamlizable does not call __init__, so attributes that are not defined above
-        # need to be initialized here
+        # yamlizable does not call __init__, so attributes that are not defined above need to be
+        # initialized here
         self = yamlize.Object.__new__(cls)
         self.assemblies = {}
         self._prepped = False
@@ -198,9 +199,8 @@ class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector):
         return self
 
     def __init__(self):
-        # Yamlize does not call __init__, instead we use Blueprints.load which
-        # creates and instance of a Blueprints object and initializes it with values
-        # using setattr.
+        # Yamlize does not call __init__, instead we use Blueprints.load which creates and instance
+        # of a Blueprints object and initializes it with valuesconstructAssemusing setattr.
         self._assembliesBySpecifier = {}
         self._prepped = False
         self.systemDesigns = Systems()
@@ -211,11 +211,9 @@ class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector):
         self.elementsToExpand = []
 
     def __repr__(self):
-        return "<{} Assemblies:{} Blocks:{}>".format(
-            self.__class__.__name__, len(self.assemDesigns), len(self.blockDesigns)
-        )
+        return f"<{self.__class__.__name__} Assemblies:{len(self.assemDesigns)} Blocks:{len(self.blockDesigns)}>"
 
-    def constructAssem(self, cs, name=None, specifier=None):
+    def constructAssem(self, cs, name=None, specifier=None, orientation=0.0):
         """
         Construct a new assembly instance from the assembly designs in this Blueprints object.
 
@@ -223,14 +221,14 @@ class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector):
         ----------
         cs : Settings
             Used to apply various modeling options when constructing an assembly.
-
         name : str (optional, and should be exclusive with specifier)
-            Name of the assembly to construct. This should match the key that was used
-            to define the assembly in the Blueprints YAML file.
-
+            Name of the assembly to construct. This should match the key that was used to define the
+            assembly in the Blueprints YAML file.
         specifier : str (optional, and should be exclusive with name)
-            Identifier of the assembly to construct. This should match the identifier
-            that was used to define the assembly in the Blueprints YAML file.
+            Identifier of the assembly to construct. This should match the identifier that was used
+            to define the assembly in the Blueprints YAML file.
+        orientation : float (optional, is usually just zero)
+            Rotate the Assembly at creation.
 
         Raises
         ------
@@ -239,14 +237,12 @@ class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector):
 
         Notes
         -----
-        There is some possibility for "compiling" the logic with closures to make
-        constructing an assembly / block / component faster. At this point is is pretty
-        much irrelevant because we are currently just deepcopying already constructed
-        assemblies.
+        There is some possibility for "compiling" the logic with closures to make constructing an
+        assembly / block / component faster. At this point is is pretty much irrelevant because we
+        are currently just deepcopying already constructed assemblies.
 
-        Currently, this method is backward compatible with other code in ARMI and
-        generates the `.assemblies` attribute (the BOL assemblies). Eventually, this
-        should be removed.
+        Currently, this method is backward compatible with other code in ARMI and generates the
+        `.assemblies` attribute (the BOL assemblies). Eventually, this should be removed.
         """
         self._prepConstruction(cs)
 
@@ -260,19 +256,22 @@ class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector):
         a = copy.deepcopy(assem)
         # since a deepcopy has the same assembly numbers and block id's, we need to make it unique
         a.makeUnique()
+
+        if orientation:
+            a.rotate(math.radians(orientation))
         return a
 
     def _prepConstruction(self, cs):
         """
-        This method initializes a bunch of information within a Blueprints object such
-        as assigning assembly and block type numbers, resolving the nuclides in the
-        problem, and pre-populating assemblies.
+        This method initializes a bunch of information within a Blueprints object such as assigning
+        assembly and block type numbers, resolving the nuclides in the problem, and pre-populating
+        assemblies.
 
-        Ideally, it would not be necessary at all, but the ``cs`` currently contains a
-        bunch of information necessary to create the applicable model. If it were
-        possible, it would be terrific to override the Yamlizable.from_yaml method to
-        run this code after the instance has been created, but we need additional
-        information in order to build the assemblies that is not within the YAML file.
+        Ideally, it would not be necessary at all, but the ``cs`` currently contains a bunch of
+        information necessary to create the applicable model. If it were possible, it would be
+        terrific to override the Yamlizable.from_yaml method to run this code after the instance has
+        been created, but we need additional information in order to build the assemblies that is
+        not within the YAML file.
 
         This method should not be called directly, but it is used in testing.
         """
@@ -293,17 +292,16 @@ class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector):
             self._checkAssemblyAreaConsistency(cs)
 
             if not cs[CONF_DETAILED_AXIAL_EXPANSION]:
-                # this is required to set up assemblies so they know how to snap
-                # to the reference mesh. They won't know the mesh to conform to
-                # otherwise....
+                # this is required to set up assemblies so they know how to snap to the reference
+                # mesh. They won't know the mesh to conform to otherwise....
                 axialExpansionChanger.makeAssemsAbleToSnapToUniformMesh(
                     self.assemblies.values(), cs[CONF_NON_UNIFORM_ASSEM_FLAGS]
                 )
 
             if not cs[CONF_INPUT_HEIGHTS_HOT]:
                 runLog.header("=========== Axially expanding all assemblies from Tinput to Thot ===========")
-                # expand axial heights from cold to hot so dims and masses are consistent
-                # with specified component hot temperatures.
+                # expand axial heights from cold to hot so dims and masses are consistent with
+                # specified component hot temperatures.
                 assemsToSkip = [Flags.fromStringIgnoreErrors(t) for t in cs[CONF_ASSEM_FLAGS_SKIP_AXIAL_EXP]]
                 assemsToExpand = list(
                     a for a in list(self.assemblies.values()) if not any(a.hasFlags(f) for f in assemsToSkip)
@@ -334,9 +332,8 @@ class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector):
 
         Also builds meta-data about which nuclides are in the problem.
 
-        This system works by building a dictionary in the
-        ``elementsToExpand`` attribute with ``Element`` keys
-        and list of ``NuclideBase`` values.
+        This system works by building a dictionary in the ``elementsToExpand`` attribute with
+        ``Element`` keys and list of ``NuclideBase`` values.
 
         The actual expansion of elementals to isotopics occurs during
         :py:meth:`Component construction <armi.reactor.blueprints.componentBlueprint.
@@ -387,10 +384,9 @@ class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector):
                 currentSet = inerts
             else:
                 # This was not specified in the nuclide flags at all as burn or xs.
-                # If a material with this in its composition is brought in
-                # it's nice from a user perspective to allow it.
-                # But current behavior is that all nuclides in problem
-                # must be declared up front.
+                # If a material with this in its composition is brought in it's nice from a user
+                # perspective to allow it.
+                # But current behavior is that all nuclides in problem must be declared up front.
                 continue
 
             self.elementsToExpand.append(elemental.element)
diff --git a/armi/reactor/blueprints/assemblyBlueprint.py b/armi/reactor/blueprints/assemblyBlueprint.py
index 7377e965..cd0ec528 100644
--- a/armi/reactor/blueprints/assemblyBlueprint.py
+++ b/armi/reactor/blueprints/assemblyBlueprint.py
@@ -106,8 +106,7 @@ class AssemblyBlueprint(yamlize.Object):
     """
     A data container for holding information needed to construct an ARMI assembly.
 
-    This class utilizes ``yamlize`` to enable serialization to and from the
-    blueprints YAML file.
+    This class utilizes ``yamlize`` to enable serialization to and from the blueprints YAML file.
 
     .. impl:: Create assembly from blueprint file.
         :id: I_ARMI_BP_ASSEM
@@ -316,7 +315,7 @@ class AssemblyKeyedList(yamlize.KeyedList):
     heights = yamlize.Attribute(type=yamlize.FloatList, default=None)
     axialMeshPoints = yamlize.Attribute(key="axial mesh points", type=yamlize.IntList, default=None)
 
-    # note: yamlize does not call an __init__ method, instead it uses __new__ and setattr
+    # NOTE: yamlize does not call an __init__ method, instead it uses __new__ and setattr
 
     @property
     def bySpecifier(self):
diff --git a/armi/reactor/blueprints/gridBlueprint.py b/armi/reactor/blueprints/gridBlueprint.py
index 0515912c..fb5eaa11 100644
--- a/armi/reactor/blueprints/gridBlueprint.py
+++ b/armi/reactor/blueprints/gridBlueprint.py
@@ -180,6 +180,9 @@ class GridBlueprint(yamlize.Object):
     gridContents : dict
         A {(i,j): str} dictionary mapping spatialGrid indices in 2-D to string specifiers of what's
         supposed to be in the grid.
+    orientationBOL : dict
+        A {(i,j): float} dictionary mapping spatialGrid indices in 2-D to the orientation of
+        what's supposed to be in the grid.
     """
 
     name = yamlize.Attribute(key="name", type=str)
@@ -196,13 +199,24 @@ class GridBlueprint(yamlize.Object):
     # input is read. When writing, we attempt to preserve the input mode and write ascii map if that
     # was what was originally provided.
     gridContents = yamlize.Attribute(key="grid contents", type=dict, default=None)
+    # allowing us to add custom orientations to the objects on this gritd, at BOL
+    orientationBOL = yamlize.Attribute(key="orientationBOL", type=dict, default=None)
 
     @gridContents.validator
     def gridContents(self, value):
         if value is None:
             return True
         if not all(isinstance(key, tuple) for key in value.keys()):
-            raise InputError("Keys need to be presented as [i, j]. Check the blueprints.")
+            raise InputError("Grid contents Keys need to be like [i, j]. Check the blueprints.")
+
+        return True
+
+    @orientationBOL.validator
+    def orientationBOL(self, value):
+        if value is None:
+            return True
+        if not all(isinstance(key, tuple) for key in value.keys()):
+            raise InputError("Orientation BOL Keys need to be like [i, j]. Check the blueprints.")
 
         return True
 
@@ -213,6 +227,7 @@ class GridBlueprint(yamlize.Object):
         latticeMap=None,
         symmetry=str(geometry.SymmetryType(geometry.DomainType.THIRD_CORE, geometry.BoundaryType.PERIODIC)),
         gridContents=None,
+        orientationBOL=None,
         gridBounds=None,
     ):
         """
@@ -220,9 +235,8 @@ class GridBlueprint(yamlize.Object):
 
         Notes
         -----
-        yamlize does not call an ``__init__`` method, instead it uses ``__new__`` and
-        setattr this is only needed for when you want to make this object from a non-YAML
-        source.
+        yamlize does not call an ``__init__`` method, instead it uses ``__new__`` and setattr this
+        is only needed for when you want to make this object from a non-YAML source.
 
         Warning
         -------
@@ -234,13 +248,14 @@ class GridBlueprint(yamlize.Object):
         self._readFromLatticeMap = False
         self.symmetry = str(symmetry)
         self.gridContents = gridContents
+        self.orientationBOL = orientationBOL
         self.gridBounds = gridBounds
 
     @property
     def readFromLatticeMap(self):
         """
-        This is implemented as a property, since as a Yamlize object, ``__init__`` is not always called
-        and we have to lazily evaluate its default value.
+        This is implemented as a property, since as a Yamlize object, ``__init__`` is not always
+        called and we have to lazily evaluate its default value.
         """
         return getattr(self, "_readFromLatticeMap", False)
 
@@ -258,8 +273,8 @@ class GridBlueprint(yamlize.Object):
         """
         Build spatial grid.
 
-        If you do not enter ``latticeDimensions``, a unit grid will be produced which must be adjusted
-        to the proper dimensions (often by inspection of children) at a later time.
+        If you do not enter ``latticeDimensions``, a unit grid will be produced which must be
+        adjusted to the proper dimensions (often by inspection of children) at a later time.
         """
         symmetry = geometry.SymmetryType.fromStr(self.symmetry) if self.symmetry else None
         geom = self.geom
@@ -267,8 +282,8 @@ class GridBlueprint(yamlize.Object):
         runLog.extra("Creating the spatial grid")
         if geom in (geometry.RZT, geometry.RZ):
             if self.gridBounds is None:
-                # This check is regrettably late. It would be nice if we could validate
-                # that bounds are provided if R-Theta mesh is being used.
+                # This check is regrettably late. It would be nice if we could validate that bounds
+                # are provided if R-Theta mesh is being used.
                 raise InputError(
                     f"Grid bounds must be provided for `{self.name}` to specify a grid with r-theta components."
                 )
@@ -294,26 +309,25 @@ class GridBlueprint(yamlize.Object):
                 cornersUp=geom == geometry.HEX_CORNERS_UP,
             )
         elif geom == geometry.CARTESIAN:
-            # if full core or not cut-off, bump the first assembly from the center of
-            # the mesh into the positive values.
+            # if full core or not cut-off, bump the first assembly from the center of the mesh into
+            # the positive values.
             xw, yw = (self.latticeDimensions.x, self.latticeDimensions.y) if self.latticeDimensions else (1.0, 1.0)
 
-            # Specifically in the case of grid blueprints, where we have grid contents
-            # available, we can also infer "through center" based on the contents.
-            # Note that the "through center" symmetry check cannot be performed when
-            # the grid contents has not been provided (i.e., None or empty).
+            # Specifically in the case of grid blueprints, where we have grid contents available, we
+            # can also infer "through center" based on the contents. Note that the "through center"
+            # symmetry check cannot be performed when the grid contents has not been provided (i.e.,
+            # None or empty).
             if self.gridContents and symmetry.domain == geometry.DomainType.FULL_CORE:
                 nx, ny = _getGridSize(self.gridContents.keys())
                 if nx == ny and nx % 2 == 1:
                     symmetry.isThroughCenterAssembly = True
 
             isOffset = symmetry is not None and not symmetry.isThroughCenterAssembly
-
             spatialGrid = grids.CartesianGrid.fromRectangle(xw, yw, numRings=maxIndex + 1, isOffset=isOffset)
+
         runLog.debug("Built grid: {}".format(spatialGrid))
-        # set geometric metadata on spatialGrid. This information is needed in various
-        # parts of the code and is best encapsulated on the grid itself rather than on
-        # the container state.
+        # set geometric metadata on spatialGrid. This information is needed in various parts of the
+        # code and is best encapsulated on the grid itself rather than on the container state.
         spatialGrid._geomType: str = str(self.geom)
         self.symmetry = str(symmetry)
         spatialGrid._symmetry: str = self.symmetry
@@ -342,9 +356,9 @@ class GridBlueprint(yamlize.Object):
         make this more sophisticated.
         """
         if geometry.SymmetryType.fromAny(self.symmetry).domain == geometry.DomainType.FULL_CORE:
-            # No need!
             return
 
+        # fill the new grid contents
         grid = self.construct()
 
         newContents = copy.copy(self.gridContents)
@@ -352,7 +366,10 @@ class GridBlueprint(yamlize.Object):
             equivs = grid.getSymmetricEquivalents(idx)
             for idx2 in equivs:
                 newContents[idx2] = contents
+
         self.gridContents = newContents
+
+        # set the grid symmetry
         split = geometry.THROUGH_CENTER_ASSEMBLY in self.symmetry
         self.symmetry = str(
             geometry.SymmetryType(
@@ -379,15 +396,15 @@ class GridBlueprint(yamlize.Object):
             self._readGridContentsLattice()
 
         if self.gridContents is None:
-            # Make sure we have at least something; clients shouldn't have to worry
-            # about whether gridContents exist at all.
+            # Make sure we have at least something; clients shouldn't have to worry about whether
+            # gridContents exist at all.
             self.gridContents = dict()
 
     def _readGridContentsLattice(self):
         """Read an ascii map of grid contents.
 
-        This update the gridContents attribute, which is a dict mapping grid i,j,k indices to
-        textual specifiers (e.g. ``IC``))
+        This update the gridContents attribute, which is a dict mapping grid i,j,k indices to textual specifiers
+        (e.g. ``IC``)).
         """
         self.readFromLatticeMap = True
         symmetry = geometry.SymmetryType.fromStr(self.symmetry)
@@ -400,12 +417,11 @@ class GridBlueprint(yamlize.Object):
         iOffset = 0
         jOffset = 0
         if geom == geometry.GeomType.CARTESIAN and symmetry.domain == geometry.DomainType.FULL_CORE:
-            # asciimaps is not smart about where the center should be, so we need to
-            # offset appropriately to get (0,0) in the middle
+            # asciimaps is not smart about where the center should be, so we need to offset
+            # apropriately to get (0,0) in the middle
             nx, ny = _getGridSize(asciimap.keys())
 
-            # turns out this works great for even and odd cases. love it when integer
-            # math works in your favor
+            # turns out this works great for even and odd cases. love it when integer math works in your favor
             iOffset = int(-nx / 2)
             jOffset = int(-ny / 2)
 
@@ -425,16 +441,16 @@ class GridBlueprint(yamlize.Object):
             return []
         if self.gridContents is None:
             return []
-        # tried using yamlize to coerce ints to strings but failed
-        # after much struggle, so we just auto-convert here to deal
-        # with int-like specifications.
-        # (yamlize.StrList fails to coerce when ints are provided)
+        # tried using yamlize to coerce ints to strings but failed after much struggle, so we just
+        # auto-convert here to deal with int-like specifications. (yamlize.StrList fails to coerce
+        # when ints are provided)
         latticeIDs = [str(i) for i in latticeIDs]
         locators = []
         for (i, j), spec in self.gridContents.items():
             locator = spatialGrid[i, j, 0]
             if spec in latticeIDs:
                 locators.append(locator)
+
         return locators
 
     def getMultiLocator(self, spatialGrid, latticeIDs):
@@ -451,12 +467,10 @@ class Grids(yamlize.KeyedList):
 
 def _getGridSize(idx) -> Tuple[int, int]:
     """
-    Return the number of spaces between the min and max of a collection of (int, int)
-    tuples, inclusive.
+    Return the number of spaces between the min and max of a collection of (int, int) tuples, inclusive.
 
-    This essentially returns the number of grid locations along the i, and j dimensions,
-    given the (i,j) indices of each occupied location. This is useful for determining
-    certain grid offset behavior.
+    This essentially returns the number of grid locations along the i, and j dimensions, given the (i,j) indices of each
+    occupied location. This is useful for determining certain grid offset behavior.
     """
     nx = max(key[0] for key in idx) - min(key[0] for key in idx) + 1
     ny = max(key[1] for key in idx) - min(key[1] for key in idx) + 1
@@ -467,16 +481,15 @@ def _getGridSize(idx) -> Tuple[int, int]:
 def _filterOutsideDomain(gridBp):
     """Remove grid contents that lie outside the represented domain.
 
-    This removes extra objects; ARMI allows the user input specifiers in regions outside
-    of the represented domain, which is fine as long as the contained specifier is
-    consistent with the corresponding region in the represented domain given the
-    symmetry condition. For instance, if we have a 1/3-core hex model, it is typically
-    okay for an assembly to be specified outside of the first 1/3rd of the core, as long
-    as it is the same assembly as would be there when expanding the first 1/3rd into a
-    full-core model.
+    This removes extra objects; ARMI allows the user input specifiers in regions outside of the
+    represented domain, which is fine as long as the contained specifier is consistent with the
+    corresponding region in the represented domain given the symmetry condition. For instance, if we
+    have a 1/3-core hex model, it is typically okay for an assembly to be specified outside of the
+    first 1/3rd of the core, as long as it is the same assembly as would be there when expanding the
+    first 1/3rd into a full-core model.
 
-    However, we do not really want these hanging around, since editing the represented
-    1/Nth of the core will probably lead to consistency issues, so we remove them.
+    However, we do not really want these hanging around, since editing the represented 1/Nth of the
+    core will probably lead to consistency issues, so we remove them.
     """
     grid = gridBp.construct()
 
@@ -534,10 +547,10 @@ def saveToStream(stream, bluep, full=False, tryMap=False):
     tryMap : bool
         regardless of input form, attempt to output as a lattice map
     """
-    # To save, we want to try our best to output our grid blueprints in the lattice
-    # map style. However, we do not want to wreck the state that the current
-    # blueprints are in. So we make a copy and do some manipulations to try to
-    # canonicalize it and save that, leaving the original blueprints unmolested.
+    # To save, we want to try our best to output our grid blueprints in the lattice map style.
+    # However, we do not want to wreck the state that the current blueprints are in. So we make a
+    # copy and do some manipulations to try to canonicalize it and save that, leaving the original
+    # blueprints unmolested.
     bp = copy.deepcopy(bluep)
 
     if isinstance(bp, blueprints.Blueprints):
@@ -548,10 +561,8 @@ def saveToStream(stream, bluep, full=False, tryMap=False):
         raise TypeError("Expected Blueprints or Grids, got {}".format(type(bp)))
 
     for gridDesignType, gridDesign in gridDesigns.items():
-        # The core equilibrium path should be put into the
-        # grid contents rather than a lattice map until we write
-        # a string-> tuple parser for reading it back in. Skip
-        # this type of grid.
+        # The core equilibrium path should be put into the grid contents rather than a lattice map
+        # until we write a string-> tuple parser for reading it back in. Skip this type of grid.
         if gridDesignType == "coreEqPath":
             continue
         _filterOutsideDomain(gridDesign)
@@ -575,9 +586,9 @@ def saveToStream(stream, bluep, full=False, tryMap=False):
                 aMap = None
 
             if aMap is not None:
-                # If there is an ascii map available then use it to fill out
-                # the contents of the lattice map section of the grid design.
-                # This also clears out the grid contents so there is not duplicate data.
+                # If there is an ascii map available then use it to fill out the contents of the
+                # lattice map section of the grid design. This also clears out the grid contents so
+                # there is not duplicate data.
                 gridDesign.gridContents = None
                 mapString = StringIO()
                 aMap.writeAscii(mapString)
diff --git a/armi/reactor/blueprints/reactorBlueprint.py b/armi/reactor/blueprints/reactorBlueprint.py
index 26829b1c..77ce89e1 100644
--- a/armi/reactor/blueprints/reactorBlueprint.py
+++ b/armi/reactor/blueprints/reactorBlueprint.py
@@ -107,8 +107,8 @@ class SystemBlueprint(yamlize.Object):
                 seen.add(key)
 
         raise ValueError(
-            "Could not determine an appropriate class for handling a "
-            "system of type `{}`. Supported types are {}.".format(typ, sorted(seen))
+            f"Could not determine an appropriate class for handling a system of type `{typ}`. "
+            f"Supported types are {seen}."
         )
 
     def construct(self, cs, bp, reactor, loadComps=True):
@@ -188,7 +188,7 @@ class SystemBlueprint(yamlize.Object):
         from armi.reactor.reactors import Core  # avoid circular import
 
         if loadComps and gridDesign is not None:
-            self._loadComposites(cs, system, gridDesign.gridContents, bp)
+            self._loadComposites(cs, bp, system, gridDesign.gridContents, gridDesign.orientationBOL)
 
             if isinstance(system, Core):
                 summarizeMaterialData(system)
@@ -197,18 +197,34 @@ class SystemBlueprint(yamlize.Object):
 
         return system
 
-    def _loadComposites(self, cs, container, gridContents, bp):
+    def _loadComposites(self, cs, bp, container, gridContents, orientationBOL):
+        from armi.reactor.cores import Core
+
         runLog.header(f"=========== Adding Composites to {container} ===========")
         badLocations = set()
         for locationInfo, aTypeID in gridContents.items():
-            newAssembly = bp.constructAssem(cs, specifier=aTypeID)
-
+            # handle the hex-grid special case, where the user enters (ring, pos)
             i, j = locationInfo
-            loc = container.spatialGrid[i, j, 0]
+            if isinstance(container, Core) and container.geomType == geometry.GeomType.HEX:
+                loc = container.spatialGrid.indicesToRingPos(i, j)
+            else:
+                loc = locationInfo
+
+            # correctly rotate the Composite
+            if orientationBOL is None or loc not in orientationBOL:
+                orientation = 0.0
+            else:
+                orientation = orientationBOL[loc]
+
+            # create a new Composite to add to the grid
+            newAssembly = bp.constructAssem(cs, specifier=aTypeID, orientation=orientation)
+
+            # add the Composite to the grid
+            posi = container.spatialGrid[i, j, 0]
             try:
-                container.add(newAssembly, loc)
+                container.add(newAssembly, posi)
             except LookupError:
-                badLocations.add(loc)
+                badLocations.add(posi)
 
         if badLocations:
             raise ValueError(f"Attempted to add objects to non-existent locations on the grid: {badLocations}.")
@@ -216,9 +232,7 @@ class SystemBlueprint(yamlize.Object):
     def _modifyGeometry(self, container, gridDesign):
         """Perform post-load geometry conversions like full core, edge assems."""
         # all cases should have no edge assemblies. They are added ephemerally when needed
-        from armi.reactor.converters import (
-            geometryConverters,
-        )
+        from armi.reactor.converters import geometryConverters
 
         runLog.header("=========== Applying Geometry Modifications ===========")
         converter = geometryConverters.EdgeAssemblyChanger()
diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py
index b0c7cb55..16ddc417 100644
--- a/armi/reactor/grids/hexagonal.py
+++ b/armi/reactor/grids/hexagonal.py
@@ -392,9 +392,8 @@ class HexGrid(StructuredGrid):
         Returns
         -------
         None or int
-            None if not line of symmetry goes through the object at the
-            requested index. Otherwise, some grid constants like ``BOUNDARY_CENTER``
-            will be returned.
+            None if not line of symmetry goes through the object at the requested index. Otherwise,
+            some grid constants like ``BOUNDARY_CENTER`` will be returned.
 
         Notes
         -----
@@ -420,18 +419,17 @@ class HexGrid(StructuredGrid):
         return symmetryLine
 
     def getSymmetricEquivalents(self, indices: IJKType) -> List[IJType]:
-        """Retrieve the equivalent indices. If full core return nothing, if 1/3-core grid,
-        return the symmetric equivalents, if any other grid, raise an error.
+        """Retrieve the equivalent indices. If full core return nothing, if 1/3-core grid, return
+        the symmetric equivalents, if any other grid, raise an error.
 
         .. impl:: Equivalent contents in 1/3-core geometries are retrievable.
             :id: I_ARMI_GRID_EQUIVALENTS
             :implements: R_ARMI_GRID_EQUIVALENTS
 
-            This method takes in (I,J,K) indices, and if this ``HexGrid`` is full core,
-            it returns nothing. If this ``HexGrid`` is 1/3-core, this method will return
-            the 1/3-core symmetric equivalent of just (I,J). If this grid is any other kind,
-            this method will just return an error; a hexagonal grid with any other
-            symmetry is probably an error.
+            This method takes in (I,J,K) indices, and if this ``HexGrid`` is full core, it returns
+            nothing. If this ``HexGrid`` is 1/3-core, this method will return the 1/3-core symmetric
+            equivalent of just (I,J). If this grid is any other kind, this method will just return
+            an error; a hexagonal grid with any other symmetry is probably an error.
         """
         if (
             self.symmetry.domain == geometry.DomainType.THIRD_CORE
@@ -441,7 +439,7 @@ class HexGrid(StructuredGrid):
         elif self.symmetry.domain == geometry.DomainType.FULL_CORE:
             return []
         else:
-            raise NotImplementedError("Unhandled symmetry condition for HexGrid: {}".format(str(self.symmetry)))
+            raise NotImplementedError(f"Unhandled symmetry condition for HexGrid: {self.symmetry}")
 
     @staticmethod
     def _getSymmetricIdenticalsThird(indices) -> List[IJType]:
@@ -449,13 +447,14 @@ class HexGrid(StructuredGrid):
         i, j = indices[:2]
         if i == 0 and j == 0:
             return []
+
         identicals = [(-i - j, i), (j, -i - j)]
         return identicals
 
     def triangleCoords(self, indices: IJKType) -> np.ndarray:
         """
-        Return 6 coordinate pairs representing the centers of the 6 triangles in a
-        hexagon centered here.
+        Return 6 coordinate pairs representing the centers of the 6 triangles in a hexagon centered
+        here.
 
         Ignores z-coordinate and only operates in 2D for now.
         """
@@ -501,8 +500,8 @@ class HexGrid(StructuredGrid):
         self._unitSteps = unitSteps[self._stepDims]
 
     def locatorInDomain(self, locator, symmetryOverlap: Optional[bool] = False) -> bool:
-        # This will include the "top" 120-degree symmetry lines. This is to support
-        # adding of edge assemblies.
+        # This will include the "top" 120-degree symmetry lines. This is to support adding of edge
+        # assemblies.
         if self.symmetry.domain == geometry.DomainType.THIRD_CORE:
             return self.isInFirstThird(locator, includeTopEdge=symmetryOverlap)
         else:
@@ -515,11 +514,10 @@ class HexGrid(StructuredGrid):
             :id: I_ARMI_GRID_SYMMETRY_LOC
             :implements: R_ARMI_GRID_SYMMETRY_LOC
 
-            This is a simple helper method to determine if a given locator (from an
-            ArmiObject) is in the first 1/3 of the ``HexGrid``. This method does not
-            attempt to check if this grid is full or 1/3-core. It just does the basic
-            math of dividing up a hex-assembly reactor core into thirds and testing if
-            the given location is in the first 1/3 or not.
+            This is a simple helper method to determine if a given locator (from an ArmiObject) is
+            in the first 1/3 of the ``HexGrid``. This method does not attempt to check if this grid
+            is full or 1/3-core. It just does the basic math of dividing up a hex-assembly reactor
+            core into thirds and testing if the given location is in the first 1/3 or not.
         """
         ring, pos = self.getRingPos(locator.indices)
         if ring == 1:
@@ -545,8 +543,8 @@ class HexGrid(StructuredGrid):
 
         IndexLocation are taken from a full core.
 
-        Ties between locations with the same distance (e.g. A3001 and A3003) are broken
-        by ring number then position number.
+        Ties between locations with the same distance (e.g. A3001 and A3003) are broken by ring
+        number then position number.
         """
         # first, roughly calculate how many rings need to be created to cover nLocs worth of assemblies
         nLocs = int(nLocs)
@@ -587,27 +585,27 @@ class HexGrid(StructuredGrid):
 
         Notes
         -----
-        Rotation uses a three-dimensional index in what can be known elsewhere
-        by the confusing name of "cubic" coordinate system for a hexagon. Cubic stems
-        from the notion of using three dimensions, ``(q, r, s)`` to describe a point in the
-        hexagonal grid. The conversion from the indexing used in the ARMI framework follows::
+        Rotation uses a three-dimensional index in what can be known elsewhere by the confusing name
+        of "cubic" coordinate system for a hexagon. Cubic stems from the notion of using three
+        dimensions, ``(q, r, s)`` to describe a point in the hexagonal grid. The conversion from the
+        indexing used in the ARMI framework follows::
 
             q = i
             r = j
             # s = - q - r = - (q + r)
             s = -(i + j)
 
-        The motivation for the cubic notation is rotation is far simpler: a clockwise
-        rotation by 60 degrees results in a shifting and negating of the coordinates. So
-        the first rotation of ``(q, r, s)`` would produce a new coordinate
-        ``(-r, -s, -q)``. Another rotation would produce ``(s, q, r)``, and so on.
+        The motivation for the cubic notation is rotation is far simpler: a clockwise rotation by 60
+        degrees results in a shifting and negating of the coordinates. So the first rotation of
+        ``(q, r, s)`` would produce a new coordinate ``(-r, -s, -q)``. Another rotation would
+        produce ``(s, q, r)``, and so on.
 
         Raises
         ------
         TypeError
-            If ``loc.grid`` is populated and not consistent with this grid. For example,
-            it doesn't make sense to rotate an index from a Cartesian grid in a hexagonal coordinate
-            system, nor hexagonal grid with different orientation (flats up vs. corners up)
+            If ``loc.grid`` is populated and not consistent with this grid. For example, it doesn't
+            make sense to rotate an index from a Cartesian grid in a hexagonal coordinate system,
+            nor hexagonal grid with different orientation (flats up vs. corners up)
         """
         if self._roughlyEqual(loc.grid) or loc.grid is None:
             i, j, k = loc[:3]
diff --git a/armi/settings/fwSettings/globalSettings.py b/armi/settings/fwSettings/globalSettings.py
index 388e3e56..a03db3c4 100644
--- a/armi/settings/fwSettings/globalSettings.py
+++ b/armi/settings/fwSettings/globalSettings.py
@@ -689,7 +689,7 @@ def defineSettings() -> List[setting.Setting]:
             CONF_SORT_REACTOR,
             default=True,
             label="Do we want to automatically sort the Reactor?",
-            description="Deprecation Warning! This setting will be remove by 2024.",
+            description="If unsorted, ArmiObject IDs will be by the order they were added to the Reactor.",
         ),
         setting.Setting(
             CONF_RM_EXT_FILES_AT_BOC,


Test patch:
diff --git a/armi/reactor/blueprints/tests/test_reactorBlueprints.py b/armi/reactor/blueprints/tests/test_reactorBlueprints.py
index 6beae77a..45e9e7e0 100644
--- a/armi/reactor/blueprints/tests/test_reactorBlueprints.py
+++ b/armi/reactor/blueprints/tests/test_reactorBlueprints.py
@@ -56,6 +56,9 @@ core:
     grid contents:
       [0, 0]: IC
       [1, 1]: IC
+    orientationBOL:
+      [1, 1]: 60.0
+      [3, 2]: 120.0
 sfp:
     lattice pitch:
         x: 25.0
@@ -65,6 +68,9 @@ sfp:
     lattice map: |
       IC IC
       IC IC
+    orientationBOL:
+      [0, 0]: 60.0
+      [0, -1]: 120.0
 evst:
     lattice pitch:
         x: 32.0
@@ -213,3 +219,18 @@ class TestReactorBlueprints(unittest.TestCase):
         self.assertEqual(len(sfp.getChildren()), 4)
         sfp.add(evst.getChildren()[0])
         self.assertEqual(len(sfp.getChildren()), 5)
+
+    def test_orientationBOL(self):
+        core, sfp, _evst = self._setupReactor()
+
+        # test for hex core
+        a0 = core.getAssembly(locationString="001-001")
+        self.assertAlmostEqual(a0.p.orientation[2], 60.0, delta=1e-9)
+        a1 = core.getAssembly(locationString="003-002")
+        self.assertAlmostEqual(a1.p.orientation[2], 120.0, delta=1e-9)
+
+        # test cartesian, non-core
+        a0 = sfp.getAssembly("A0005")
+        self.assertAlmostEqual(a0.p.orientation[2], 60.0, delta=1e-9)
+        a1 = sfp.getAssembly("A0003")
+        self.assertAlmostEqual(a1.p.orientation[2], 120.0, delta=1e-9)
diff --git a/armi/reactor/tests/test_assemblies.py b/armi/reactor/tests/test_assemblies.py
index e55876f4..f3ab1893 100644
--- a/armi/reactor/tests/test_assemblies.py
+++ b/armi/reactor/tests/test_assemblies.py
@@ -220,24 +220,6 @@ class Assembly_TestCase(unittest.TestCase):
             "ztop": 111.80279999999999,
         }
 
-        self.blockSettings = {
-            "axMesh": 1,
-            "bondBOL": 0.0028698019026172574,
-            "envGroup": "A",
-            "height": 14.4507,
-            "molesHmAtBOL": 65.8572895758245,
-            "nHMAtBOL": 0.011241485251783766,
-            "nPins": 169.0,
-            "name": "B0011F",
-            "newDPA": 0.0,
-            "pitch": 16.79,
-            "regName": False,
-            "topIndex": 5,
-            "tsIndex": 0,
-            "type": "igniter fuel",
-            "xsType": "C",
-            "z": 104.57745,
-        }
         # add some blocks with a component
         self.blockList = []
         for i in range(NUM_BLOCKS):
diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py
index 4664bf3f..c769c066 100644
--- a/armi/reactor/tests/test_blocks.py
+++ b/armi/reactor/tests/test_blocks.py
@@ -368,6 +368,16 @@ class TestValidateSFPSpatialGrids(unittest.TestCase):
         )
         self.assertIsNotNone(r.excore.sfp.spatialGrid)
 
+    def test_orientationBOL(self):
+        _o, r = loadTestReactor(
+            os.path.join(TEST_ROOT, "smallestTestReactor"),
+            inputFileName="armiRunSmallest.yaml",
+        )
+
+        # Test the null-case; these should all be zero.
+        for a in r.core.iterChildren():
+            self.assertIsNone(a.p.orientation)
+
 
 class Block_TestCase(unittest.TestCase):
     def setUp(self):