Repository: terrapower/armi
Pull request #1355 (2023-07-20 23:16:50)

Update reactivity coefficient parameters
## What is the change?

Update the parameter definition for some reactivity coefficient parameters to get the correct uniform mesh mapping behavior (change from `ParamLocation.AVERAGE` to `ParamLocation.VOLUME_INTEGRATED`).

Also changed some parameter defaults (`None` for list/array-like, `0.0` for scalar floats).

Simplified the logic for getting parameter values to be mapped in the uniform mesh converter.

## Why is the change being made?

Certain parameters were not being mapped correctly because they were listed with the wrong `ParamLocation` tag.

<!-- Optional: Link to any related GitHub Issues -->

---

## Checklist

<!--
    You (the pull requester) should put an `x` in the boxes below you have completed.
    If you're unsure about any of them, don't hesitate to ask. We're here to help!
    Learn what a "good PR" looks like here:
    https://terrapower.github.io/armi/developer/tooling.html#good-pull-requests
-->

- [x] This PR has only one purpose or idea.
- [x] Tests have been added/updated to verify that the new/changed code works.

<!-- Check the code quality -->

- [x] The code style follows [good practices](https://terrapower.github.io/armi/developer/standards_and_practices.html).
- [x] The commit message(s) follow [good practices](https://terrapower.github.io/armi/developer/tooling.html).

<!-- Check the project-level cruft -->

- [x] The [release notes](https://terrapower.github.io/armi/release/index.html) (location `doc/release/0.X.rst`) are up-to-date with any important changes.
- [x] The documentation is still up-to-date in the `doc` folder.
- [x] The dependencies are still up-to-date in `setup.py`.



Patch:
diff --git a/.github/workflows/black.yaml b/.github/workflows/black.yaml
index 513b2fb8..4cad8b26 100644
--- a/.github/workflows/black.yaml
+++ b/.github/workflows/black.yaml
@@ -8,10 +8,10 @@ jobs:
         runs-on: ubuntu-22.04
         steps:
             - uses: actions/checkout@v2
-            - name: Set up Python 3.10
+            - name: Set up Python 3.11
               uses: actions/setup-python@v2
               with:
-                python-version: '3.10'
+                python-version: '3.11'
             - name: Install Black
               run: pip install 'black==20.8b1' 'click==8.0.1'
             - name: Run black --check .
diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml
index fd049661..0e85a640 100644
--- a/.github/workflows/coverage.yaml
+++ b/.github/workflows/coverage.yaml
@@ -8,19 +8,16 @@ on:
 
 jobs:
   build:
-
     runs-on: ubuntu-22.04
-
     env:
       GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
       COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
-
     steps:
       - uses: actions/checkout@v2
       - name: Setup Python
         uses: actions/setup-python@v2
         with:
-          python-version: '3.10'
+          python-version: '3.11'
       - name: Update package index
         run: sudo apt-get update
       - name: Install mpi libs
@@ -31,17 +28,8 @@ jobs:
         run: tox -e cov1 || true
       - name: Run Coverage Part 2
         run: tox -e cov2
-      - name: Convert Coverage Results
-        env:
-          COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
-        run: |
-          pip install click==8.1.3
-          pip install coverage==6.5.0
-          pip install coveragepy-lcov==0.1.2
-          coveragepy-lcov --data_file_path coverage_results.cov --output_file_path lcov.txt
       - name: Publish to coveralls.io
         uses: coverallsapp/github-action@v1.1.2
         with:
           github-token: ${{ secrets.GITHUB_TOKEN }}
-          path-to-lcov: lcov.txt
-
+          path-to-lcov: coverage.lcov
diff --git a/.github/workflows/validatemanifest.yaml b/.github/workflows/validatemanifest.yaml
index c9504a0d..c9e86c37 100644
--- a/.github/workflows/validatemanifest.yaml
+++ b/.github/workflows/validatemanifest.yaml
@@ -12,7 +12,7 @@ jobs:
       - name: Setup Python
         uses: actions/setup-python@v2
         with:
-          python-version: '3.10'
+          python-version: '3.11'
       - name: Install Tox and any other packages
         run: pip install tox
       - name: Run Tox
diff --git a/.gitignore b/.gitignore
index 9b209fa9..83bb002f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,6 +24,7 @@ doc/gallery
 doc/gallery-src/framework/*.yaml
 .coverage
 coverage.xml
+coverage.lcov
 coverage_results.*
 htmlcov/
 monkeytype.*
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8ab8b974..da7a88d9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,77 +1,3 @@
-# How to contribute
+# How to Contribute
 
-The ARMI framework project strongly encourages developers to help contribute to the codebase.
-
-The ARMI framework code is open source, and your contributions will become open source.
-Although fewer laws apply to open source materials because they are publicly-available, you still
-must comply with all applicable laws and regulations.
-
-## Help Wanted
-
-There are a lot of things we need help with right off the bat, to get your feet wet:
-
-* Many more type annotations are desired. Type issues cause lots of bugs.
-* Fewer Pylint warnings
-* Better documentation
-* Better test coverage
-* Targeted speedups (e.g. informed by a profiler)
-* Additional relevance to thermal reactors
-
-Naturally, we encourage other kinds of contributions as well.
-
-## Testing
-
-Any contribution must pass all included unit tests. The tests are built and run with the
-`pytest` system. Please add new tests if you add new functionality. You can generally just run
-`tox` to build the testing environment and execute all the tests and ruff checks.
-
-## Submitting changes
-
-Please send a [GitHub Pull Request to the ARMI main branch](https://github.com/terrapower/armi/pull/new/main) with a clear
-list of what you've done (read more about [pull requests](http://help.github.com/pull-requests/)).  Please follow our
-coding conventions (below) and make sure all of your commits are atomic (one feature per commit).
-
-Please write a clear log messages for your commits. One-liners are OK for small changes, but bigger changes should include more:
-
-    $ git commit -m "A brief summary of the commit
-    >
-    > A paragraph describing what changed and its impact."
-
-Note that a bot will require that you sign [our Contributor License
-Agreement](https://gist.github.com/youngmit/8654abcf93f309771ae9296abebe9d4a)
-before we can accept a pull request from you.
-
-## Coding conventions
-
-We use the [Black](https://black.readthedocs.io/en/stable/) code formatter so we don't have to argue or worry about trivial
-whitespacing conventions during code review. You should always run `black` before submitting a pull request.
-
-We really like Robert C Martin's [Clean Code](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882) book
-and at least nominally try to follow some of the advice in there.
-
-  * Consider contacting some ARMI developers before embarking on a major new feature or development on the framework.
-    We may prefer to keep various physics or design-specific developments in modular plugins (which can also be
-    open source, of course!). The framework should be focused on frameworky stuff (broad, sharable stuff).
-    As the Framework matures, we hope it will stabilize, though we recognize we need work before then.
-  * We believe that comments can increase maintenance burdens and should only be used when one fails to explain
-    the entire situation with the code itself. In practice, we fail to do this all the time, and need comments
-    to help guide future readers of the code.
-  * Names //really// matter.
-  * Your PR will be reviewed and probably need some iteration. We aren't trying to be a pain or discourage you,
-    we just want to make sure the code is optimal.
-
-## Licensing of Tools
-
-Be careful when including any dependency in ARMI (say in a requirements.txt file) not
-to include anything with a license that superceeds our Apache license. For instance,
-any third-party Python library included in ARMI with a GPL license will make the whole
-project fall under the GPL license. But a lot of potential users of ARMI will want to
-keep some of their work private, so we can't allow any GPL tools.
-
-For that reason, it is generally considered best-practice in the ARMI ecosystem to
-only use third-party Python libraries that have MIT or BSD licenses.
-
-## Documentation
-
-We use Sphinx for our documentation, and numpydoc to parse docstrings into the API docs section of our documentation.
-Thus, all docstrings are officially part of the technical documentation and should be written as such.
+For information on how to contribute to ARMI, see [our official documenation](https://terrapower.github.io/armi/developer/first_time_contributors.html).
diff --git a/README.rst b/README.rst
index 7ffe2710..f44420b5 100644
--- a/README.rst
+++ b/README.rst
@@ -49,6 +49,8 @@ found in [#touranarmi]_.
      - https://github.com/terrapower/armi
    * - Documentation
      - https://terrapower.github.io/armi
+   * - First time contributor's guide
+     - https://terrapower.github.io/armi/developer/first_time_contributors.html
    * - Bug tracker
      - https://github.com/terrapower/armi/issues
    * - Plugin directory
@@ -58,7 +60,7 @@ found in [#touranarmi]_.
 
 Quick start
 -----------
-Before starting, you need to have `Python <https://www.python.org/downloads/>`_ 3.7+ on
+Before starting, you need to have `Python <https://www.python.org/downloads/>`_ 3.9+ on
 Windows or Linux.
 
 Get the ARMI code, install the prerequisites, and fire up the launcher with the following
@@ -83,6 +85,11 @@ and then run::
 This runs the unit tests in parallel on 6 processes. Omit the ``-n 6`` argument
 to run on a single process.
 
+The tests can also be run directly, using ``pytest``::
+
+    $ pip3 install -r requirements-testing.txt
+    $ pytest -n 4 armi
+
 From here, we recommend going through a few of our `gallery examples
 <https://terrapower.github.io/armi/gallery/index.html>`_ and
 `tutorials <https://terrapower.github.io/armi/tutorials/index.html>`_ to
diff --git a/armi/cases/case.py b/armi/cases/case.py
index 19658ed1..7ad72532 100644
--- a/armi/cases/case.py
+++ b/armi/cases/case.py
@@ -545,8 +545,8 @@ class Case:
         if not os.path.exists(self.cs["burnChainFileName"]):
             raise ValueError(
                 f"The burn-chain file {self.cs['burnChainFileName']} does not exist. The "
-                f"data cannot be loaded. Fix this path or disable burn-chain initialization using "
-                f"the `initializeBurnChain` setting."
+                "data cannot be loaded. Fix this path or disable burn-chain initialization using "
+                "the `initializeBurnChain` setting."
             )
 
         with open(self.cs["burnChainFileName"]) as burnChainStream:
@@ -939,7 +939,7 @@ def copyInterfaceInputs(
                 except NonexistentSetting(key):
                     raise ValueError(
                         f"{key} is not a valid setting. Ensure the relevant specifyInputs "
-                        f"method uses a correct setting name."
+                        "method uses a correct setting name."
                     )
             label = key.name
 
diff --git a/armi/interfaces.py b/armi/interfaces.py
index f9f6b4c0..5d48075e 100644
--- a/armi/interfaces.py
+++ b/armi/interfaces.py
@@ -169,7 +169,7 @@ class TightCoupler:
         if self._previousIterationValue is None:
             raise ValueError(
                 f"Cannot check convergence of {self} with no previous iteration value set. "
-                f"Set using `storePreviousIterationValue` first."
+                "Set using `storePreviousIterationValue` first."
             )
 
         previous = self._previousIterationValue
diff --git a/armi/nucDirectory/nuclideBases.py b/armi/nucDirectory/nuclideBases.py
index aeafee52..3cc9427a 100644
--- a/armi/nucDirectory/nuclideBases.py
+++ b/armi/nucDirectory/nuclideBases.py
@@ -407,7 +407,7 @@ class INuclide(NuclideInterface):
                         runLog.info(
                             f"nuSF provided for {self} will be updated from "
                             f"{self.nuSF:<8.6e} to {userSpontaneousFissionYield:<8.6e} based on "
-                            f"user provided burn-chain data."
+                            "user provided burn-chain data."
                         )
                         self.nuSF = userSpontaneousFissionYield
             else:
diff --git a/armi/nucDirectory/transmutations.py b/armi/nucDirectory/transmutations.py
index b1e94ff3..fe2c103f 100644
--- a/armi/nucDirectory/transmutations.py
+++ b/armi/nucDirectory/transmutations.py
@@ -229,7 +229,7 @@ class DecayMode(Transmutable):
                 runLog.info(
                     f"Half-life provided for {self} will be updated from "
                     f"{parent.halflife:<15.11e} to {userHalfLife:<15.11e} seconds based on "
-                    f"user provided burn-chain data."
+                    "user provided burn-chain data."
                 )
 
                 self.halfLifeInSeconds = userHalfLife
diff --git a/armi/nuclearDataIO/xsLibraries.py b/armi/nuclearDataIO/xsLibraries.py
index 570296e9..12643776 100644
--- a/armi/nuclearDataIO/xsLibraries.py
+++ b/armi/nuclearDataIO/xsLibraries.py
@@ -264,9 +264,9 @@ def mergeXSLibrariesInWorkingDirectory(
                 os.path.exists(gamisoLibraryPath) and os.path.exists(pmatrxLibraryPath)
             ):
                 runLog.warning(
-                    f"One of GAMISO or PMATRX data exist for "
+                    "One of GAMISO or PMATRX data exist for "
                     f"XS ID {xsID} with suffix {xsLibrarySuffix}. "
-                    f"Attempting to find GAMISO/PMATRX data with "
+                    "Attempting to find GAMISO/PMATRX data with "
                     f"only XS ID {xsID} instead."
                 )
                 gamisoLibraryPath = os.path.join(
diff --git a/armi/physics/fuelPerformance/parameters.py b/armi/physics/fuelPerformance/parameters.py
index 7622c2da..604a3701 100644
--- a/armi/physics/fuelPerformance/parameters.py
+++ b/armi/physics/fuelPerformance/parameters.py
@@ -39,7 +39,7 @@ def _getFuelPerformanceBlockParams():
         def gasReleaseFraction(self, value):
             if value < 0.0 or value > 1.0:
                 raise ValueError(
-                    f"Cannot set a gas release fraction "
+                    "Cannot set a gas release fraction "
                     f"of {value} outside of the bounds of [0.0, 1.0]"
                 )
             self._p_gasReleaseFraction = value
@@ -55,7 +55,7 @@ def _getFuelPerformanceBlockParams():
         def bondRemoved(self, value):
             if value < 0.0 or value > 1.0:
                 raise ValueError(
-                    f"Cannot set a bond removed "
+                    "Cannot set a bond removed "
                     f"of {value} outside of the bounds of [0.0, 1.0]"
                 )
             self._p_bondRemoved = value
diff --git a/armi/physics/neutronics/crossSectionGroupManager.py b/armi/physics/neutronics/crossSectionGroupManager.py
index 5ba6e91b..3c816217 100644
--- a/armi/physics/neutronics/crossSectionGroupManager.py
+++ b/armi/physics/neutronics/crossSectionGroupManager.py
@@ -466,7 +466,7 @@ class CylindricalComponentsAverageBlockCollection(BlockCollection):
         if len(b) != len(repBlock):
             raise ValueError(
                 f"Blocks {b} and {repBlock} have differing number "
-                f"of components and cannot be homogenized"
+                "of components and cannot be homogenized"
             )
         # Using Fe-56 as a proxy for structure and Na-23 as proxy for coolant is undesirably SFR-centric
         # This should be generalized in the future, if possible
diff --git a/armi/physics/neutronics/fissionProductModel/lumpedFissionProduct.py b/armi/physics/neutronics/fissionProductModel/lumpedFissionProduct.py
index 9cf1127a..3610b2ca 100644
--- a/armi/physics/neutronics/fissionProductModel/lumpedFissionProduct.py
+++ b/armi/physics/neutronics/fissionProductModel/lumpedFissionProduct.py
@@ -92,7 +92,7 @@ class LumpedFissionProduct:
         if val < 0.0:
             raise ValueError(
                 f"Cannot set the yield of {key} in {self} to be "
-                f"less than zero as this is non-physical."
+                "less than zero as this is non-physical."
             )
         if val > NUM_FISSION_PRODUCTS_PER_LFP:
             raise ValueError(
@@ -387,7 +387,7 @@ def lumpedFissionProductFactory(cs):
     lfpPath = cs[CONF_LFP_COMPOSITION_FILE_PATH]
     if not lfpPath or not os.path.exists(lfpPath):
         raise ValueError(
-            f"The fission product reference file does "
+            "The fission product reference file does "
             f"not exist or is not a valid path. Path provided: {lfpPath}"
         )
 
diff --git a/armi/physics/neutronics/latticePhysics/latticePhysicsInterface.py b/armi/physics/neutronics/latticePhysics/latticePhysicsInterface.py
index 67de67e5..f03c21af 100644
--- a/armi/physics/neutronics/latticePhysics/latticePhysicsInterface.py
+++ b/armi/physics/neutronics/latticePhysics/latticePhysicsInterface.py
@@ -425,7 +425,7 @@ class LatticePhysicsInterface(interfaces.Interface):
                 runLog.info(
                     f"Although a XS library {self.r.core._lib} exists on {self.r.core}, "
                     f"there are missing XS IDs {missing} required. The XS generation on cycle {cycle} "
-                    f"is not enabled, but will be run to generate these missing cross sections."
+                    "is not enabled, but will be run to generate these missing cross sections."
                 )
                 executeXSGen = True
             elif missing:
diff --git a/armi/physics/thermalHydraulics/parameters.py b/armi/physics/thermalHydraulics/parameters.py
index 16fdc272..88e6384e 100644
--- a/armi/physics/thermalHydraulics/parameters.py
+++ b/armi/physics/thermalHydraulics/parameters.py
@@ -268,7 +268,7 @@ def _getBlockParams():
         )
 
     with pDefs.createBuilder(
-        default=0.0, categories=["thermal hydraulics", "mongoose"], saveToDB=True
+        default=None, categories=["thermal hydraulics", "mongoose"], saveToDB=True
     ) as pb:
 
         pb.defParam(
@@ -285,6 +285,9 @@ def _getBlockParams():
             location=ParamLocation.TOP | ParamLocation.EDGES,
         )
 
+    with pDefs.createBuilder(
+        default=0, categories=["thermal hydraulics", "mongoose"], saveToDB=True
+    ) as pb:
         pb.defParam(
             "THhotChannel",
             units=units.UNITLESS,
diff --git a/armi/physics/thermalHydraulics/settings.py b/armi/physics/thermalHydraulics/settings.py
index 5dd76b20..a8df91ed 100644
--- a/armi/physics/thermalHydraulics/settings.py
+++ b/armi/physics/thermalHydraulics/settings.py
@@ -28,7 +28,7 @@ def defineSettings():
             default=False,
             label="Run Thermal Hydraulics",
             description=(
-                f"Activate thermal hydraulics calculations using the physics module defined in "
+                "Activate thermal hydraulics calculations using the physics module defined in "
                 f"`{CONF_TH_KERNEL}`"
             ),
         ),
diff --git a/armi/reactor/blockParameters.py b/armi/reactor/blockParameters.py
index 06ac8d02..478c2cda 100644
--- a/armi/reactor/blockParameters.py
+++ b/armi/reactor/blockParameters.py
@@ -704,7 +704,7 @@ def getBlockParameterDefinitions():
 
     with pDefs.createBuilder(
         default=0.0,
-        location=ParamLocation.AVERAGE,
+        location=ParamLocation.VOLUME_INTEGRATED,
         categories=[
             "reactivity coefficients",
             "spatially dependent",
diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py
index 326b03f0..a7a11550 100644
--- a/armi/reactor/blocks.py
+++ b/armi/reactor/blocks.py
@@ -847,7 +847,7 @@ class Block(composites.Composite):
             runLog.warning(
                 f"More than one boron10-containing component found  in {self.name}. "
                 f"Only {b10Comp} will be considered for calculation of initialB10ComponentVol "
-                f"Since adding multiple volumes is not conservative for captures/cc."
+                "Since adding multiple volumes is not conservative for captures/cc."
                 f"All compos found {b10Comps}",
                 single=True,
             )
@@ -1965,13 +1965,6 @@ class HexBlock(Block):
                     pinToDuctGap, self
                 )
             )
-            wire = self.getComponent(Flags.WIRE)
-            wireThicknesses = wire.getDimension("od", cold=False)
-            if pinToDuctGap < wireThicknesses:
-                raise ValueError(
-                    "Gap between pins and duct is {0:.4f} cm in {1} which does not allow room for the wire "
-                    "with diameter {2}".format(pinToDuctGap, self, wireThicknesses)
-                )
         elif pinToDuctGap is None:
             # only produce a warning if pin or clad are found, but not all of pin, clad and duct. We
             # may need to tune this logic a bit
diff --git a/armi/reactor/blueprints/gridBlueprint.py b/armi/reactor/blueprints/gridBlueprint.py
index d6eb86ba..1cc6eb1d 100644
--- a/armi/reactor/blueprints/gridBlueprint.py
+++ b/armi/reactor/blueprints/gridBlueprint.py
@@ -574,7 +574,7 @@ def saveToStream(stream, bluep, full=False, tryMap=False):
                 aMap.gridContentsToAscii()
             except Exception as e:
                 runLog.warning(
-                    f"The `lattice map` for the current assembly arrangement cannot be written. "
+                    "The `lattice map` for the current assembly arrangement cannot be written. "
                     f"Defaulting to using the `grid contents` dictionary instead. Exception: {e}"
                 )
                 aMap = None
diff --git a/armi/reactor/blueprints/isotopicOptions.py b/armi/reactor/blueprints/isotopicOptions.py
index 99385158..0e37b4cf 100644
--- a/armi/reactor/blueprints/isotopicOptions.py
+++ b/armi/reactor/blueprints/isotopicOptions.py
@@ -580,7 +580,7 @@ def autoUpdateNuclideFlags(cs, nuclideFlags, inerts):
     nbs = getAllNuclideBasesByLibrary(cs)
     if nbs:
         runLog.info(
-            f"Adding explicit fission products to the nuclide flags based on the "
+            "Adding explicit fission products to the nuclide flags based on the "
             f"fission product model set to `{cs[CONF_FP_MODEL]}`."
         )
         for nb in nbs:
@@ -605,8 +605,8 @@ def getAllNuclideBasesByLibrary(cs):
             nbs = nuclideBases.byMcc3Id.values()
         else:
             raise ValueError(
-                f"An option to handle the `CONF_FISSION_PRODUCT_LIBRARY_NAME` "
+                "An option to handle the `CONF_FISSION_PRODUCT_LIBRARY_NAME` "
                 f"set to `{cs[CONF_FISSION_PRODUCT_LIBRARY_NAME]}` has not been "
-                f"implemented."
+                "implemented."
             )
     return nbs
diff --git a/armi/reactor/components/__init__.py b/armi/reactor/components/__init__.py
index 26c4c06f..9d3e940d 100644
--- a/armi/reactor/components/__init__.py
+++ b/armi/reactor/components/__init__.py
@@ -376,8 +376,8 @@ class DerivedShape(UnshapedComponent):
         if remainingVolume < 0:
             msg = (
                 f"The component areas in {self.parent} exceed the maximum "
-                f"allowable volume based on the geometry. Check that the "
-                f"geometry is defined correctly.\n"
+                "allowable volume based on the geometry. Check that the "
+                "geometry is defined correctly.\n"
                 f"Maximum allowable volume: {self.parent.getMaxVolume()} cm^3\n"
                 f"Volume of all non-derived shape components: {siblingVolume} cm^3\n"
             )
diff --git a/armi/reactor/converters/axialExpansionChanger.py b/armi/reactor/converters/axialExpansionChanger.py
index 8e7f365d..b3dd509a 100644
--- a/armi/reactor/converters/axialExpansionChanger.py
+++ b/armi/reactor/converters/axialExpansionChanger.py
@@ -622,7 +622,7 @@ class ExpansionData:
         """
         if len(componentLst) != len(expFrac):
             runLog.error(
-                f"Number of components and expansion fractions must be the same!\n"
+                "Number of components and expansion fractions must be the same!\n"
                 f"    len(componentLst) = {len(componentLst)}\n"
                 f"        len(expFrac) = {len(expFrac)}"
             )
@@ -801,7 +801,7 @@ class ExpansionData:
             raise RuntimeError(f"No target component found!\n   Block {b}")
         if len(componentWFlag) > 1:
             raise RuntimeError(
-                f"Cannot have more than one component within a block that has the target flag!"
+                "Cannot have more than one component within a block that has the target flag!"
                 f"Block {b}\nflagOfInterest {flagOfInterest}\nComponents {componentWFlag}"
             )
         self._componentDeterminesBlockHeight[componentWFlag[0]] = True
diff --git a/armi/reactor/converters/uniformMesh.py b/armi/reactor/converters/uniformMesh.py
index 488696db..a89349ac 100644
--- a/armi/reactor/converters/uniformMesh.py
+++ b/armi/reactor/converters/uniformMesh.py
@@ -200,7 +200,7 @@ class UniformMeshGenerator:
         )
 
         runLog.extra(
-            f"Attempting to honor control and fuel material boundaries in uniform mesh "
+            "Attempting to honor control and fuel material boundaries in uniform mesh "
             f"for {self} while also keeping minimum mesh size of {self.minimumMeshSize}. "
             f"Material boundaries are: {allMatBounds}"
         )
@@ -420,7 +420,7 @@ class UniformMeshGeometryConverter(GeometryConverter):
         if self._hasNonUniformAssems:
             runLog.extra(
                 f"Replacing non-uniform assemblies in reactor {r}, "
-                f"with assemblies whose axial mesh is uniform with "
+                "with assemblies whose axial mesh is uniform with "
                 f"the core's reference assembly mesh: {r.core.refAssem.getAxialMesh()}"
             )
             self.convReactor = self._sourceReactor
@@ -560,8 +560,8 @@ class UniformMeshGeometryConverter(GeometryConverter):
                     runLog.error(
                         f"No assembly matching name {assem.getName()} "
                         f"was found in the temporary storage list. {assem} "
-                        f"will persist as an axially unified assembly. "
-                        f"This is likely not intended."
+                        "will persist as an axially unified assembly. "
+                        "This is likely not intended."
                     )
 
             self._sourceReactor.core.updateAxialMesh()
@@ -775,12 +775,12 @@ class UniformMeshGeometryConverter(GeometryConverter):
                 continue
             elif not sourceBlocksInfo:
                 raise ValueError(
-                    f"An error occurred when attempting to map to the "
+                    "An error occurred when attempting to map to the "
                     f"results from {sourceAssembly} to {destinationAssembly}. "
                     f"No blocks in {sourceAssembly} exist between the axial "
                     f"elevations of {zLower:<12.5f} cm and {zUpper:<12.5f} cm. "
-                    f"This a major bug in the uniform mesh converter that should "
-                    f"be reported to the developers."
+                    "This a major bug in the uniform mesh converter that should "
+                    "be reported to the developers."
                 )
 
             if mapNumberDensities:
@@ -1317,7 +1317,7 @@ class ParamMapper:
             if val is None:
                 continue
 
-            if isinstance(val, (list, numpy.ndarray)):
+            if isinstance(val, (tuple, list, numpy.ndarray)):
                 ParamMapper._arrayParamSetter(block, [val], [paramName])
             else:
                 ParamMapper._scalarParamSetter(block, [val], [paramName])
@@ -1327,22 +1327,11 @@ class ParamMapper:
         paramVals = []
         for paramName in paramNames:
             val = block.p[paramName]
-            defaultValue = self.paramDefaults[paramName]
-            valType = type(defaultValue)
-            # Array / list parameters can be have values that are `None`, lists, or numpy arrays. This first
-            # checks if the value type is any of these and if so, the block-level parameter is treated as an
-            # array.
-            if isinstance(valType, (list, numpy.ndarray)) or isinstance(None, valType):
-                if val is None or len(val) == 0:
-                    paramVals.append(None)
-                else:
-                    paramVals.append(numpy.array(val))
-            # Otherwise, the parameter is treated as a scalar, like a float/string/integer.
+            # list-like should be treated as a numpy array
+            if isinstance(val, (tuple, list, numpy.ndarray)):
+                paramVals.append(numpy.array(val) if len(val) > 0 else None)
             else:
-                if val == defaultValue:
-                    paramVals.append(defaultValue)
-                else:
-                    paramVals.append(val)
+                paramVals.append(val)
 
         return numpy.array(paramVals, dtype=object)
 
diff --git a/armi/reactor/parameters/parameterDefinitions.py b/armi/reactor/parameters/parameterDefinitions.py
index 13ead330..458a849f 100644
--- a/armi/reactor/parameters/parameterDefinitions.py
+++ b/armi/reactor/parameters/parameterDefinitions.py
@@ -38,7 +38,7 @@ from armi.reactor.flags import Flags
 from armi.reactor.parameters.exceptions import ParameterError, ParameterDefinitionError
 
 # bitwise masks for high-speed operations on the `assigned` attribute
-# (see http://www.vipan.com/htdocs/bitwisehelp.html)
+# see: https://web.archive.org/web/20120225043338/http://www.vipan.com/htdocs/bitwisehelp.html
 # Note that the various operations are responsible for clearing the flags on the events.
 # These should be interpreted as:
 #   The Parameter or ParameterCollection has been modified SINCE_<time-description>
@@ -70,6 +70,8 @@ class Category:
     * `neutronics` parameters are calculated in a neutronics global flux solve
     * `gamma` parameters are calculated in a fixed-source gamma solve
     * `detailedAxialExpansion` parameters are marked as such so that they are mapped from the uniform mesh back to the non-uniform mesh
+    * `reactivity coefficients` parameters are related to reactivity coefficient or kinetics parameters for kinetics solutions
+    * `thermal hydraulics` parameters come from a thermal hydraulics physics plugin (e.g., flow rates, temperatures, etc.)
     """
 
     depletion = "depletion"
@@ -83,6 +85,8 @@ class Category:
     neutronics = "neutronics"
     gamma = "gamma"
     detailedAxialExpansion = "detailedAxialExpansion"
+    reactivityCoefficients = "reactivity coefficients"
+    thermalHydraulics = "thermal hydraulics"
 
 
 class ParamLocation(enum.Flag):
@@ -254,7 +258,8 @@ class Parameter:
         # TODO: This warning is temporary. At some point, it will become an AssertionError.
         if not len(description):
             runLog.warning(
-                f"DeprecationWarning: Parameter {name} defined without description."
+                f"DeprecationWarning: Parameter {name} defined without description.",
+                single=True,
             )
         self.collectionType = _Undefined
         self.name = name
diff --git a/armi/reactor/reactorParameters.py b/armi/reactor/reactorParameters.py
index 570d5429..2fe1bc5b 100644
--- a/armi/reactor/reactorParameters.py
+++ b/armi/reactor/reactorParameters.py
@@ -629,6 +629,12 @@ def defineCoreParameters():
             description="Fuel Axial Expansion Coefficient",
         )
 
+        pb.defParam(
+            "rxFuelAxialExpansionCoeffPerPercent",
+            units="dk/kk'-%",
+            description="Fuel Axial Expansion Coefficient",
+        )
+
         pb.defParam(
             "rxGridPlateRadialExpansionCoeffPerTemp",
             units=f"{units.REACTIVITY}/{units.DEGK}",
diff --git a/armi/runLog.py b/armi/runLog.py
index b87e47b7..b69ecca8 100644
--- a/armi/runLog.py
+++ b/armi/runLog.py
@@ -61,7 +61,6 @@ _ADD_LOG_METHOD_STR = """def {0}(self, message, *args, **kws):
     if self.isEnabledFor({1}):
         self._log({1}, message, args, **kws)
 logging.Logger.{0} = {0}"""
-_WHITE_SPACE = " " * 6
 LOG_DIR = os.path.join(os.getcwd(), "logs")
 OS_SECONDS_TIMEOUT = 2 * 60
 SEP = "|"
@@ -88,7 +87,6 @@ class _RunLog:
         ----------
         mpiRank : int
             If this is zero, we are in the parent process, otherwise child process.
-            The default of 0 means we assume the parent process.
             This should not be adjusted after instantiation.
         """
         self._mpiRank = mpiRank
@@ -107,25 +105,49 @@ class _RunLog:
         self.logger = NullLogger("NULL")
         self.stderrLogger = NullLogger("NULL2", isStderr=True)
 
-    def _setLogLevels(self):
-        """Here we fill the logLevels dict with custom strings that depend on the MPI rank."""
+    @staticmethod
+    def getLogLevels(mpiRank):
+        """Helper method to build an important data object this class needs.
+
+        Parameters
+        ----------
+        mpiRank : int
+            If this is zero, we are in the parent process, otherwise child process.
+            This should not be adjusted after instantiation.
+        """
+        rank = "" if mpiRank == 0 else "-{:>03d}".format(mpiRank)
+
         # NOTE: using ordereddict so we can get right order of options in GUI
-        _rank = "" if self._mpiRank == 0 else "-{:>03d}".format(self._mpiRank)
-        self.logLevels = collections.OrderedDict(
+        return collections.OrderedDict(
             [
-                ("debug", (logging.DEBUG, "[dbug{}] ".format(_rank))),
-                ("extra", (15, "[xtra{}] ".format(_rank))),
-                ("info", (logging.INFO, "[info{}] ".format(_rank))),
-                ("important", (25, "[impt{}] ".format(_rank))),
-                ("prompt", (27, "[prmt{}] ".format(_rank))),
-                ("warning", (logging.WARNING, "[warn{}] ".format(_rank))),
-                ("error", (logging.ERROR, "[err {}] ".format(_rank))),
-                ("header", (100, "{}".format(_rank))),
+                ("debug", (logging.DEBUG, f"[dbug{rank}] ")),
+                ("extra", (15, f"[xtra{rank}] ")),
+                ("info", (logging.INFO, f"[info{rank}] ")),
+                ("important", (25, f"[impt{rank}] ")),
+                ("prompt", (27, f"[prmt{rank}] ")),
+                ("warning", (logging.WARNING, f"[warn{rank}] ")),
+                ("error", (logging.ERROR, f"[err {rank}] ")),
+                ("header", (100, f"{rank}")),
             ]
         )
+
+    @staticmethod
+    def getWhiteSpace(mpiRank):
+        """Helper method to build the white space used to left-adjust the log lines.
+
+        Parameters
+        ----------
+        mpiRank : int
+            If this is zero, we are in the parent process, otherwise child process.
+            This should not be adjusted after instantiation.
+        """
+        logLevels = _RunLog.getLogLevels(mpiRank)
+        return " " * len(max([ll[1] for ll in logLevels.values()]))
+
+    def _setLogLevels(self):
+        """Here we fill the logLevels dict with custom strings that depend on the MPI rank."""
+        self.logLevels = self.getLogLevels(self._mpiRank)
         self._logLevelNumbers = sorted([ll[0] for ll in self.logLevels.values()])
-        global _WHITE_SPACE
-        _WHITE_SPACE = " " * len(max([ll[1] for ll in self.logLevels.values()]))
 
         # modify the logging module strings for printing
         for longLogString, (logValue, shortLogString) in self.logLevels.items():
@@ -462,7 +484,8 @@ class DeduplicationFilter(logging.Filter):
                     return False
 
         # Handle some special string-mangling we want to do, for multi-line messages
-        record.msg = msg.rstrip().replace("\n", "\n" + _WHITE_SPACE)
+        whiteSpace = _RunLog.getWhiteSpace(context.MPI_RANK)
+        record.msg = msg.rstrip().replace("\n", "\n" + whiteSpace)
         return True
 
 
diff --git a/armi/settings/fwSettings/globalSettings.py b/armi/settings/fwSettings/globalSettings.py
index 18ff2743..16a126ff 100644
--- a/armi/settings/fwSettings/globalSettings.py
+++ b/armi/settings/fwSettings/globalSettings.py
@@ -142,10 +142,10 @@ def defineSettings() -> List[setting.Setting]:
             label="Initialize Burn Chain",
             description=(
                 f"This setting is paired with the `{CONF_BURN_CHAIN_FILE_NAME}` setting. "
-                f"When enabled, this will initialize the burn-chain on initializing the case and "
-                f"is required for running depletion calculations where the transmutations and decays "
-                f"are controlled by the framework. If an external software, such as ORIGEN, contains "
-                f"data for the burn-chain already embedded then this may be disabled."
+                "When enabled, this will initialize the burn-chain on initializing the case and "
+                "is required for running depletion calculations where the transmutations and decays "
+                "are controlled by the framework. If an external software, such as ORIGEN, contains "
+                "data for the burn-chain already embedded then this may be disabled."
             ),
         ),
         setting.Setting(
diff --git a/doc/.static/theme_fixes.css b/doc/.static/css/theme_fixes.css
similarity index 90%
rename from doc/.static/theme_fixes.css
rename to doc/.static/css/theme_fixes.css
index 976840f1..e5d239b6 100644
--- a/doc/.static/theme_fixes.css
+++ b/doc/.static/css/theme_fixes.css
@@ -1,3 +1,5 @@
+@import 'theme.css';
+
 /* override table width restrictions */
 @media screen and (min-width: 767px) {
 
diff --git a/doc/conf.py b/doc/conf.py
index eab91134..3d062421 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -137,7 +137,9 @@ extensions = [
     "sphinx_gallery.gen_gallery",
     "sphinx.ext.imgconverter",  # to convert GH Actions badge SVGs to PNG for LaTeX
     "sphinxcontrib.plantuml",
-    "sphinxcontrib.needs",
+    "sphinx_needs",
+    "sphinx_rtd_theme",  # needed here for loading jquery in sphinx 6
+    "sphinxcontrib.jquery",  # see https://github.com/readthedocs/sphinx_rtd_theme/issues/1452
 ]
 
 # Our API should make sense without documenting private/special members.
@@ -238,11 +240,24 @@ html_logo = os.path.join(".static", "armiicon_24x24.ico")
 html_theme_options = {
     "style_external_links": True,
     "style_nav_header_background": "#233C5B",  # TP blue looks better than green
+    "logo_only": False,
+    "display_version": True,
+    "prev_next_buttons_location": "bottom",
+    "vcs_pageview_mode": "",
+    # Toc options
+    "collapse_navigation": True,
+    "sticky_navigation": True,
+    "navigation_depth": 4,
+    "includehidden": True,
+    "titles_only": False,
 }
 
 # Add any paths that contain custom themes here, relative to this directory.
 html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
 
+# as long as this file @import's the theme's main css it won't break anything
+html_style = "css/theme_fixes.css"
+
 # The name of an image file (within the static path) to use as favicon of the
 # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
 # pixels large.
@@ -260,14 +275,7 @@ html_last_updated_fmt = "%Y-%m-%d"
 # Output file base name for HTML help builder.
 htmlhelp_basename = "ARMIdoc"
 
-# Need to manually add gallery css files or else the theme_fixes override them.
 html_context = {
-    "css_files": [
-        "_static/theme_fixes.css",  # overrides for wide tables in RTD theme
-        "_static/gallery.css",  # for the sphinx-gallery plugin
-        "_static/gallery-binder.css",
-        "_static/gallery-dataframe.css",
-    ],
     "display_github": True,  # Integrate GitHub
     "github_user": "terrapower",  # Username
     "github_repo": "armi",  # Repo name
diff --git a/doc/developer/first_time_contributors.rst b/doc/developer/first_time_contributors.rst
new file mode 100644
index 00000000..93a0d64a
--- /dev/null
+++ b/doc/developer/first_time_contributors.rst
@@ -0,0 +1,76 @@
+*****************************
+First Time Contributors Guide
+*****************************
+
+The ARMI team strongly encourages developers to contribute to the codebase.
+
+The ARMI framework code is open source, and your contributions will become open source.
+Although fewer laws apply to open source materials because they are publicly-available, you still
+must comply with all applicable laws and regulations.
+
+Help Wanted
+===========
+
+There are a lot of places you can get started to help the ARMI project and team:
+
+* Better :doc:`documentation </developer/documenting>`
+* Better test coverage
+* Many more type annotations are desired. Type issues cause lots of bugs.
+* Targeted speedups (e.g. informed by a profiler)
+* Additional relevance to thermal reactors
+
+Naturally, you can also look at the open `ARMI issues <https://github.com/terrapower/armi/issues>`_ to see what work needs to be done. In particular, check out the `help wanted tickets <https://github.com/terrapower/armi/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22>`_ and `good first issue tickets <https://github.com/terrapower/armi/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22>`_.
+
+Testing
+=======
+
+Any contribution must pass all included unit tests. You will frequently have to fix
+tests your code changes break. And you should definitely add tests to cover anything
+new your code does.
+
+The standard way to run the tests is to install and use `tox <https://tox.readthedocs.io/en/latest/>`_::
+
+    $ pip3 install tox
+    $ tox -- -n 6
+
+This runs the unit tests in parallel on 6 processes. Omit the ``-n 6`` argument
+to run on a single process.
+
+Or the tests can also be run using ``pytest`` directly::
+
+    $ pip3 install -r requirements-testing.txt
+    $ pytest -n 4 armi
+
+Submitting Changes
+==================
+
+To submit a change to ARMI, you will have to open a Pull Request (PR) on GitHub.com.
+
+The process for opening a PR against ARMI goes something like this:
+
+1. `Fork the ARMI repo <https://docs.github.com/en/get-started/quickstart/fork-a-repo>`_
+2. `Create a new branch <https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-and-deleting-branches-within-your-repository>`_ in your repo
+3. Make your code changes to your new branch
+4. Submit a Pull Request against `ARMIs main branch <https://github.com/terrapower/armi/pull/new/main>`_
+    a. See `GitHubs general guidance on Pull Requests <https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request>`_
+    b. See :doc:`ARMIs specific guidance </developer/tooling>` on what makes a "good" Pull Request.
+5. Actively engage with your PR reviewer's questions and comments.
+
+> Note that a bot will require that you sign our `Contributor License Agreement <https://gist.github.com/youngmit/8654abcf93f309771ae9296abebe9d4a>`_
+before we can accept a pull request from you.
+
+See our published documentation for a complete guide to our :doc:`coding standards and practices </developer/standards_and_practices.html>`.
+
+Also, please check out our (quick) synopsis on :doc:`good commit messages </developer/tooling>`.
+
+Licensing of Tools
+==================
+
+Be careful when including any dependency in ARMI (say in a ``requirements.txt`` file) not
+to include anything with a license that superceeds our Apache license. For instance,
+any third-party Python library included in ARMI with a GPL license will make the whole
+project fall under the GPL license. But a lot of potential users of ARMI will want to
+keep some of their work private, so we can't allow any GPL tools.
+
+For that reason, it is generally considered best-practice in the ARMI ecosystem to
+only use third-party Python libraries that have MIT or BSD licenses.
diff --git a/doc/developer/index.rst b/doc/developer/index.rst
index cc9879f2..23e42675 100644
--- a/doc/developer/index.rst
+++ b/doc/developer/index.rst
@@ -20,4 +20,8 @@ ARMI for the community.
    guide
    making_armi_based_apps
    entrypoints
-   *
+   documenting
+   parallel_coding
+   profiling
+   reports
+   *
\ No newline at end of file
diff --git a/doc/developer/standards_and_practices.rst b/doc/developer/standards_and_practices.rst
index a8f23987..3d7464a2 100644
--- a/doc/developer/standards_and_practices.rst
+++ b/doc/developer/standards_and_practices.rst
@@ -186,7 +186,7 @@ code, consider pulling the repeated code out into it's own function, or using a
 
 Public methods should have docstrings
 =====================================
-Always create the :doc:`proper docstrings </developer/code_practices/documentation_guidelines>` for all public
+Always create the `proper docstrings <https://numpydoc.readthedocs.io/en/latest/example.html>`_ for all public
 functions and public classes.
 
 Unit tests
diff --git a/doc/gallery-src/analysis/run_blockMcnpMaterialCard.py b/doc/gallery-src/analysis/run_blockMcnpMaterialCard.py
index f0da2649..683a9c74 100644
--- a/doc/gallery-src/analysis/run_blockMcnpMaterialCard.py
+++ b/doc/gallery-src/analysis/run_blockMcnpMaterialCard.py
@@ -12,7 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 """
-Write MCNP Material Cards.
+Write MCNP Material Cards
+=========================
 
 Here we load a test reactor and write each component of one fuel block out as
 MCNP material cards.
diff --git a/doc/gallery-src/framework/run_blockVolumeFractions.py b/doc/gallery-src/framework/run_blockVolumeFractions.py
index 4ad09062..993cb6ba 100644
--- a/doc/gallery-src/framework/run_blockVolumeFractions.py
+++ b/doc/gallery-src/framework/run_blockVolumeFractions.py
@@ -14,7 +14,8 @@
 
 # -*- coding: utf-8 -*-
 """
-Computing Component Volume Fractions on a Block with Automatic Thermal Expansion.
+Computing Component Volume Fractions on a Block with Automatic Thermal Expansion
+================================================================================
 
 Given an :py:mod:`Block <armi.reactor.blocks.Block>`, compute
 the component volume fractions. Assess the change in volume
diff --git a/doc/gallery-src/framework/run_chartOfNuclides.py b/doc/gallery-src/framework/run_chartOfNuclides.py
index 4c6599c9..13fcfe3a 100644
--- a/doc/gallery-src/framework/run_chartOfNuclides.py
+++ b/doc/gallery-src/framework/run_chartOfNuclides.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 """
 Plot a chart of the nuclides.
+=============================
 
 Use the nuclide directory of ARMI to plot a chart of the nuclides
 coloring the squares with the natural abundance.
diff --git a/doc/gallery-src/framework/run_computeReactionRates.py b/doc/gallery-src/framework/run_computeReactionRates.py
index 585ae42e..8639cdb7 100644
--- a/doc/gallery-src/framework/run_computeReactionRates.py
+++ b/doc/gallery-src/framework/run_computeReactionRates.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 """
 Computing Reaction Rates on a Block.
+====================================
 
 In this example, a set of 1-group reaction rates (in #/s) are evaluated
 for a dummy fuel block containing UZr fuel, HT9 structure, and
diff --git a/doc/gallery-src/framework/run_fuelManagement.py b/doc/gallery-src/framework/run_fuelManagement.py
index a013bee8..49895c09 100644
--- a/doc/gallery-src/framework/run_fuelManagement.py
+++ b/doc/gallery-src/framework/run_fuelManagement.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 """
 Fuel management in a LWR.
+=========================
 
 Demo of locating and swapping assemblies in a core with Cartesian geometry. Given a burnup
 distribution, this swaps high burnup assemblies with low ones.
diff --git a/doc/gallery-src/framework/run_grids1_hex.py b/doc/gallery-src/framework/run_grids1_hex.py
index 41733309..7d39ca1f 100644
--- a/doc/gallery-src/framework/run_grids1_hex.py
+++ b/doc/gallery-src/framework/run_grids1_hex.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 """
 Make a hex grid.
+================
 
 This uses a grid factory method to build an infinite 2-D grid of hexagons with pitch
 equal to 1.0 cm.
diff --git a/doc/gallery-src/framework/run_grids2_cartesian.py b/doc/gallery-src/framework/run_grids2_cartesian.py
index 67d68e3e..ac842b0a 100644
--- a/doc/gallery-src/framework/run_grids2_cartesian.py
+++ b/doc/gallery-src/framework/run_grids2_cartesian.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 """
 Make a Cartesian grid.
+======================
 
 This builds a Cartesian grid with squares 1 cm square, with the z-coordinates
 provided explicitly. It is also offset in 3D space to X, Y, Z = 10, 5, 5 cm.
diff --git a/doc/gallery-src/framework/run_grids3_rzt.py b/doc/gallery-src/framework/run_grids3_rzt.py
index 50998ec4..08c8f776 100644
--- a/doc/gallery-src/framework/run_grids3_rzt.py
+++ b/doc/gallery-src/framework/run_grids3_rzt.py
@@ -14,6 +14,7 @@
 
 """
 Make a Theta-R-Z grid.
+======================
 
 This builds a 3-D grid in Theta-R-Z geometry by specifying the theta, r, and z
 dimension bounds explicitly.
diff --git a/doc/gallery-src/framework/run_isotxs.py b/doc/gallery-src/framework/run_isotxs.py
index 1aa60da9..b0dbc50f 100644
--- a/doc/gallery-src/framework/run_isotxs.py
+++ b/doc/gallery-src/framework/run_isotxs.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 """
 Plotting Multi-group XS from ISOTXS.
+====================================
 
 In this example, several cross sections are plotted from
 an existing binary cross section library file in :py:mod:`ISOTXS <armi.nuclearDataIO.isotxs>` format.
diff --git a/doc/gallery-src/framework/run_isotxs2_matrix.py b/doc/gallery-src/framework/run_isotxs2_matrix.py
index 6067c26e..e197d985 100644
--- a/doc/gallery-src/framework/run_isotxs2_matrix.py
+++ b/doc/gallery-src/framework/run_isotxs2_matrix.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 """
 Plotting a multi-group scatter matrix.
+======================================
 
 Here we plot scatter matrices from an ISOTXS microscopic cross section library.
 We plot the inelastic scatter cross section of U235 as well as the (n,2n) source
diff --git a/doc/gallery-src/framework/run_materials.py b/doc/gallery-src/framework/run_materials.py
index 84fe2a6c..bb78d4e8 100644
--- a/doc/gallery-src/framework/run_materials.py
+++ b/doc/gallery-src/framework/run_materials.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 """
 Listing of Material Library.
+============================
 
 This is a listing of all the elements in all the materials that are included in the ARMI
 material library. Many of the materials in this library are academic in quality and
diff --git a/doc/gallery-src/framework/run_programmaticReactorDefinition.py b/doc/gallery-src/framework/run_programmaticReactorDefinition.py
index 98d8c4dd..e6fc92d2 100644
--- a/doc/gallery-src/framework/run_programmaticReactorDefinition.py
+++ b/doc/gallery-src/framework/run_programmaticReactorDefinition.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 """
 Build Reactor Inputs Programmatically.
+======================================
 
 Sometimes it's desirable to build input definitions for ARMI using
 code rather than by writing the textual input files directly.
diff --git a/doc/gallery-src/framework/run_reactorFacemap.py b/doc/gallery-src/framework/run_reactorFacemap.py
index e4553c1d..b00c22d2 100644
--- a/doc/gallery-src/framework/run_reactorFacemap.py
+++ b/doc/gallery-src/framework/run_reactorFacemap.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 """
 Plot a reactor facemap.
+=======================
 
 Load a test reactor from the test suite and plot a dummy
 power distribution from it. You can plot any block parameter.
diff --git a/doc/gallery-src/framework/run_transmutationMatrix.py b/doc/gallery-src/framework/run_transmutationMatrix.py
index 1f57eac8..2546c65f 100644
--- a/doc/gallery-src/framework/run_transmutationMatrix.py
+++ b/doc/gallery-src/framework/run_transmutationMatrix.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 """
 Transmutation and decay reactions.
+==================================
 
 This plots some of the transmutation and decay pathways for the actinides and some light
 nuclides using the burn chain definition that is included with ARMI. Note that many of
diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst
index f796a64a..44065b86 100644
--- a/doc/release/0.2.rst
+++ b/doc/release/0.2.rst
@@ -12,8 +12,10 @@ What's new in ARMI
 #. Broad cleanup of ``Parameters``: filled in all empty units and descriptions, removed unused params. (`PR#1345 <https://github.com/terrapower/armi/pull/1345>`_)
 #. Removed redundant ``Material.name`` variable. (`PR#1335 <https://github.com/terrapower/armi/pull/1335>`_)
 #. Added SHA1 hashes of XS control files to the welcome text. (`PR#1334 <https://github.com/terrapower/armi/pull/1334>`_)
+#. Add python 3.11 to ARMI's CI testing GH actions! (`PR#1341 <https://github.com/terrapower/armi/pull/1341>`_)
 #. Put back ``avgFuelTemp`` block parameter. (`PR#1362 <https://github.com/terrapower/armi/pull/1362>`_)
 #. Make cylindrical component block collection less strict about pre-homogenization checks. (`PR#1347 <https://github.com/terrapower/armi/pull/1347>`_)
+#. Updated some parameter definitions and defaults. (`PR#1355 <https://github.com/terrapower/armi/pull/1355>`_)
 #. TBD
 
 Bug fixes
diff --git a/doc/requirements-docs.txt b/doc/requirements-docs.txt
index 49d66466..fccddce6 100644
--- a/doc/requirements-docs.txt
+++ b/doc/requirements-docs.txt
@@ -1,40 +1,46 @@
 # these are most specified that usual, because Sphinx docs seem to be quite fragile
-Sphinx==4.4.0
+# limited <7 by sphinx-rtd-theme at the moment.
+# sphinx-rtd-theme requires docutils <0.19 but sphinx dropped support for 0.18 in 6.0.0
+# so we're stuck at these versions
+Sphinx==5.3.0
+docutils==0.18.1
+
+# Read-The-Docs theme for Sphinx
+sphinx-rtd-theme==1.2.2
 
 # Parses Jupyter notebooks
-nbsphinx==0.8.8
+nbsphinx==0.9.2
 
 # Includes Jupyter notebooks in Sphinx source root
 nbsphinx-link==1.3.0
 
 # builds a HTML version of a Python script and puts it into a gallery
-sphinx-gallery==0.9.0
-
-# Read-The-Docs theme for Sphinx
-sphinx-rtd-theme==0.5.2
+sphinx-gallery==0.13.0
 
 # allows us to more easily document our API
 sphinxcontrib-apidoc==0.3.0
 
-# generates OpenGraph metadata?
-sphinxext-opengraph==0.5.1
+# generates OpenGraph metadata to make good-looking cards on social media
+sphinxext-opengraph==0.8.2
 
-# supporting requirement documentation
-sphinxcontrib-needs==0.7.6
-sphinxcontrib-plantuml==0.23
+# supporting requirements traceability matrices for QA
+sphinx-needs==1.2.2
+
+# uml support in sphinx-needs
+sphinxcontrib-plantuml==0.25
 
 # This is not strictly necessary via PIP, but MUST be in the path.
 # Used to convert between file formats.
 pandoc
 
 # iPython kernel to run Jupyter notebooks
-ipykernel==6.7.0
-
-# helps us use RST files
-docutils<0.18
+ipykernel==6.25.1
 
 # used to generate UML diagrams
-pylint==2.7.4
+pylint==2.17.5
+
+# used in numpydoc and nbconvert
+Jinja2==3.0.3
 
-# needed to side step bug: https://github.com/numpy/numpydoc/issues/380
-Jinja2==3.0.1
+# deal with missing jquery errors 
+sphinxcontrib-jquery==4.1
diff --git a/doc/requirements/srsd.rst b/doc/requirements/srsd.rst
index 8ac12e6e..26039c93 100644
--- a/doc/requirements/srsd.rst
+++ b/doc/requirements/srsd.rst
@@ -510,7 +510,7 @@ TODO: This is completed by the :doc:`Settings Report </user/inputs/settings_repo
    :id: REQ_LATTICE_OUTPUTS
    :status: needs implementation, needs test
 
-.. reg:: The reactor package shall check input for basic correctness.
+.. req:: The reactor package shall check input for basic correctness.
    :id: REQ_REACTOR_CORRECTNESS
    :status: needs implementation, needs test
 
diff --git a/ruff.toml b/ruff.toml
index dc9e3223..402d61c6 100644
--- a/ruff.toml
+++ b/ruff.toml
@@ -76,6 +76,7 @@ line-length = 88
 # N - We have our own naming conventions for unit tests.
 # SLF001 - private member access
 "*/tests/*" = ["D1", "E741", "N", "SLF001"]
+"doc/gallery-src/*" = ["D400"]
 
 [pydocstyle]
 # Use numpy-style docstrings.
diff --git a/setup.py b/setup.py
index c105c6ad..9b5468d7 100644
--- a/setup.py
+++ b/setup.py
@@ -56,7 +56,7 @@ setup(
     entry_points={"console_scripts": ["armi = armi.__main__:main"]},
     install_requires=[
         "configparser",
-        "coverage<=6.5.0",
+        "coverage",
         "future",
         "h5py>=3.0",
         "htmltree",
@@ -76,8 +76,8 @@ setup(
         "yamlize==0.7.1",
     ],
     extras_require={
-        "mpi": ["mpi4py==3.0.3"],
-        "grids": ["wxpython<=4.1.1"],
+        "mpi": ["mpi4py"],
+        "grids": ["wxpython==4.2.1"],
         "memprof": ["psutil"],
         "dev": [
             "black==20.8b1",
diff --git a/tox.ini b/tox.ini
index 33eb3217..8795e5a9 100644
--- a/tox.ini
+++ b/tox.ini
@@ -39,7 +39,7 @@ deps=
 allowlist_externals =
     /usr/bin/mpiexec
 commands =
-    mpiexec -n 2 --use-hwthread-cpus coverage run --rcfile=.coveragerc -m pytest --cov=armi --cov-config=.coveragerc --ignore=venv --cov-fail-under=80 armi/tests/test_mpiFeatures.py
+    mpiexec -n 2 --use-hwthread-cpus coverage run --rcfile=.coveragerc -m pytest --cov=armi --cov-config=.coveragerc --cov-report=lcov --ignore=venv --cov-fail-under=80 armi/tests/test_mpiFeatures.py
 
 # Second, run code coverage over the rest of the unit tests, and combine the coverage results together
 [testenv:cov2]
@@ -51,7 +51,7 @@ deps=
 allowlist_externals =
     /usr/bin/mpiexec
 commands =
-    coverage run --rcfile=.coveragerc -m pytest -n 4 --cov=armi --cov-config=.coveragerc --cov-append --ignore=venv armi
+    coverage run --rcfile=.coveragerc -m pytest -n 4 --cov=armi --cov-config=.coveragerc --cov-report=lcov --cov-append --ignore=venv armi
     coverage combine --rcfile=.coveragerc --keep -a
 
 # NOTE: This only runs the MPI unit tests.


Test patch:
diff --git a/.github/workflows/unittests.yaml b/.github/workflows/unittests.yaml
index 0cde1b32..475d7801 100644
--- a/.github/workflows/unittests.yaml
+++ b/.github/workflows/unittests.yaml
@@ -8,7 +8,7 @@ jobs:
     runs-on: ubuntu-22.04
     strategy:
       matrix:
-        python: [3.7, 3.8, 3.9, '3.10']
+        python: [3.7, 3.8, 3.9, '3.10', '3.11']
 
     steps:
       - uses: actions/checkout@v2
diff --git a/.github/workflows/wintests.yaml b/.github/workflows/wintests.yaml
index 966118c8..4d1dd435 100644
--- a/.github/workflows/wintests.yaml
+++ b/.github/workflows/wintests.yaml
@@ -12,7 +12,7 @@ jobs:
       - name: Setup Python
         uses: actions/setup-python@v2
         with:
-          python-version: '3.10'
+          python-version: '3.11'
       - name: Upgrade PIP
         run: python -m pip install --upgrade pip
       - name: Install deps
diff --git a/armi/cli/tests/test_runEntryPoint.py b/armi/cli/tests/test_runEntryPoint.py
index 7a09dd16..069ad20e 100644
--- a/armi/cli/tests/test_runEntryPoint.py
+++ b/armi/cli/tests/test_runEntryPoint.py
@@ -56,8 +56,8 @@ class TestInitializationEntryPoints(unittest.TestCase):
                     settingsArg,
                     msg=(
                         f"A settings file argument was expected for {entryPoint}, "
-                        f"but does not exist. This is a error in the EntryPoint "
-                        f"implementation."
+                        "but does not exist. This is a error in the EntryPoint "
+                        "implementation."
                     ),
                 )
 
@@ -224,11 +224,17 @@ class TestExtractInputs(unittest.TestCase):
     def test_extractInputsBasics(self):
         ei = ExtractInputs()
         ei.addOptions()
-        ei.parse_args(["/path/to/fake.h5"])
+        ei.parse_args(["/path/to/fake"])
 
         self.assertEqual(ei.name, "extract-inputs")
         self.assertEqual(ei.args.output_base, "/path/to/fake")
 
+        with mockRunLogs.BufferLog() as mock:
+            self.assertEqual("", mock.getStdout())
+            with self.assertRaises(FileNotFoundError):
+                # The "fake" file doesn't exist, so this should fail.
+                ei.invoke()
+
 
 class TestInjectInputs(unittest.TestCase):
     def test_injectInputsBasics(self):
@@ -239,6 +245,32 @@ class TestInjectInputs(unittest.TestCase):
         self.assertEqual(ii.name, "inject-inputs")
         self.assertIsNone(ii.args.blueprints)
 
+    def test_injectInputsInvokeIgnore(self):
+        ii = InjectInputs()
+        ii.addOptions()
+        ii.parse_args(["/path/to/fake.h5"])
+
+        with mockRunLogs.BufferLog() as mock:
+            self.assertEqual("", mock.getStdout())
+            ii.invoke()
+            self.assertIn("No settings", mock.getStdout())
+
+    def test_injectInputsInvokeNoData(self):
+        with TemporaryDirectoryChanger():
+            # init CLI
+            ii = InjectInputs()
+            ii.addOptions()
+
+            bp = os.path.join(TEST_ROOT, "refSmallReactor.yaml")
+            ii.parse_args(["/path/to/fake.h5", "--blueprints", bp])
+
+            # invoke and check log
+            with mockRunLogs.BufferLog() as mock:
+                self.assertEqual("", mock.getStdout())
+                with self.assertRaises(FileNotFoundError):
+                    # The "fake.h5" doesn't exist, so this should fail.
+                    ii.invoke()
+
 
 class TestMigrateInputs(unittest.TestCase):
     def test_migrateInputsBasics(self):
@@ -287,11 +319,17 @@ class TestReportsEntryPoint(unittest.TestCase):
     def test_reportsEntryPointBasics(self):
         rep = ReportsEntryPoint()
         rep.addOptions()
-        rep.parse_args(["-h5db", "/path/to/fake.yaml"])
+        rep.parse_args(["-h5db", "/path/to/fake.h5"])
 
         self.assertEqual(rep.name, "report")
         self.assertEqual(rep.settingsArgument, "optional")
 
+        with mockRunLogs.BufferLog() as mock:
+            self.assertEqual("", mock.getStdout())
+            with self.assertRaises(ValueError):
+                # The "fake.h5" doesn't exist, so this should fail.
+                rep.invoke()
+
 
 class TestCompareIsotxsLibsEntryPoint(unittest.TestCase):
     def test_compareIsotxsLibsBasics(self):
diff --git a/armi/reactor/converters/tests/test_uniformMesh.py b/armi/reactor/converters/tests/test_uniformMesh.py
index 35fc66a3..fb1ad27a 100644
--- a/armi/reactor/converters/tests/test_uniformMesh.py
+++ b/armi/reactor/converters/tests/test_uniformMesh.py
@@ -415,8 +415,8 @@ class TestUniformMesh(unittest.TestCase):
 
         self.converter.convert(self.r)
         for ib, b in enumerate(self.converter.convReactor.core.getBlocks()):
-            b.p.mgFlux = range(33)
-            b.p.adjMgFlux = range(33)
+            b.p.mgFlux = list(range(33))
+            b.p.adjMgFlux = list(range(33))
             b.p.fastFlux = 2.0
             b.p.flux = 5.0
             b.p.power = 5.0
@@ -507,8 +507,8 @@ class TestGammaUniformMesh(unittest.TestCase):
 
         # set original parameters on pre-mapped core with non-uniform assemblies
         for b in self.r.core.getBlocks():
-            b.p.mgFlux = range(33)
-            b.p.adjMgFlux = range(33)
+            b.p.mgFlux = list(range(33))
+            b.p.adjMgFlux = list(range(33))
             b.p.fastFlux = 2.0
             b.p.flux = 5.0
             b.p.power = 5.0
diff --git a/armi/tests/test_runLog.py b/armi/tests/test_runLog.py
index fb70f8f0..15e5c101 100644
--- a/armi/tests/test_runLog.py
+++ b/armi/tests/test_runLog.py
@@ -88,6 +88,15 @@ class TestRunLog(unittest.TestCase):
         self.assertIn("Hello", streamVal, msg=streamVal)
         self.assertIn("world", streamVal, msg=streamVal)
 
+    def test_getWhiteSpace(self):
+        log = runLog._RunLog(0)
+        space0 = len(log.getWhiteSpace(0))
+        space1 = len(log.getWhiteSpace(1))
+        space9 = len(log.getWhiteSpace(9))
+
+        self.assertGreater(space1, space0)
+        self.assertEqual(space1, space9)
+
     def test_warningReport(self):
         """A simple test of the warning tracking and reporting logic."""
         # create the logger and do some logging
@@ -125,6 +134,11 @@ class TestRunLog(unittest.TestCase):
         self.assertNotIn("invisible", streamVal, msg=streamVal)
         self.assertEqual(streamVal.count("test_warningReport"), 2, msg=streamVal)
 
+        # bonus check: edge case in duplicates filter
+        backupLog, log.logger = log.logger, None
+        self.assertIsNone(log.getDuplicatesFilter())
+        log.logger = backupLog
+
     def test_warningReportInvalid(self):
         """A test of warningReport in an invalid situation."""
         # create the logger and do some logging
diff --git a/requirements-testing.txt b/requirements-testing.txt
index 1efa2670..a30ad27c 100644
--- a/requirements-testing.txt
+++ b/requirements-testing.txt
@@ -1,7 +1,4 @@
 numpy>=1.21,<=1.23.5
 ipython>=7.34.0,<=8.12.0
 
-# docutils 0.17 introduced a bug that prevents bullets from rendering
-# see https://github.com/terrapower/armi/issues/274
-docutils <0.17
 -e .[memprof,dev]