Repository: terrapower/armi

Issue:
c5g7 tutorial case errors when plotting
Working through https://terrapower.github.io/armi/user/tutorials/walkthrough_lwr_inputs.html with no edits to the inputs at the bottom of the page I got the following error. The ANL case worked fine, local tests passed. 

```
$ armi run c5g7-settings.yaml 

[impt] Beginning of BOL Reports
[err ] <class 'AttributeError'>\n'NoneType' object has no attribute 'getPitch'\<traceback object at 0x128243208>
[info] Writing to database for statepoint: /c00n00error
[CRIT 000 ] Unhandled exception in __main__ on local.
[CRIT 000 ] Stack trace: Traceback (most recent call last):
  File "/Users/veeshy/projects/armi/armi-venv/lib/python3.7/site-packages/armi/__main__.py", line 34, in main
    code = ArmiCLI().run()
  File "/Users/veeshy/projects/armi/armi-venv/lib/python3.7/site-packages/armi/cli/__init__.py", line 171, in run
    return self.executeCommand(args.command, args.args)
  File "/Users/veeshy/projects/armi/armi-venv/lib/python3.7/site-packages/armi/cli/__init__.py", line 197, in executeCommand
    return cmd.invoke()
  File "/Users/veeshy/projects/armi/armi-venv/lib/python3.7/site-packages/armi/cli/run.py", line 36, in invoke
    inputCase.run()
  File "/Users/veeshy/projects/armi/armi-venv/lib/python3.7/site-packages/armi/cases/case.py", line 347, in run
    o.operate()
  File "/Users/veeshy/projects/armi/armi-venv/lib/python3.7/site-packages/armi/operators/operator.py", line 204, in operate
    self._mainOperate()
  File "/Users/veeshy/projects/armi/armi-venv/lib/python3.7/site-packages/armi/operators/operator.py", line 210, in _mainOperate
    self.interactAllBOL()
  File "/Users/veeshy/projects/armi/armi-venv/lib/python3.7/site-packages/armi/operators/operator.py", line 407, in interactAllBOL
    self._interactAll("BOL", activeInterfaces)
  File "/Users/veeshy/projects/armi/armi-venv/lib/python3.7/site-packages/armi/operators/operator.py", line 292, in _interactAll
    halt = halt or interactMethod(*args)
  File "/Users/veeshy/projects/armi/armi-venv/lib/python3.7/site-packages/armi/bookkeeping/report/reportInterface.py", line 58, in interactBOL
    reportingUtils.makeCoreAndAssemblyMaps(self.r, self.cs)
  File "/Users/veeshy/projects/armi/armi-venv/lib/python3.7/site-packages/armi/bookkeeping/report/reportingUtils.py", line 986, in makeCoreAndAssemblyMaps
    fontSize=8,
  File "/Users/veeshy/projects/armi/armi-venv/lib/python3.7/site-packages/armi/reactor/reactors.py", line 1977, in plotFaceMap
    Flags.FUEL
AttributeError: 'NoneType' object has no attribute 'getPitch'
```

Discussion:
ntouran: Good call. I propagated version info to the layout here and handle it. I tested manually loading an old DB and it worked. 

Patch:
diff --git a/armi/bookkeeping/db/database3.py b/armi/bookkeeping/db/database3.py
index a946da84..71ac977c 100644
--- a/armi/bookkeeping/db/database3.py
+++ b/armi/bookkeeping/db/database3.py
@@ -44,6 +44,9 @@ Minor revision changelog
    for reading previous versions, and for performing a ``mergeHistory()`` and converting
    to the new reference strategy, but the old version cannot be written.
 
+ - 3.3: Compress the way locations are stored in the database and allow MultiIndex locations
+   to be read and written.
+
 """
 import collections
 import copy
@@ -89,13 +92,20 @@ from armi.reactor import geometry
 from armi.utils.textProcessors import resolveMarkupInclusions
 
 ORDER = interfaces.STACK_ORDER.BOOKKEEPING
-DB_VERSION = "3.2"
+DB_MAJOR = 3
+DB_MINOR = 3
+DB_VERSION = f"{DB_MAJOR}.{DB_MINOR}"
 
 ATTR_LINK = re.compile("^@(.*)$")
 
 _SERIALIZER_NAME = "serializerName"
 _SERIALIZER_VERSION = "serializerVersion"
 
+LOC_NONE = "N"
+LOC_COORD = "C"
+LOC_INDEX = "I"
+LOC_MULTI = "M:"
+
 
 def getH5GroupName(cycle, timeNode, statePointName=None):
     return "c{:0>2}n{:0>2}{}".format(cycle, timeNode, statePointName or "")
@@ -809,7 +819,7 @@ class Database3(database.Database):
         # _createLayout is recursive
         h5group = self.getH5Group(reactor, statePointName)
         runLog.info("Writing to database for statepoint: {}".format(h5group.name))
-        layout = Layout(comp=reactor)
+        layout = Layout(comp=reactor, version=(self.versionMajor, self.versionMinor))
         layout.writeToDB(h5group)
         groupedComps = layout.groupedComps
 
@@ -850,7 +860,7 @@ class Database3(database.Database):
 
         h5group = self.h5db[getH5GroupName(cycle, node, statePointName)]
 
-        layout = Layout(h5group=h5group)
+        layout = Layout(h5group=h5group, version=(self.versionMajor, self.versionMinor))
         comps, groupedComps = layout._initComps(cs, bp)
 
         # populate data onto initialized components
@@ -1191,7 +1201,9 @@ class Database3(database.Database):
 
             cycle = h5TimeNodeGroup.attrs["cycle"]
             timeNode = h5TimeNodeGroup.attrs["timeNode"]
-            layout = Layout(h5group=h5TimeNodeGroup)
+            layout = Layout(
+                h5group=h5TimeNodeGroup, version=(self.versionMajor, self.versionMinor)
+            )
 
             for compType, compsBySerialNum in compsByTypeThenSerialNum.items():
                 compTypeName = compType.__name__
@@ -1284,6 +1296,129 @@ class Database3(database.Database):
         return histData
 
 
+def _packLocation(
+    loc: grids.LocationBase, version: Tuple[int, int] = (DB_MAJOR, DB_MINOR)
+) -> Tuple[str, List]:
+    """
+    Extract information from a location needed to write it to this DB.
+
+    Each locator has one locationType and up to N location-defining datums,
+    where N is the number of entries in a possible multiindex, or just 1
+    for everything else.
+
+    Shrink grid locator names for storage efficiency.
+    
+    Notes
+    -----
+    Contains some conditionals to still load databases made before
+    db version 3.3 which can be removed once no users care about
+    those DBs anymore.
+    """
+    oldStyle = version[0] == 3 and version[1] < 3
+    if oldStyle:
+        locationType, locationData = _packLocationOld(loc)
+    else:
+        locationType, locationData = _packLocationNew(loc)
+    return locationType, locationData
+
+
+def _packLocationOld(loc: grids.LocationBase):
+    """Delete when reading v <=3.2 DB's no longer wanted."""
+    locationType = loc.__class__.__name__
+    if loc is None:
+        locationType = "None"
+        locData = [(0.0, 0.0, 0.0)]
+    elif loc.__class__ is grids.CoordinateLocation:
+        locData = [loc.indices]
+    elif loc.__class__ is grids.IndexLocation:
+        locData = [loc.indices]
+    else:
+        raise ValueError(f"Invalid location type: {loc}")
+
+    return locationType, locData
+
+
+def _packLocationNew(loc: grids.LocationBase):
+    if loc is None:
+        locationType = LOC_NONE
+        locData = [(0.0, 0.0, 0.0)]
+    elif loc.__class__ is grids.CoordinateLocation:
+        locationType = LOC_COORD
+        locData = [loc.indices]
+    elif loc.__class__ is grids.IndexLocation:
+        locationType = LOC_INDEX
+        locData = [loc.indices]
+    elif loc.__class__ is grids.MultiIndexLocation:
+        # encode number of sub-locations to allow in-line unpacking.
+        locationType = LOC_MULTI + f"{len(loc)}"
+        locData = [subloc.indices for subloc in loc]
+    else:
+        raise ValueError(f"Invalid location type: {loc}")
+
+    return locationType, locData
+
+
+def _unpackLocation(
+    locationTypes, locData, version: Tuple[int, int] = (DB_MAJOR, DB_MINOR)
+):
+    """
+    Convert location data as read from DB back into data structure for building reactor model.
+
+    location and locationType will only have different lengths
+    when multiindex locations are used.
+    """
+    oldStyle = version[0] == 3 and version[1] < 3
+    if oldStyle:
+        return _unpackLocationOld(locationTypes, locData)
+    else:
+        return _unpackLocationNew(locationTypes, locData)
+
+
+def _unpackLocationOld(locationTypes, locData):
+    """Delete when reading v <=3.2 DB's no longer wanted."""
+    locsIter = iter(locData)
+    unpackedLocs = []
+    for lt in locationTypes:
+        if lt == "None":
+            loc = next(locsIter)
+            unpackedLocs.append(None)
+        elif lt == "IndexLocation":
+            loc = next(locsIter)
+            # the data is stored as float, so cast back to int
+            unpackedLocs.append(tuple(int(i) for i in loc))
+        else:
+            loc = next(locsIter)
+            unpackedLocs.append(tuple(loc))
+    return unpackedLocs
+
+
+def _unpackLocationNew(locationTypes, locData):
+    locsIter = iter(locData)
+    unpackedLocs = []
+    for lt in locationTypes:
+        if lt == LOC_NONE:
+            loc = next(locsIter)
+            unpackedLocs.append(None)
+        elif lt == LOC_INDEX:
+            loc = next(locsIter)
+            # the data is stored as float, so cast back to int
+            unpackedLocs.append(tuple(int(i) for i in loc))
+        elif lt == LOC_COORD:
+            loc = next(locsIter)
+            unpackedLocs.append(tuple(loc))
+        elif lt.startswith(LOC_MULTI):
+            # extract number of sublocations from e.g. "M:345" string.
+            numSubLocs = int(lt.split(":")[1])
+            for _ in range(numSubLocs):
+                subLoc = next(locsIter)
+                # All multiindexes sublocs are index locs
+                unpackedLocs.append(tuple(int(i) for i in subLoc))
+        else:
+            raise ValueError(f"Read unknown location type {lt}. Invalid DB.")
+
+    return unpackedLocs
+
+
 class Layout(object):
     """
     The Layout class describes the hierarchical layout of the composite structure in a flat representation.
@@ -1300,7 +1435,7 @@ class Layout(object):
     Layout.
     """
 
-    def __init__(self, h5group=None, comp=None):
+    def __init__(self, h5group=None, comp=None, version=None):
         self.type = []
         self.name = []
         self.serialNum = []
@@ -1316,6 +1451,7 @@ class Layout(object):
         self._seenGridParams = dict()
         # actual list of grid parameters, with stable order for safe indexing
         self.gridParams = []
+        self.version = version
 
         self.groupedComps = collections.defaultdict(list)
 
@@ -1341,8 +1477,19 @@ class Layout(object):
         )
 
     def _createLayout(self, comp):
-        """Recursive function to populate a hierarchical representation and group the
-        items by type."""
+        """
+        Populate a hierarchical representation and group the reactor model items by type.
+
+        This is used when writing a reactor model to the database.
+
+        Notes
+        -----
+        This is recursive.
+
+        See Also
+        --------
+        _readLayout : does the opposite
+        """
         compList = self.groupedComps[type(comp)]
         compList.append(comp)
 
@@ -1361,12 +1508,7 @@ class Layout(object):
         else:
             self.gridIndex.append(None)
 
-        if comp.spatialLocator is None:
-            self.locationType.append("None")
-            self.location.append((0.0, 0.0, 0.0))
-        else:
-            self.locationType.append(comp.spatialLocator.__class__.__name__)
-            self.location.append(comp.spatialLocator.indices)
+        self._addSpatialLocatorData(comp.spatialLocator)
 
         try:
             self.temperatures.append((comp.inputTemperatureInC, comp.temperatureInC))
@@ -1376,17 +1518,31 @@ class Layout(object):
             self.material.append("")
 
         try:
-            comps = sorted([c for c in comp])
+            comps = sorted(list(comp))
         except ValueError:
             runLog.error(
                 "Failed to sort some collection of ArmiObjects for database output: {} "
-                "value {}".format(type(comp), [c for c in comp])
+                "value {}".format(type(comp), list(comp))
             )
             raise
 
         for c in comps:
             self._createLayout(c)
 
+    def _addSpatialLocatorData(self, locator):
+        """
+        Extend ``locationType`` and ``location`` attributes with location info.
+
+        Notes
+        -----
+        There are several types of locators and they must be encoded properly.
+        Most complicated are the MultiIndexLocations from grids, which
+        have multiple indices per component.
+        """
+        locationType, locations = _packLocation(locator, self.version)
+        self.locationType.append(locationType)
+        self.location.extend(locations)
+
     def _readLayout(self, h5group):
         try:
             # location is either an index, or a point
@@ -1395,16 +1551,7 @@ class Layout(object):
             self.locationType = numpy.char.decode(
                 h5group["layout/locationType"][:]
             ).tolist()
-            self.location = locs = []
-            for l, lt in zip(locations, self.locationType):
-                if lt == "None":
-                    locs.append(None)
-                elif lt == "IndexLocation":
-                    # the data is stored as float, so cast back to int
-                    locs.append(tuple(int(i) for i in l))
-                else:
-                    locs.append(tuple(l))
-
+            self.location = _unpackLocation(self.locationType, locations, self.version)
             self.type = numpy.char.decode(h5group["layout/type"][:])
             self.name = numpy.char.decode(h5group["layout/name"][:])
             self.serialNum = h5group["layout/serialNum"][:]
diff --git a/armi/bookkeeping/mainInterface.py b/armi/bookkeeping/mainInterface.py
index 24496215..31b57b44 100644
--- a/armi/bookkeeping/mainInterface.py
+++ b/armi/bookkeeping/mainInterface.py
@@ -22,8 +22,8 @@ import glob
 import os
 import subprocess
 import re
-
-from six.moves import zip_longest
+import shutil
+import itertools
 
 from armi import interfaces
 from armi import runLog
@@ -122,7 +122,7 @@ class MainInterface(interfaces.Interface):
             # if any files to copy, then use the first as the default, i.e. len() == 1,
             # otherwise assume '.'
             default = copyFilesTo[0] if any(copyFilesTo) else "."
-            for filename, dest in zip_longest(
+            for filename, dest in itertools.zip_longest(
                 copyFilesFrom, copyFilesTo, fillvalue=default
             ):
                 pathTools.copyOrWarn("copyFilesFrom", filename, dest)
@@ -165,6 +165,8 @@ class MainInterface(interfaces.Interface):
 
     def updateClusterProgress(self):
         """Updates the status window on the Windows HPC client."""
+        if not shutil.which("job"):
+            return
         totalSteps = max(
             (self.cs["burnSteps"] + 1) * self.cs["nCycles"] - 1, 1
         )  # 0 through 5 if 2 cycles
diff --git a/armi/cases/case.py b/armi/cases/case.py
index 75e83cb3..b6dd70d4 100644
--- a/armi/cases/case.py
+++ b/armi/cases/case.py
@@ -101,7 +101,7 @@ class Case:
         self.enabled = True
 
         # NOTE: in order to prevent slow submission times for loading massively large
-        # blueprints (e.g. ZPPR or other computer-generated input files),
+        # blueprints (e.g. certain computer-generated input files),
         # self.bp and self.geom can be None.
         self.cs = cs
         self._bp = bp
@@ -392,7 +392,7 @@ class Case:
 
     def initializeOperator(self, r=None):
         """Creates and returns an Operator."""
-        with DirectoryChanger(self.cs.inputDirectory):
+        with DirectoryChanger(self.cs.inputDirectory, dumpOnException=False):
             self._initBurnChain()
             o = operators.factory(self.cs)
             if not r:
@@ -465,7 +465,7 @@ class Case:
         """Uses the ReportInterface to create a fancy HTML page describing the design inputs."""
         settings.setMasterCs(self.cs)
         o = self.initializeOperator()
-        with DirectoryChanger(self.cs.inputDirectory):
+        with DirectoryChanger(self.cs.inputDirectory, dumpOnException=False):
             # There are global variables that are modified when a report is
             # generated, so reset it all
             six.moves.reload_module(report)  # pylint: disable=too-many-function-args
@@ -566,21 +566,35 @@ class Case:
         copyInterfaceInputs(self.cs, clone.cs.inputDirectory)
 
         with open(self.cs["loadingFile"], "r") as f:
-            for includePath, mark in textProcessors.findYamlInclusions(
-                f, root=pathlib.Path(self.cs.inputDirectory)
-            ):
-                if not includePath.exists():
+            # The root for handling YAML includes is relative to the YAML file, not the
+            # settings file
+            root = (
+                pathlib.Path(self.cs.inputDirectory)
+                / pathlib.Path(self.cs["loadingFile"]).parent
+            )
+            cloneRoot = (
+                pathlib.Path(clone.cs.inputDirectory)
+                / pathlib.Path(clone.cs["loadingFile"]).parent
+            )
+            for includePath, mark in textProcessors.findYamlInclusions(f, root=root):
+                if not includePath.is_absolute():
+                    includeSrc = root / includePath
+                    includeDest = cloneRoot / includePath
+                else:
+                    # don't bother copying absolute files
+                    continue
+                if not includeSrc.exists():
                     raise OSError(
                         "The input file file `{}` referenced at {} does not exist.".format(
-                            includePath, mark
+                            includeSrc, mark
                         )
                     )
                 pathTools.copyOrWarn(
                     "auxiliary input file `{}` referenced at {}".format(
-                        includePath, mark
+                        includeSrc, mark
                     ),
-                    fromPath(includePath),
-                    clone.cs.inputDirectory,
+                    includeSrc,
+                    includeDest,
                 )
 
         for fileName in additionalFiles or []:
@@ -652,7 +666,9 @@ class Case:
             Similar to this but doesn't let you write out new/modified
             geometry or blueprints objects
         """
-        with ForcedCreationDirectoryChanger(self.cs.inputDirectory):
+        with ForcedCreationDirectoryChanger(
+            self.cs.inputDirectory, dumpOnException=False
+        ):
             # trick: these seemingly no-ops load the bp and geom via properties if
             # they are not yet initialized.
             self.bp
diff --git a/armi/cases/suite.py b/armi/cases/suite.py
index bb4a07fd..bf94cedf 100644
--- a/armi/cases/suite.py
+++ b/armi/cases/suite.py
@@ -149,26 +149,27 @@ class CaseSuite:
         """
         Clone a CaseSuite to a new place.
 
-        Creates a clone for each case within a CaseSuite. If ``oldRoot`` is not specified, then each
-        case clone is made in a directory with the title of the case. If ``oldRoot`` is specified,
-        then a relative path from ``oldRoot`` will be used to determine a new relative path to the
-        current directory ``oldRoot``.
+        Creates a clone for each case within a CaseSuite. If ``oldRoot`` is not
+        specified, then each case clone is made in a directory with the title of the
+        case. If ``oldRoot`` is specified, then a relative path from ``oldRoot`` will be
+        used to determine a new relative path to the current directory ``oldRoot``.
 
         Parameters
         ----------
         oldRoot : str (optional)
-            root directory of original case suite used to help filter when a suite contains one or
-            more cases with the same case title.
+            root directory of original case suite used to help filter when a suite
+            contains one or more cases with the same case title.
 
         Notes
         -----
-        By design, a CaseSuite has no location dependence; this allows any set of cases to compose
-        a CaseSuite. The thought is that the post-analysis capabilities without restricting a root
-        directory could be beneficial. For example, this allows one to perform analysis on cases
-        analyzed by Person A and Person B, even if the analyses were performed in completely
-        different locations. As a consequence, when you want to clone, we need to infer a "root"
-        of the original cases to attempt to mirror whatever existing directory structure there
-        may have been.
+        By design, a CaseSuite has no location dependence; this allows any set of cases
+        to compose a CaseSuite. The thought is that the post-analysis capabilities
+        without restricting a root directory could be beneficial. For example, this
+        allows one to perform analysis on cases analyzed by Person A and Person B, even
+        if the analyses were performed in completely different locations. As a
+        consequence, when you want to clone, we need to infer a "root" of the original
+        cases to attempt to mirror whatever existing directory structure there may have
+        been.
         """
         clone = CaseSuite(self.cs.duplicate())
 
@@ -180,7 +181,9 @@ class CaseSuite:
                 newDir = os.path.dirname(os.path.relpath(case.cs.path, oldRoot))
             else:
                 newDir = case.title
-            with ForcedCreationDirectoryChanger(newDir, clean=True):
+            with ForcedCreationDirectoryChanger(
+                newDir, clean=True, dumpOnException=False
+            ):
                 clone.add(case.clone(modifiedSettings=modifiedSettings))
         return clone
 
@@ -190,8 +193,8 @@ class CaseSuite:
 
         .. warning: Suite running may not work yet if the cases have interdependencies.
                     We typically run on a HPC but are still working on a platform
-                    independent way of handling HPCs. 
-                    
+                    independent way of handling HPCs.
+
         """
         for case in self:
             case.run()
diff --git a/armi/operators/settingsValidation.py b/armi/operators/settingsValidation.py
index 52003d7d..a3dd53db 100644
--- a/armi/operators/settingsValidation.py
+++ b/armi/operators/settingsValidation.py
@@ -291,7 +291,9 @@ class Inspector:
 
     def _setGeomType(self):
         if self.cs["geomFile"]:
-            with directoryChangers.DirectoryChanger(self.cs.inputDirectory):
+            with directoryChangers.DirectoryChanger(
+                self.cs.inputDirectory, dumpOnException=False
+            ):
                 geom = geometry.SystemLayoutInput()
                 geom.readGeomFromFile(self.cs["geomFile"])
 
diff --git a/armi/physics/neutronics/fissionProductModel/fissionProductModel.py b/armi/physics/neutronics/fissionProductModel/fissionProductModel.py
index 9927dec0..5918f1c1 100644
--- a/armi/physics/neutronics/fissionProductModel/fissionProductModel.py
+++ b/armi/physics/neutronics/fissionProductModel/fissionProductModel.py
@@ -26,16 +26,17 @@ All other modules can just assume there's a LFP collection and use it as needed.
 Examples
 --------
 
-from armi.physics.neutronics.fissionProductModel import fissionProductModel
-fpInterface = fissionProductModel.FissionProductModel()
-lfp = fpInterface.getGlobalLumpedFissionProducts()
-lfp['LFP35']
-lfp35 = lfp['LFP35']
-lfp35.printDensities(0.05)
-lfp35.values()
-allFPs = [(fpY, fpNuc) for (fpNuc,fpY) in lfp35.items()]
-allFPs.sort()
-lfp35.keys()
+    from armi.physics.neutronics.fissionProductModel import fissionProductModel
+    fpInterface = fissionProductModel.FissionProductModel()
+    lfp = fpInterface.getGlobalLumpedFissionProducts()
+    lfp['LFP35']
+    lfp35 = lfp['LFP35']
+    lfp35.printDensities(0.05)
+    lfp35.values()
+    allFPs = [(fpY, fpNuc) for (fpNuc,fpY) in lfp35.items()]
+    allFPs.sort()
+    lfp35.keys()
+
 """
 
 from armi import runLog
diff --git a/armi/reactor/blueprints/__init__.py b/armi/reactor/blueprints/__init__.py
index 9fc88abd..1daf753f 100644
--- a/armi/reactor/blueprints/__init__.py
+++ b/armi/reactor/blueprints/__init__.py
@@ -70,7 +70,6 @@ import pathlib
 import traceback
 import typing
 
-import ruamel
 import tabulate
 import yamlize
 import yamlize.objects
@@ -108,12 +107,10 @@ def loadFromCs(cs):
     # pylint: disable=import-outside-toplevel; circular import protection
     from armi.utils import directoryChangers
 
-    textProcessors.registerYamlIncludeConstructor()
-
-    with directoryChangers.DirectoryChanger(cs.inputDirectory):
+    with directoryChangers.DirectoryChanger(cs.inputDirectory, dumpOnException=False):
         with open(cs["loadingFile"], "r") as bpYaml:
-            # Make sure that the !include constructor is registered
-            bpYaml = textProcessors.resolveMarkupInclusions(bpYaml)
+            root = pathlib.Path(cs["loadingFile"]).parent.absolute()
+            bpYaml = textProcessors.resolveMarkupInclusions(bpYaml, root)
             try:
                 bp = Blueprints.load(bpYaml)
             except yamlize.yamlizing_error.YamlizingError as err:
diff --git a/armi/reactor/grids.py b/armi/reactor/grids.py
index 374dc812..e6497316 100644
--- a/armi/reactor/grids.py
+++ b/armi/reactor/grids.py
@@ -429,6 +429,10 @@ class MultiIndexLocation(IndexLocation):
     def pop(self, location: IndexLocation):
         self._locations.pop(location)
 
+    @property
+    def indices(self):
+        raise NotImplementedError
+
 
 class CoordinateLocation(IndexLocation):
     """
diff --git a/armi/settings/__init__.py b/armi/settings/__init__.py
index efc9d77a..35c3ffff 100644
--- a/armi/settings/__init__.py
+++ b/armi/settings/__init__.py
@@ -38,6 +38,8 @@ import glob
 import shutil
 from typing import Union
 
+from ruamel import yaml
+
 import armi
 from armi import runLog
 from armi.localization import exceptions
@@ -120,6 +122,12 @@ def recursivelyLoadSettingsFiles(
             runLog.extra("loaded {}".format(possibleSettingsFile))
         except exceptions.InvalidSettingsFileError as ee:
             runLog.info("skipping {}\n    {}".format(possibleSettingsFile, ee))
+        except yaml.composer.ComposerError as ee:
+            runLog.info(
+                "skipping {}; it appears to be an incomplete YAML snippet\n    {}".format(
+                    possibleSettingsFile, ee
+                )
+            )
         except Exception as ee:
             runLog.error(
                 "Failed to parse {}.\nIt looked like a settings file but gave this exception:\n{}: {}".format(
diff --git a/armi/settings/settingsIO.py b/armi/settings/settingsIO.py
index 2ab7fadf..10e32927 100644
--- a/armi/settings/settingsIO.py
+++ b/armi/settings/settingsIO.py
@@ -101,7 +101,10 @@ class _SettingsReader(object):
         self.format = self.FORMAT_FROM_EXT[ext]
         self.inputPath = path
         with open(path, "r") as f:
-            self.readFromStream(f, handleInvalids, self.format)
+            try:
+                self.readFromStream(f, handleInvalids, self.format)
+            except Exception as ee:
+                raise exceptions.InvalidSettingsFileError(path, str(ee))
 
     def readFromStream(self, stream, handleInvalids=True, fmt=SettingsInputFormat.YAML):
         """Read from a file-like stream."""
diff --git a/armi/utils/textProcessors.py b/armi/utils/textProcessors.py
index e2f9fd8d..706d9e35 100644
--- a/armi/utils/textProcessors.py
+++ b/armi/utils/textProcessors.py
@@ -18,10 +18,7 @@ import os
 import re
 import io
 import pathlib
-from typing import List, Tuple, Union, Optional
-
-import ruamel
-from ruamel.yaml.error import FileMark
+from typing import List, Tuple, Union, Optional, TextIO
 
 from armi import runLog
 from armi.utils import directoryChangers
@@ -29,63 +26,103 @@ from armi.localization import strings
 
 
 _INCLUDE_CTOR = False
-
-
-def registerYamlIncludeConstructor():
+_INCLUDE_RE = re.compile(r"^([^#]*\s+)?!include\s+(.*)\n?$")
+_INDENT_RE = re.compile(r"^[\s\-\?:]*([^\s\-\?:].*)?$")
+
+
+class FileMark:
+    def __init__(self, fName, line, column, relativeTo):
+        self.path = fName
+        self.line = line
+        self.column = column
+        # if the path is relative, where is it relative to? We need this to be able to
+        # normalize relative paths to a root file.
+        self.relativeTo = relativeTo
+
+    def __str__(self):
+        return "{}, line {}, column {}".format(self.path, self.line, self.column)
+
+
+def _processIncludes(
+    src,
+    out,
+    includes: List[Tuple[pathlib.Path, FileMark]],
+    root,
+    indentation=0,
+    currentFile="<stream>",
+):
     """
-    Register a custom !include tag constructor with ruamel.yaml.
+    This is the workhorse of ``resolveMarkupInclusions`` and friends.
 
-    This teaches the ruamel.yaml parser to interpret ``!include`` tags as essentially a
-    textual inclusion. This is somewhat of a weird feature, as tags are typically used
-    to represent custom objects in YAML, rather than to directly manipulate the YAML
-    itself. As such it leads to interesting interactions with the yamlize library that
-    we use to do much of our input processing.
-
-    See Also
-    --------
-    resolveMarkupInclusions
+    Recursively inserts the contents of !included YAML files into the output stream,
+    keeping track of indentation and a list of included files along the way.
     """
-    global _INCLUDE_CTOR
-    if _INCLUDE_CTOR:
-        # already done; short circuit
-        return
 
-    def includeCtor(loader, node):
-        y = loader.loader
-        typ = getattr(y, "typ", None)
-        pure = getattr(y, "pure", None)
-        yaml = ruamel.yaml.YAML(typ=typ, pure=pure)
-        path = pathlib.Path(os.getcwd()) / node.value
-        return yaml.load(path)
+    def _beginningOfContent(line: str) -> int:
+        """
+        Return the position of the first "content" character.
 
-    ruamel.yaml.Constructor.add_constructor("!include", includeCtor)
-    ruamel.yaml.constructor.SafeConstructor.add_constructor("!include", includeCtor)
-    ruamel.yaml.constructor.RoundTripConstructor.add_constructor("!include", includeCtor)
+        This follows the YAML spec at https://yaml.org/spec/current.html#id2519916
 
-    _INCLUDE_CTOR = True
+        In short, it will return the position of the first character that is not
+        whitespace or one of the special "block collection" markers ("-", "?", and ":")
+        """
+        m = _INDENT_RE.match(line)
+        if m and m.group(1) is not None:
+            return m.start(1)
+        else:
+            return 0
+
+    indentSpace = " " * indentation
+    for i, line in enumerate(src.readlines()):
+        leadingSpace = indentSpace if i > 0 else ""
+        m = _INCLUDE_RE.match(line)
+        if m:
+            # this line has an !include on it
+            if m.group(1) is not None:
+                out.write(leadingSpace + m.group(1))
+            fName = pathlib.Path(m.group(2))
+            path = root / fName
+            if not path.exists():
+                raise ValueError(
+                    "The !included file, `{}` does not exist from {}!".format(
+                        fName, root
+                    )
+                )
+            includes.append((fName, FileMark(currentFile, i, m.start(2), root)))
+
+            with open(path, "r") as includedFile:
+                firstCharacterPos = _beginningOfContent(line)
+                newIndent = indentation + firstCharacterPos
+                _processIncludes(
+                    includedFile,
+                    out,
+                    includes,
+                    path.parent,
+                    indentation=newIndent,
+                    currentFile=path,
+                )
+        else:
+            out.write(leadingSpace + line)
 
 
 def resolveMarkupInclusions(
-    src: Union[io.TextIOBase, pathlib.Path],
-    typ: str = None,
-    root: Optional[pathlib.Path] = None,
+    src: Union[TextIO, pathlib.Path], root: Optional[pathlib.Path] = None
 ) -> io.StringIO:
     """
-    Process a YAML stream, appropriately handling ``!include`` tags."
+    Process a text stream, appropriately handling ``!include`` tags.
+
+    This will take the passed IO stream or file path, replacing any instances of
+    ``!include [path]`` with the appropriate contents of the ``!include`` file.
 
-    This will take the passed string or file path, and attempt to parse it as YAML. In
-    the process, any instances of ``!include [path]`` will be replaced with the
-    appropriate contents of the ``!include`` file.
+    What is returned is a new text stream, containing the contents of all of the files
+    stitched together.
 
     Parameters
     ----------
     src : TextIOBase or Path
-        If a Path is provided, read YAML from there. If is stream is provided, consume
-        read text from the stream. If a stream is provided, ``root`` must also be
-        provided.
-    typ : str
-        A valid parse type for the ruamel YAML library. Defaults to "rt" to preserve
-        round-trip data.
+        If a Path is provided, read text from there. If is stream is provided, consume
+        text from the stream. If a stream is provided, ``root`` must also be provided.
     root : Optional Path
         The root directory to use for resolving relative paths in !include tags. If a
         stream is provided for ``src``, ``root`` must be provided. Otherwise, the
@@ -93,79 +130,108 @@ def resolveMarkupInclusions(
 
     Notes
     -----
-    This does a full (albeit not Yamlize) parse of the input YAML into the appropriate
-    data structure, resolving the textual inclusions along the way. Then, the resulting
-    data are re-serialized to a new YAML stream. As such, this is not super efficient.
-    We do it this way because we don't have a whole lot of control over how yamlize
-    handles the parsing. For instance, yamlize will not honor the custom !include tag.
-    This sort of makes sense, as yamlize and YAML tags are two distinct approaches to
-    representing custom objects in YAML.
-
-    For now, this is not handling cross-document anchors and aliases, which sucks. We
-    may wish to resolve this, but methods to do so are rather complex and not without
-    their own drawbacks. There is some pure gold on that topic here:
+    While the use of ``!include`` appears as though it would invoke some sort of special
+    custom YAML constructor code, this does not do that. Processing these inclusions as
+    part of the document parsing/composition that comes with pyyaml or ruamel.yaml could
+    work, but has a number of prohibitive drawbacks (or at least reasons why it might
+    not be worth doing). Using a custom constructor is more-or-less supported by
+    ruamel.yaml (which we do use, as it is what underpins the yamlize package), but it
+    carries limitations about how anchors and aliases can cross included-file
+    boundaries. Getting around this requires either monkey-patching ruamel.yaml, or
+    subclassing it, which in turn would require monkey-patching yamlize.
+
+    Instead, we treat the ``!include``\ s as a sort of pre-processor directive, which
+    essentially pastes the contents of the ``!include``\ d file into the location of the
+    ``!include``. The result is a text stream containing the entire contents, with all
+    ``!include``\ s resolved. The only degree of sophistication lies in how indentation
+    is handled; since YAML cares about indentation to keep track of object hierarchy,
+    care must be taken that the included file contents are indented appropriately.
+
+    To precisely describe how the indentation works, it helps to have some definitions:
+
+     - Included file: The file specified in the ``!include [Included file]``
+     - Including line: The line that actually contains the ``!include [Included file]``
+     - Meaningful YAML content: Text in a YAML file that is not either indentation or a
+       special character like "-", ":" or "?".
+
+    The contents of the included file will be indented such that that the first
+    character of each line in the included file will be found at the first column in the
+    including line that contains meaningful YAML content. The only exception is the
+    first line of the included file, which starts at the location of the ``!include``
+    itself and is not deliberately indented.
+
+    In the future, we may wish to do the more sophisticated processing of the
+    ``!include``\ s as part of the YAML parse. For future reference, there is some pure
+    gold on that topic here:
     https://stackoverflow.com/questions/44910886/pyyaml-include-file-and-yaml-aliases-anchors-references
     """
-    registerYamlIncludeConstructor()
+    return _resolveMarkupInclusions(src, root)[0]
 
+
+def _getRootFromSrc(
+    src: Union[TextIO, pathlib.Path], root: Optional[pathlib.Path]
+) -> pathlib.Path:
     if isinstance(src, pathlib.Path):
         root = root or src.parent.absolute()
+    elif isinstance(src, io.TextIOBase):
+        if root is None:
+            raise ValueError("A stream was provided without a root directory.")
     else:
-        root = root or pathlib.Path(os.getcwd()).absolute()
-
-    with directoryChangers.DirectoryChanger(root):
-        yaml = ruamel.yaml.YAML(typ=typ, pure=True)
-        data = yaml.load(src)
-
-    out = io.StringIO()
-
-    yaml.dump(data, out)
-    out.seek(0)
-    if isinstance(src, io.TextIOBase):
-        src.seek(0)
+        raise TypeError("Unsupported source type: `{}`!".format(type(src)))
 
-    return out
+    return root
 
 
 def findYamlInclusions(
-    src: Union[io.TextIOBase, pathlib.Path], root: Optional[pathlib.Path] = None
+    src: Union[TextIO, pathlib.Path], root: Optional[pathlib.Path] = None
 ) -> List[Tuple[pathlib.Path, FileMark]]:
     """
-    Return a list of paths to ``!include`` files.
+    Return a list containing all of the !included YAML files from a root file.
 
-    src : TextIOBase or Path
-        If a Path is provided, read YAML from there. If is stream is provided, consume
-        read text from the stream. If a stream is provided, ``root`` must also be
-        provided.
-    root : Optional Path
-        The root directory to use for resolving relative paths in !include tags. If a
-        stream is provided for ``src``, the current working directory is implied.
-        Otherwise, the directory containing the ``src`` path will be used by default.
-
-    This is useful for determining which files need to be copied in order to have a
-    complete input, without explicitly inserting the content from ``!include`` tags.
+    This will attempt to "normalize" relative paths to the passed root. If that is not
+    possible, then an absolute path will be used instead. For example, if a file (A)
+    !includes another file (B) by an absolute path, which in turn !includes more files
+    relative to (B), all of (B)'s relative includes will be turned into absolute paths
+    from the perspective of the root file (A).
     """
-    if isinstance(src, pathlib.Path):
-        root = root or src.parent.absolute()
-    else:
-        root = root or pathlib.Path(os.getcwd()).absolute()
+    includes = _resolveMarkupInclusions(src, root)[1]
+    root = _getRootFromSrc(src, root)
+    normalizedIncludes = []
 
-    includes = []
+    for path, mark in includes:
+        if not path.is_absolute():
+            try:
+                path = (mark.relativeTo / path).relative_to(root or os.getcwd())
+            except ValueError as _:
+                # Can't make a relative path. IMO, pathlib gives up a little too early,
+                # but we still probably want to decay to absolute paths if the files
+                # arent in the same tree.
+                path = (mark.relativeTo / path).absolute()
 
-    def includeCtor(loader, node):
-        y = loader.loader
-        yaml = ruamel.yaml.YAML(typ=y.typ, pure=y.pure)
-        includes.append((root / pathlib.Path(node.value), node.start_mark))
-        return yaml.load(root / pathlib.Path(node.value))
+        normalizedIncludes.append((path, mark))
 
-    yaml = ruamel.yaml.YAML(typ="safe", pure=True)
-    yaml.Constructor.add_constructor("!include", includeCtor)
-    yaml.load(src)
+    return normalizedIncludes
 
-    if isinstance(src, io.TextIOBase):
-        src.seek(0)
 
-    return includes
+def _resolveMarkupInclusions(
+    src: Union[TextIO, pathlib.Path], root: Optional[pathlib.Path] = None
+) -> Tuple[io.StringIO, List[Tuple[pathlib.Path, FileMark]]]:
+    root = _getRootFromSrc(src, root)
+
+    if isinstance(src, pathlib.Path):
+        # this is inefficient, but avoids having to play with io buffers
+        with open(src, "r") as rootFile:
+            src = io.StringIO(rootFile.read())
+
+    out = io.StringIO()
+    includes = []
+    _processIncludes(src, out, includes, root)
+
+    out.seek(0)
+    # be kind; rewind
+    src.seek(0)
+
+    return out, includes
 
 
 class SequentialReader:
diff --git a/doc/user/tutorials/walkthrough_lwr_inputs.rst b/doc/user/tutorials/walkthrough_lwr_inputs.rst
index 7d841cd5..f5d783f0 100644
--- a/doc/user/tutorials/walkthrough_lwr_inputs.rst
+++ b/doc/user/tutorials/walkthrough_lwr_inputs.rst
@@ -11,6 +11,18 @@ in `NEA/NSC/DOC(2003)16 <https://www.oecd-nea.org/science/docs/2003/nsc-doc2003-
 .. tip:: The full inputs created in this tutorial are available for download at the bottom of
 	this page.
 
+.. warning:: ARMI was historically developed in support of fast reactors and most
+    features have been used and tested in fast reactor contexts. This
+    tutorial shows that simple LWR cases can be defined in input,
+    but there is still a lot of work to make sure all ARMI capabilities
+    are operational in this context. Thus, be warned that
+    as of 2020, doing LWR analysis with ARMI will certainly require
+    new developments. We are excited to expand ARMI scope fully
+    into LWR relevant analysis.
+
+    In particular the handling of detailed locations within a block is
+    relatively experimental (fast reactors usually just smear it out).
+
 Setting up the blueprints
 =========================
 This tutorial is shorter than the previous, focusing mostly on the new information.


Test patch:
diff --git a/armi/bookkeeping/db/tests/test_database3.py b/armi/bookkeeping/db/tests/test_database3.py
index 8f1b24ba..ce53dd3e 100644
--- a/armi/bookkeeping/db/tests/test_database3.py
+++ b/armi/bookkeeping/db/tests/test_database3.py
@@ -13,16 +13,15 @@
 # limitations under the License.
 
 import unittest
-import os
 
 import numpy
-import numpy.testing
 import h5py
 
+from armi.bookkeeping.db import database3 as database
+from armi.reactor import grids
 from armi.reactor.tests import test_reactors
-from armi.tests import TEST_ROOT, ARMI_RUN_PATH
 
-from armi.bookkeeping.db import database3 as database
+from armi.tests import TEST_ROOT
 
 
 class TestDatabase3(unittest.TestCase):
@@ -112,8 +111,12 @@ class TestDatabase3(unittest.TestCase):
         self.r.p.timeNode = 0
         tnGroup = self.db.getH5Group(self.r)
         database._writeAttrs(
-            tnGroup["layout/serialNum"], tnGroup, {"fakeBigData": numpy.eye(6400),
-                "someString": "this isn't a reference to another dataset"}
+            tnGroup["layout/serialNum"],
+            tnGroup,
+            {
+                "fakeBigData": numpy.eye(6400),
+                "someString": "this isn't a reference to another dataset",
+            },
         )
 
         db2 = database.Database3("restartDB.h5", "w")
@@ -124,8 +127,10 @@ class TestDatabase3(unittest.TestCase):
             tnGroup = db2.getH5Group(self.r)
 
             # this test is a little bit implementation-specific, but nice to be explicit
-            self.assertEqual(tnGroup["layout/serialNum"].attrs["fakeBigData"],
-                    "@/c01n00/attrs/0_fakeBigData")
+            self.assertEqual(
+                tnGroup["layout/serialNum"].attrs["fakeBigData"],
+                "@/c01n00/attrs/0_fakeBigData",
+            )
 
             # actually exercise the _resolveAttrs function
             attrs = database._resolveAttrs(tnGroup["layout/serialNum"].attrs, tnGroup)
@@ -148,6 +153,31 @@ class TestDatabase3(unittest.TestCase):
             self.assertTrue(newDb.attrs["databaseVersion"] == database.DB_VERSION)
 
 
+class Test_LocationPacking(unittest.TestCase):
+    def test_locationPacking(self):
+        # pylint: disable=protected-access
+        loc1 = grids.IndexLocation(1, 2, 3, None)
+        loc2 = grids.CoordinateLocation(4.0, 5.0, 6.0, None)
+        loc3 = grids.MultiIndexLocation(None)
+        loc3.append(grids.IndexLocation(7, 8, 9, None))
+        loc3.append(grids.IndexLocation(10, 11, 12, None))
+
+        tp, data = database._packLocation(loc1)
+        self.assertEqual(tp, database.LOC_INDEX)
+        unpacked = database._unpackLocation([tp], data)
+        self.assertEqual(unpacked[0], (1, 2, 3))
+
+        tp, data = database._packLocation(loc2)
+        self.assertEqual(tp, database.LOC_COORD)
+        unpacked = database._unpackLocation([tp], data)
+        self.assertEqual(unpacked[0], (4.0, 5.0, 6.0))
+
+        tp, data = database._packLocation(loc3)
+        self.assertEqual(tp, database.LOC_MULTI + "2")
+        unpacked = database._unpackLocation([tp], data)
+        self.assertEqual(unpacked[0], (7, 8, 9))
+
+
 if __name__ == "__main__":
     import sys
 
diff --git a/armi/reactor/blueprints/tests/test_blueprints.py b/armi/reactor/blueprints/tests/test_blueprints.py
index 0e9031b6..7be7adec 100644
--- a/armi/reactor/blueprints/tests/test_blueprints.py
+++ b/armi/reactor/blueprints/tests/test_blueprints.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 
 """Tests the blueprints (loading input) file"""
+import os
 import pathlib
 import unittest
 
@@ -55,10 +56,11 @@ class TestBlueprints(unittest.TestCase):
         cls.directoryChanger.open()
         isotopicDepletion.applyDefaultBurnChain()
 
-        with open("refSmallReactor.yaml", "r") as y:
-            y = textProcessors.resolveMarkupInclusions(y)
-            cls.blueprints = blueprints.Blueprints.load(y)
-            cls.blueprints._prepConstruction(cls.cs)
+        y = textProcessors.resolveMarkupInclusions(
+            pathlib.Path(os.getcwd()) / "refSmallReactor.yaml"
+        )
+        cls.blueprints = blueprints.Blueprints.load(y)
+        cls.blueprints._prepConstruction(cls.cs)
 
     @classmethod
     def tearDownClass(cls):
diff --git a/armi/reactor/tests/test_assemblies.py b/armi/reactor/tests/test_assemblies.py
index dd3b1ce1..747ca95c 100644
--- a/armi/reactor/tests/test_assemblies.py
+++ b/armi/reactor/tests/test_assemblies.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 
 """Tests assemblies.py"""
+import pathlib
 import random
 import unittest
 
@@ -576,7 +577,9 @@ class Assembly_TestCase(unittest.TestCase):
         with directoryChangers.DirectoryChanger(TEST_ROOT):
             self.cs["loadingFile"] = "refSmallReactor.yaml"
             with open(self.cs["loadingFile"], "r") as y:
-                y = textProcessors.resolveMarkupInclusions(y)
+                y = textProcessors.resolveMarkupInclusions(
+                    y, pathlib.Path(self.cs.inputDirectory)
+                )
                 self.r.blueprints = blueprints.Blueprints.load(y)
 
             self.r.blueprints._prepConstruction(self.cs)
diff --git a/armi/tests/refSmallReactor.yaml b/armi/tests/refSmallReactor.yaml
index 9064eb08..059fbcfd 100644
--- a/armi/tests/refSmallReactor.yaml
+++ b/armi/tests/refSmallReactor.yaml
@@ -444,4 +444,5 @@ grids:
         lattice pitch:
             x: 50.0
             y: 50.0
-        grid contents: !include sfpGeom.yaml
+        grid contents:
+            !include sfpGeom.yaml
diff --git a/armi/tests/test_lwrInputs.py b/armi/tests/test_lwrInputs.py
index a17aa7d0..91ea2bd3 100644
--- a/armi/tests/test_lwrInputs.py
+++ b/armi/tests/test_lwrInputs.py
@@ -19,6 +19,9 @@ from armi.utils import directoryChangers
 from armi.tests import TEST_ROOT
 from armi.reactor.flags import Flags
 import armi
+from armi.bookkeeping import db
+
+TEST_INPUT_TITLE = "c5g7-settings"
 
 
 class C5G7ReactorTests(unittest.TestCase):
@@ -38,7 +41,7 @@ class C5G7ReactorTests(unittest.TestCase):
         """
         Load the C5G7 case and check basic counts.
         """
-        o = armi.init(fName="c5g7-settings.yaml")
+        o = armi.init(fName=TEST_INPUT_TITLE + ".yaml")
         b = o.r.core.getFirstBlock(Flags.MOX)
         # there are 100 of each high, medium, and low MOX pins
         fuelPinsHigh = b.getComponent(Flags.HIGH | Flags.MOX)
@@ -46,3 +49,11 @@ class C5G7ReactorTests(unittest.TestCase):
 
         gt = b.getComponent(Flags.GUIDE_TUBE)
         self.assertEqual(gt.getDimension("mult"), 24)
+
+    def test_runC5G7(self):
+        """
+        Run C5G7 in basic no-op app and load from the result.
+        """
+        o = armi.init(fName=TEST_INPUT_TITLE + ".yaml")
+        o.operate()
+        o2 = db.loadOperator(TEST_INPUT_TITLE + ".h5", 0, 0)
diff --git a/armi/tests/tutorials/anl-afci-177-blueprints.yaml b/armi/tests/tutorials/anl-afci-177-blueprints.yaml
index e86e8425..0b51be14 100644
--- a/armi/tests/tutorials/anl-afci-177-blueprints.yaml
+++ b/armi/tests/tutorials/anl-afci-177-blueprints.yaml
@@ -338,7 +338,8 @@ systems:
           y: 0.0
           z: 0.0
 grids:
-    core: !include anl-afci-177-coreMap.yaml
+    core:
+        !include anl-afci-177-coreMap.yaml
 
 # end-systems-section
 
diff --git a/armi/tests/tutorials/c5g7-settings.yaml b/armi/tests/tutorials/c5g7-settings.yaml
index a68d70e6..557803ee 100644
--- a/armi/tests/tutorials/c5g7-settings.yaml
+++ b/armi/tests/tutorials/c5g7-settings.yaml
@@ -12,3 +12,4 @@ settings:
   comment: C5G7 LWR Benchmark inputs
   genXS: Neutron
   numProcessors: 1
+  genReports: false
diff --git a/armi/utils/tests/resources/includeA.yaml b/armi/utils/tests/resources/lower/includeA.yaml
similarity index 100%
rename from armi/utils/tests/resources/includeA.yaml
rename to armi/utils/tests/resources/lower/includeA.yaml
index bdc4be3f..74ae1e98 100644
--- a/armi/utils/tests/resources/includeA.yaml
+++ b/armi/utils/tests/resources/lower/includeA.yaml
@@ -1,4 +1,4 @@
-# some comment in includeA
 full_name: Jennifer Person
+# some comment in includeA
 children:
     !include includeB.yaml
diff --git a/armi/utils/tests/resources/includeB.yaml b/armi/utils/tests/resources/lower/includeB.yaml
similarity index 100%
rename from armi/utils/tests/resources/includeB.yaml
rename to armi/utils/tests/resources/lower/includeB.yaml
diff --git a/armi/utils/tests/resources/root.yaml b/armi/utils/tests/resources/root.yaml
index 3fb12f98..e5b07d37 100644
--- a/armi/utils/tests/resources/root.yaml
+++ b/armi/utils/tests/resources/root.yaml
@@ -4,6 +4,7 @@ bobby: &bobby
 
 billy:
     full_name: William Person
+    # comment
     children:
         - *bobby
-        - !include includeA.yaml
+        - !include lower/includeA.yaml
diff --git a/armi/utils/tests/test_textProcessors.py b/armi/utils/tests/test_textProcessors.py
index aa961ddc..18a898ca 100644
--- a/armi/utils/tests/test_textProcessors.py
+++ b/armi/utils/tests/test_textProcessors.py
@@ -30,7 +30,7 @@ RES_DIR = os.path.join(THIS_DIR, "resources")
 
 
 class YamlIncludeTest(unittest.TestCase):
-    def testIncludeCtor(self):
+    def testResolveIncludes(self):
         with open(os.path.join(RES_DIR, "root.yaml")) as f:
             resolved = textProcessors.resolveMarkupInclusions(
                 f, root=pathlib.Path(RES_DIR)
@@ -66,9 +66,11 @@ class YamlIncludeTest(unittest.TestCase):
         self.assertTrue(anchorFound)
 
     def testFindIncludes(self):
-        includes = textProcessors.findYamlInclusions(pathlib.Path(RES_DIR) / "root.yaml")
+        includes = textProcessors.findYamlInclusions(
+            pathlib.Path(RES_DIR) / "root.yaml"
+        )
         for i, _mark in includes:
-            self.assertTrue(i.exists())
+            self.assertTrue((RES_DIR/i).exists())
 
         self.assertEqual(len(includes), 2)