Repository: terrapower/armi
Pull request #1970 (2024-10-24 17:05:31)

Add new memory runLog info to memoryProfiler.py 
## What is the change?
Adds some potentially useful `runLog` statements to the memory profiler interface on how much memory is is use , how much is available, and the total available for a simulation. The first two are dynamic (i.e., change with (cycle, node)) while the third is static. 

## Why is the change being made?
Some memory intensive downstream analysis found this useful, so we thought we'd push it up to ARMI. 

---

## Checklist

<!--
    You (the pull requester) should put an `x` in the boxes below you have completed.

    (If a checkbox doesn't apply to your PR, check it anyway.)
-->

- [x] This PR has only [one purpose or idea](https://terrapower.github.io/armi/developer/tooling.html#one-idea-one-pr).
- [x] [Tests](https://terrapower.github.io/armi/developer/tooling.html#test-it) have been added/updated to verify any new/changed code.

<!-- 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/developer/tooling.html#add-release-notes) have been updated if necessary.
- [x] The [documentation](https://terrapower.github.io/armi/developer/tooling.html#document-it) is still up-to-date in the `doc` folder.
- [x] The dependencies are still up-to-date in `pyproject.toml`.


Patch:
diff --git a/armi/bookkeeping/db/database.py b/armi/bookkeeping/db/database.py
index 06ab9c04..4e484c0e 100644
--- a/armi/bookkeeping/db/database.py
+++ b/armi/bookkeeping/db/database.py
@@ -20,7 +20,7 @@ component level. As a result, the structure of the underlying data is bound to t
 hierarchical Composite Reactor Model. Furthermore, this database format is intended to
 be more dynamic, permitting as-yet undeveloped levels and classes in the Composite
 Reactor Model to be supported as they are added. More high-level discussion is
-contained in :doc:`/user/outputs/database`.
+contained in :ref:`database-file`.
 
 The :py:class:`Database` class contains most of the functionality for interacting
 with the underlying data. This includes things like dumping a Reactor state to the
diff --git a/armi/bookkeeping/memoryProfiler.py b/armi/bookkeeping/memoryProfiler.py
index c249953c..12f9aebe 100644
--- a/armi/bookkeeping/memoryProfiler.py
+++ b/armi/bookkeeping/memoryProfiler.py
@@ -36,6 +36,8 @@ See Also
 https://pythonhosted.org/psutil/
 https://docs.python.org/3/library/gc.html#gc.garbage
 """
+from math import floor
+from os import cpu_count
 from typing import Optional
 import gc
 import sys
@@ -46,6 +48,7 @@ from armi import mpiActions
 from armi import runLog
 from armi.reactor.composites import ArmiObject
 from armi.utils import tabulate
+from armi.utils.customExceptions import NonexistentSetting
 
 try:
     # psutil is an optional requirement, since it doesnt support MacOS very well
@@ -68,6 +71,28 @@ def describeInterfaces(cs):
     return (MemoryProfiler, {})
 
 
+def getTotalJobMemory(nTasksPerNode):
+    """Function to calculate the total memory of a job. This is a constant during a simulation."""
+    cpuPerNode = cpu_count()
+    ramPerCpuGB = psutil.virtual_memory().total / (1024**3) / cpuPerNode
+    if nTasksPerNode == 0:
+        nTasksPerNode = cpuPerNode
+    cpusPerTask = floor(cpuPerNode / nTasksPerNode)
+    jobMem = nTasksPerNode * cpusPerTask * ramPerCpuGB
+    return jobMem
+
+
+def getCurrentMemoryUsage():
+    """This scavenges the memory profiler in ARMI to get the current memory usage."""
+    memUsageAction = PrintSystemMemoryUsageAction()
+    memUsageAction.broadcast()
+    smpu = SystemAndProcessMemoryUsage()
+    memUsages = memUsageAction.gather(smpu)
+    # Grab virtual memory instead of physical. There is a large discrepancy, we will be conservative
+    memoryUsageInMB = sum([mu.processVirtualMemoryInMB for mu in memUsages])
+    return memoryUsageInMB
+
+
 class MemoryProfiler(interfaces.Interface):
 
     name = "memoryProfiler"
@@ -78,6 +103,7 @@ class MemoryProfiler(interfaces.Interface):
 
     def interactBOL(self):
         interfaces.Interface.interactBOL(self)
+        self.printCurrentMemoryState()
         mpiAction = PrintSystemMemoryUsageAction()
         mpiAction.broadcast().invoke(self.o, self.r, self.cs)
         mpiAction.printUsage("BOL SYS_MEM")
@@ -88,6 +114,8 @@ class MemoryProfiler(interfaces.Interface):
             mpiAction.broadcast().invoke(self.o, self.r, self.cs)
 
     def interactEveryNode(self, cycle, node):
+        self.printCurrentMemoryState()
+
         mp = PrintSystemMemoryUsageAction()
         mp.broadcast()
         mp.invoke(self.o, self.r, self.cs)
@@ -101,11 +129,30 @@ class MemoryProfiler(interfaces.Interface):
             mpiAction.broadcast().invoke(self.o, self.r, self.cs)
 
     def interactEOL(self):
-        r"""End of life hook. Good place to wrap up or print out summary outputs."""
+        """End of life hook. Good place to wrap up or print out summary outputs."""
         if self.cs["debugMem"]:
             mpiAction = ProfileMemoryUsageAction("EOL")
             mpiAction.broadcast().invoke(self.o, self.r, self.cs)
 
+    def printCurrentMemoryState(self):
+        """Print the current memory footprint and available memory."""
+        try:
+            nTasksPerNode = self.cs["nTasksPerNode"]
+        except NonexistentSetting:
+            runLog.extra(
+                "To view memory consumed, remaining available, and total allocated for a case, "
+                "add the setting 'nTasksPerNode' to your application."
+            )
+            return
+        totalMemoryInGB = getTotalJobMemory(nTasksPerNode)
+        currentMemoryUsageInGB = getCurrentMemoryUsage() / 1024
+        availableMemoryInGB = totalMemoryInGB - currentMemoryUsageInGB
+        runLog.info(
+            f"Currently using {currentMemoryUsageInGB} GB of memory. "
+            f"There is {availableMemoryInGB} GB of memory left. "
+            f"There is a total allocation of {totalMemoryInGB} GB."
+        )
+
     def displayMemoryUsage(self, timeDescription):
         r"""
         Print out some information to stdout about the memory usage of ARMI.
diff --git a/armi/materials/custom.py b/armi/materials/custom.py
index d5f6ebc2..9db7b9a6 100644
--- a/armi/materials/custom.py
+++ b/armi/materials/custom.py
@@ -16,7 +16,7 @@
 Custom materials are ones that you can specify all the number densities yourself.
 
 Useful for benchmarking when you have a particular specified material density.
-Use the isotopic input described in :doc:`/user/inputs/blueprints`.
+Use the isotopic input described in :ref:`bp-input-file`.
 
 The density function gets applied from custom isotopics by
 :py:meth:`armi.reactor.blueprints.isotopicOptions.CustomIsotopic.apply`.
diff --git a/armi/materials/thU.py b/armi/materials/thU.py
index e46b9c6a..e37599df 100644
--- a/armi/materials/thU.py
+++ b/armi/materials/thU.py
@@ -15,10 +15,8 @@
 """
 Thorium Uranium metal.
 
-Data is from [IAEA-TECDOCT-1450]_.
+Data is from [IAEA-TECDOC-1450]_.
 
-.. [IAEA-TECDOCT-1450] Thorium fuel cycle -- Potential benefits and challenges, IAEA-TECDOC-1450 (2005).
-    https://www-pub.iaea.org/mtcd/publications/pdf/te_1450_web.pdf
 """
 
 from armi import runLog
diff --git a/armi/materials/thorium.py b/armi/materials/thorium.py
index 62807a87..5daddeb7 100644
--- a/armi/materials/thorium.py
+++ b/armi/materials/thorium.py
@@ -15,10 +15,8 @@
 """
 Thorium Metal.
 
-Data is from [IAEA-TECDOCT-1450]_.
+Data is from [IAEA-TECDOC-1450]_.
 
-.. [IAEA-TECDOCT-1450] Thorium fuel cycle -- Potential benefits and challenges, IAEA-TECDOC-1450 (2005).
-    https://www-pub.iaea.org/mtcd/publications/pdf/te_1450_web.pdf
 """
 from armi.materials.material import FuelMaterial
 from armi.utils.units import getTk
diff --git a/armi/materials/thoriumOxide.py b/armi/materials/thoriumOxide.py
index 157f4a0c..af5177c5 100644
--- a/armi/materials/thoriumOxide.py
+++ b/armi/materials/thoriumOxide.py
@@ -15,9 +15,9 @@
 """
 Thorium Oxide solid ceramic.
 
-Data is from [IAEA-TECDOCT-1450]_.
+Data is from [IAEA-TECDOC-1450]_.
 
-.. [IAEA-TECDOCT-1450] Thorium fuel cycle -- Potential benefits and challenges, IAEA-TECDOC-1450 (2005).
+.. [IAEA-TECDOC-1450] Thorium fuel cycle -- Potential benefits and challenges, IAEA-TECDOC-1450 (2005).
     https://www-pub.iaea.org/mtcd/publications/pdf/te_1450_web.pdf
 """
 from armi import runLog
diff --git a/armi/physics/fuelCycle/fuelHandlers.py b/armi/physics/fuelCycle/fuelHandlers.py
index 035bfe96..0c1f8c45 100644
--- a/armi/physics/fuelCycle/fuelHandlers.py
+++ b/armi/physics/fuelCycle/fuelHandlers.py
@@ -18,7 +18,7 @@ fuel processing (in fluid systems).
 The :py:class:`FuelHandlerInterface` instantiates a ``FuelHandler``, which is typically a user-defined
 subclass the :py:class:`FuelHandler` object in custom shuffle-logic input files.
 Users point to the code modules with their custom fuel handlers using the
-``shuffleLogic`` and ``fuelHandlerName`` settings, as described in :doc:`/user/inputs/fuel_management`.
+``shuffleLogic`` and ``fuelHandlerName`` settings, as described in :ref:`fuel-management-input`.
 These subclasses override ``chooseSwaps`` that determine
 the particular shuffling of a case.
 
@@ -177,7 +177,7 @@ class FuelHandler:
 
         This is the default shuffle control function. Usually you would override this
         with your own in a custom shuffleLogic.py file. For more details about how this
-        works, refer to :doc:`/user/inputs/fuel_management`.
+        works, refer to :ref:`fuel-management-input`.
 
         This will get bound to the default FuelHandler as a static method below. This is
         done to allow a user to mix and match FuelHandler class implementations and
diff --git a/armi/reactor/blueprints/__init__.py b/armi/reactor/blueprints/__init__.py
index a17ddbdf..48532ed5 100644
--- a/armi/reactor/blueprints/__init__.py
+++ b/armi/reactor/blueprints/__init__.py
@@ -22,7 +22,7 @@ custom material properties or basic structures like the assemblies in use.
 This is essentially a wrapper for a yaml loader.
 The given yaml file is expected to rigidly adhere to given key:value pairings.
 
-See the :doc:`blueprints documentation </user/inputs/blueprints>` for more details.
+See the :ref:`blueprints documentation <bp-input-file>` for more details.
 
 The file structure is expectation is::
 
diff --git a/armi/reactor/blueprints/reactorBlueprint.py b/armi/reactor/blueprints/reactorBlueprint.py
index d4b6773c..75b363a6 100644
--- a/armi/reactor/blueprints/reactorBlueprint.py
+++ b/armi/reactor/blueprints/reactorBlueprint.py
@@ -15,7 +15,7 @@
 """
 Definitions of top-level reactor arrangements like the Core (default), SFP, etc.
 
-See documentation of blueprints in :doc:`/user/inputs/blueprints` for more context. See example in
+See documentation of blueprints in :ref:`bp-input-file` for more context. See example in
 :py:mod:`armi.reactor.blueprints.tests.test_reactorBlueprints`.
 
 This was built to replace the old system that loaded the core geometry from the ``cs['geometry']``
diff --git a/armi/reactor/flags.py b/armi/reactor/flags.py
index cba4efe5..190d58c7 100644
--- a/armi/reactor/flags.py
+++ b/armi/reactor/flags.py
@@ -23,7 +23,7 @@ By default, the specific Flags that are bound to each object are derived by that
 object's name when constructed; if the name contains any valid flag names, those Flags
 will be assigned to the object. However, specific Flags may be specified within
 blueprints, in which case the name is ignored and only the explicitly-requested Flags
-are applied (see :doc:`/user/inputs/blueprints` for more details).
+are applied (see :ref:`bp-input-file` for more details).
 
 Individual Flags tend to be various nouns and adjectives that describe common objects
 that go into a reactor (e.g. "fuel", "shield", "control", "duct", "plenum", etc.). In
diff --git a/armi/settings/settingsValidation.py b/armi/settings/settingsValidation.py
index ed11f5b3..6a4534a7 100644
--- a/armi/settings/settingsValidation.py
+++ b/armi/settings/settingsValidation.py
@@ -778,9 +778,9 @@ def validateVersion(versionThis: str, versionRequired: str) -> bool:
     bool
         Does this version match the version in the Settings file/object?
     """
-    fullV = "\d+\.\d+\.\d+"
-    medV = "\d+\.\d+"
-    minV = "\d+"
+    fullV = r"\d+\.\d+\.\d+"
+    medV = r"\d+\.\d+"
+    minV = r"\d+"
 
     if versionRequired == "uncontrolled":
         # This default flag means we don't want to check the version.
diff --git a/armi/utils/gridEditor.py b/armi/utils/gridEditor.py
index 5bf53033..025afa38 100644
--- a/armi/utils/gridEditor.py
+++ b/armi/utils/gridEditor.py
@@ -27,6 +27,12 @@ as the first argument and the system will load up the associated grid, e.g.::
     $ python -m armi grids FFTF-blueprints.yaml
 
 
+.. figure:: /.static/gridEditor.png
+    :align: center
+
+    An example of the Grid Editor being used on a FFTF input file
+
+
 **Known Issues**
 
 * There is no action stack or undo functionality. Save frequently if you want to
@@ -48,6 +54,7 @@ as the first argument and the system will load up the associated grid, e.g.::
   specifiers. Adding zoom would make for a fun first task to a new developer interested
   in computer graphics.
 """
+
 import colorsys
 import enum
 import io
diff --git a/doc/conf.py b/doc/conf.py
index d21b56a8..e0696642 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -212,6 +212,26 @@ class PyReverse(Directive):
             )
 
 
+class SkipNeedsDirective(Directive):
+    """
+    A no-op directive that filters out any sphinx-need directives from docs.
+
+    Temporary patch until we figure out a different/better way to maintain formal QA docs.
+    """
+
+    has_content = True
+    required_arguments = 1
+    optional_arguments = 0
+    final_argument_whitespace = True
+
+    def run(self):
+        _delete_opt = self.options.get("delete")
+        _collapse = self.options.get("collapse")
+        _jinja_content = self.options.get("jinja_content")
+        _hide = "hide" in self.options
+        return []
+
+
 def autodoc_skip_member_handler(app, what, name, obj, skip, options):
     """Manually exclude certain methods/functions from docs."""
     # exclude special methods from unittest
@@ -234,8 +254,8 @@ def setup(app):
     app.add_domain(PatchedPythonDomain, override=True)
     app.add_directive("exec", ExecDirective)
     app.add_directive("pyreverse", PyReverse)
-    app.add_directive("impl", directives.admonitions.Note)
-    app.add_directive("test", directives.admonitions.Note)
+    app.add_directive("impl", SkipNeedsDirective)
+    app.add_directive("test", SkipNeedsDirective)
 
     # making tutorial data dir
     dataDir = pathlib.Path("user") / ".." / "anl-afci-177"
diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst
index 9cfed61e..6650d007 100644
--- a/doc/release/0.2.rst
+++ b/doc/release/0.2.rst
@@ -175,7 +175,7 @@ What's new in ARMI
 #. Changed the default Git branch name to ``main``.
 #. Moved math utilities into their own module.
 #. Moved ``newReports`` into their final location in ``armi/bookkeeping/report/``.
-#. Removed ``_swapFluxParam`` method (`PR#665 <https://github.com/terrapower/armi/pull/665#discussion_r893348409>`_)
+#. Removed ``_swapFluxParam`` method (`PR#665 <https://github.com/terrapower/armi/pull/665#discussion_r893348409>`__)
 #. Removed the last usage of ``settingsRules``; now only use ``settingsValidation``.
 #. Removed separate blueprints in snapshot runs, they must come from the database (`PR#872 https://github.com/terrapower/armi/pull/872`)
 #. Added reporting of neutron and gamma energy groups in the XS library ``__repr__``.
@@ -183,7 +183,7 @@ What's new in ARMI
 #. Store thermally expanded block heights at BOL in ``armi/reactor/reactors.py::Core::processLoading``.
 #. Added neutronics settings: ``inners`` and ``outers`` for downstream support.
 #. Removed unused Thermal Hydraulics settings.
-#. Replaced setting ``stationaryBlocks`` with ``stationaryBlockFlags`` setting (`PR#665 <https://github.com/terrapower/armi/pull/665>`_)
+#. Replaced setting ``stationaryBlocks`` with ``stationaryBlockFlags`` setting (`PR#665 <https://github.com/terrapower/armi/pull/665>`__))
 #. Changed the default value of the ``trackAssems`` setting to ``False``.
 #. Add setting ``inputHeightsConsideredHot`` to enable thermal expansion of assemblies at BOL.
 
@@ -215,7 +215,7 @@ Release Date: 2022-02-08
 What's new in ARMI
 ------------------
 #. Upgrading the version of NumPy for a security alert (`PR#530 <https://github.com/terrapower/armi/pull/530>`_)
-#. Upgraded ThoriumOxide material (`PR#558 <https://github.com/terrapower/armi/pull/548>`_)
+#. Upgraded ThoriumOxide material (`PR#548 <https://github.com/terrapower/armi/pull/548>`_)
 #. Upgraded Lithium material (`PR#546 <https://github.com/terrapower/armi/pull/546>`_)
 #. Improved Helix class (`PR#558 <https://github.com/terrapower/armi/pull/558>`_)
 
diff --git a/doc/release/0.4.rst b/doc/release/0.4.rst
index c2f54a32..7fb0b475 100644
--- a/doc/release/0.4.rst
+++ b/doc/release/0.4.rst
@@ -21,7 +21,7 @@ New Features
 #. Flags can now be defined with letters and numbers. (`PR#1966 <https://github.com/terrapower/armi/pull/1966>`_)
 #. Plugins can provide the ``getAxialExpansionChanger`` hook to customize axial expansion. (`PR#1870 <https://github.com/terrapower/armi/pull/1870>`_)
 #. New plugin hook ``beforeReactorConstruction`` added to enable plugins to process case settings before reactor init. (`PR#1945 <https://github.com/terrapower/armi/pull/1945>`_)
-#. Provide ``Block.getInputHeight`` for determining the height of a block from blueprints. (`PR#1927 <https://github.com/terrapower/armi/pull/1927`_)
+#. Provide ``Block.getInputHeight`` for determining the height of a block from blueprints. (`PR#1927 <https://github.com/terrapower/armi/pull/1927>`_)
 #. Improve performance by changing the lattice physics interface so that cross sections are not updated on ``everyNode`` calls during coupled calculations (`PR#1963 <https://github.com/terrapower/armi/pull/1963>`_)
 #. Improve efficiency of reaction rate calculations. (`PR#1887 <https://github.com/terrapower/armi/pull/1887>`_)
 #. TBD
diff --git a/doc/user/index.rst b/doc/user/index.rst
index 360dea65..7b95e6a2 100644
--- a/doc/user/index.rst
+++ b/doc/user/index.rst
@@ -12,7 +12,6 @@ analyzing ARMI output files, etc.
    :maxdepth: 2
    :numbered:
 
-   user_install
    inputs
    outputs
    manual_data_access
diff --git a/doc/user/inputs.rst b/doc/user/inputs.rst
index 4e650079..918153cc 100644
--- a/doc/user/inputs.rst
+++ b/doc/user/inputs.rst
@@ -67,7 +67,7 @@ Here is an excerpt from a settings file:
     :lines: 3-15
 
 A full listing of settings available in the framework may be found in the
-`Table of all global settings <#settings-report>`_ .
+:ref:`Table of all global settings <settings-report>` .
 
 Many settings are provided by the ARMI Framework, and others are defined by various plugins.
 
@@ -84,11 +84,12 @@ them.
 Note that one settings input file is required for each ARMI case, though many ARMI cases can refer
 to the same Blueprints, Core Map, and Fuel Management inputs.
 
-.. tip:: The ARMI GUI is not yet included in the open-source ARMI framework
+.. tip:: The ARMI GUI is not yet included in the open-source ARMI framework, but a simple
+    grid editor GUI is, as described in :ref:`grids`
 
 The assembly clicker
 ^^^^^^^^^^^^^^^^^^^^
-The assembly clicker (in the ``grids`` editor) allows users to define the 2-D layout of the assemblies defined in the
+The assembly clicker (aka the :py:mod:`Grid Editor <armi.utils.gridEditor>`) allows users to define the 2-D layout of the assemblies defined in the
 :ref:`bp-input-file`. This can be done in hexagon or cartesian. The results of this arrangement get written to
 grids in blueprints. Click on the assembly palette on the right and click on the locations where you want to put the
 assembly. By default, the input assumes a 1/3 core model, but you can create a full core model through the menu.
@@ -1372,6 +1373,7 @@ The code will crash if materials used in :ref:`blocks-and-components` contain nu
 .. |Tinput| replace:: T\ :sub:`input`
 .. |Thot| replace:: T\ :sub:`hot`
 
+.. _fuel-management-input:
 
 Fuel Management Input
 =====================
diff --git a/doc/user/outputs.rst b/doc/user/outputs.rst
index a0e39f15..71152caa 100644
--- a/doc/user/outputs.rst
+++ b/doc/user/outputs.rst
@@ -64,6 +64,8 @@ Some Linux users tend to use the **tail** command to monitor the progress of an
 
 This provides live information on the progress.
 
+.. _database-file:
+
 The Database File
 =================
 The **database** file is a self-contained complete (or nearly complete) binary
diff --git a/doc/user/radial_and_axial_expansion.rst b/doc/user/radial_and_axial_expansion.rst
index 2b47c600..f1b75cae 100644
--- a/doc/user/radial_and_axial_expansion.rst
+++ b/doc/user/radial_and_axial_expansion.rst
@@ -2,7 +2,11 @@
 Radial and Axial Expansion and Contraction
 ******************************************
 
-ARMI natively supports linear expansion in both the radial and axial dimensions. These expansion types function independently of one another and each have their own set of underlying assumptions and use-cases. The remainder of this section is described as follows: in Section :ref:`thermalExpansion` the methodology used for thermal expansion within ARMI is described; in Sections :ref:`radialExpansion` and :ref:`axialExpansion`, we describe the design, limitations, and intended functionality of radial and axial expansion, respectively.
+ARMI natively supports linear expansion in both the radial and axial dimensions.
+These expansion types function independently of one another and each have their
+own set of underlying assumptions and use-cases. The remainder of this section
+is described as follows: in Section :ref:`thermalExpansion` the methodology used
+for thermal expansion within ARMI is described.
 
 .. _thermalExpansion:
 
@@ -14,13 +18,18 @@ ARMI treats thermal expansion as a linear phenomena using the standard linear ex
     \frac{\Delta L}{L_0} = \alpha(T) \Delta T,
     :label: linearExp
 
-where, :math:`\Delta L` and :math:`\Delta T` are the change in length and temperature from the reference state, respectively, and :math:`\alpha` is the thermal expansion coefficient relative to :math:`T_0`. Expanding and rearranging Equation :eq:`linearExp`, we can obtain an expression for the new length, :math:`L_1`,
+where, :math:`\Delta L` and :math:`\Delta T` are the change in length and
+temperature from the reference state, respectively, and :math:`\alpha` is the
+thermal expansion coefficient relative to :math:`T_0`. Expanding and rearranging
+Equation :eq:`linearExp`, we can obtain an expression for the new length,
+:math:`L_1`,
 
 .. math::
     L_1 = L_0\left[1 + \alpha(T_1)\left(T_1 - T_0\right) \right].
     :label: newLength
 
-Given Equation :eq:`linearExp`, we can create expressions for the change in length between our "hot" temperature (Equation :eq:`hotExp`)
+Given Equation :eq:`linearExp`, we can create expressions for the change in
+length between our "hot" temperature (Equation :eq:`hotExp`)
 
 .. math::
     \begin{aligned}
@@ -38,7 +47,11 @@ and "non-reference" temperature, :math:`T_c` (Equation :eq:`nonRefExp`),
     \end{aligned}
     :label: nonRefExp
 
-These are used within ARMI to enable thermal expansion and contraction with a temperature not equal to the reference temperature, :math:`T_0`. By taking the difference between Equation :eq:`hotExp` and :eq:`nonRefExp`, we can obtain an expression relating the change in length, :math:`L_h - L_c`, to the reference length, :math:`L_0`,
+These are used within ARMI to enable thermal expansion and contraction with a
+temperature not equal to the reference temperature, :math:`T_0`. By taking the
+difference between Equation :eq:`hotExp` and :eq:`nonRefExp`, we can obtain an
+expression relating the change in length, :math:`L_h - L_c`, to the reference
+length, :math:`L_0`,
 
 .. math::
     \begin{aligned}
@@ -47,20 +60,27 @@ These are used within ARMI to enable thermal expansion and contraction with a te
     \end{aligned}
     :label: diffHotNonRef
 
-Using Equations :eq:`diffHotNonRef` and :eq:`nonRefExp`, we can obtain an expression for the change in length, :math:`L_h - L_c`, relative to the non-reference temperature,
+Using Equations :eq:`diffHotNonRef` and :eq:`nonRefExp`, we can obtain an
+expression for the change in length, :math:`L_h - L_c`, relative to the
+non-reference temperature,
 
 .. math::
     \frac{L_h - L_c}{L_c} &= \frac{L_h - L_c}{L_0} \frac{L_0}{L_c}\\
     &= \left( \frac{L_h}{L_0} - \frac{L_c}{L_0} \right) \left( 1 + \alpha(T_c)\left(T_c - T_0\right) \right)^{-1}.
     :label: expNewRelative
 
-Using Equations :eq:`hotExp` and :eq:`nonRefExp`, we can simplify Equation :eq:`expNewRelative` to find,
+Using Equations :eq:`hotExp` and :eq:`nonRefExp`, we can simplify Equation
+:eq:`expNewRelative` to find,
 
 .. math::
     \frac{L_h - L_c}{L_c} = \frac{\alpha(T_h) \left(T_h - T_0\right) - \alpha(T_c)\left(T_c - T_0\right)}{1 + \alpha(T_c)\left(T_c - T_0\right)}.
     :label: linearExpansionFactor
 
-Equation :eq:`linearExpansionFactor` is the expression used by ARMI in :py:meth:`linearExpansionFactor <armi.materials.material.Material.linearExpansionFactor>`.
+Equation :eq:`linearExpansionFactor` is the expression used by ARMI in
+:py:meth:`linearExpansionFactor
+<armi.materials.material.Material.linearExpansionFactor>`.
 
 .. note::
-    :py:meth:`linearExpansionPercent <armi.materials.material.Material.linearExpansionPercent>` returns :math:`\frac{L - L_0}{L_0}` in %.
+    :py:meth:`linearExpansionPercent
+    <armi.materials.material.Material.linearExpansionPercent>` returns
+    :math:`\frac{L - L_0}{L_0}` in %.
diff --git a/doc/user/user_install.rst b/doc/user/user_install.rst
index e45a45d1..b6ace5da 100644
--- a/doc/user/user_install.rst
+++ b/doc/user/user_install.rst
@@ -1,3 +1,5 @@
+.. Note that this is currently within the user dir but rendered at the top level.
+
 ************
 Installation
 ************


Test patch:
diff --git a/armi/bookkeeping/tests/test_memoryProfiler.py b/armi/bookkeeping/tests/test_memoryProfiler.py
index b750ae1c..94854cf8 100644
--- a/armi/bookkeeping/tests/test_memoryProfiler.py
+++ b/armi/bookkeeping/tests/test_memoryProfiler.py
@@ -13,11 +13,16 @@
 # limitations under the License.
 
 """Tests for memoryProfiler."""
+from unittest.mock import MagicMock, patch
 import logging
 import unittest
 
 from armi import runLog
 from armi.bookkeeping import memoryProfiler
+from armi.bookkeeping.memoryProfiler import (
+    getCurrentMemoryUsage,
+    getTotalJobMemory,
+)
 from armi.reactor.tests import test_reactors
 from armi.tests import mockRunLogs, TEST_ROOT
 
@@ -29,7 +34,9 @@ class TestMemoryProfiler(unittest.TestCase):
             {"debugMem": True},
             inputFileName="smallestTestReactor/armiRunSmallest.yaml",
         )
-        self.memPro = self.o.getInterface("memoryProfiler")
+        self.memPro: memoryProfiler.MemoryProfiler = self.o.getInterface(
+            "memoryProfiler"
+        )
 
     def tearDown(self):
         self.o.removeInterface(self.memPro)
@@ -123,6 +130,73 @@ class TestMemoryProfiler(unittest.TestCase):
         pmua = memoryProfiler.ProfileMemoryUsageAction("timeDesc")
         self.assertEqual(pmua.timeDescription, "timeDesc")
 
+    @patch("psutil.virtual_memory")
+    @patch("armi.bookkeeping.memoryProfiler.cpu_count")
+    def test_getTotalJobMemory(self, mockCpuCount, mockVMem):
+        """Use an example node with 50 GB of total physical memory and 10 CPUs."""
+        mockCpuCount.return_value = 10
+        vMem = MagicMock()
+        vMem.total = (1024**3) * 50
+        mockVMem.return_value = vMem
+
+        expectedArrangement = {0: 50, 1: 50, 2: 50, 3: 45, 4: 40, 5: 50}
+        for nTasksPerNode, jobMemory in expectedArrangement.items():
+            self.assertEqual(getTotalJobMemory(nTasksPerNode), jobMemory)
+
+    @patch("armi.bookkeeping.memoryProfiler.PrintSystemMemoryUsageAction")
+    @patch("armi.bookkeeping.memoryProfiler.SystemAndProcessMemoryUsage")
+    def test_getCurrentMemoryUsage(
+        self, mockSysAndProcMemUse, mockPrintSysMemUseAction
+    ):
+        """Mock the memory usage across 3 different processes and that the total usage is as expected (6 MB)."""
+        self._setMemUseMock(mockPrintSysMemUseAction)
+        self.assertAlmostEqual(getCurrentMemoryUsage(), 6 * 1024)
+
+    @patch("armi.bookkeeping.memoryProfiler.PrintSystemMemoryUsageAction")
+    @patch("armi.bookkeeping.memoryProfiler.SystemAndProcessMemoryUsage")
+    @patch("psutil.virtual_memory")
+    @patch("armi.bookkeeping.memoryProfiler.cpu_count")
+    def test_printCurrentMemoryState(
+        self, mockCpuCount, mockVMem, mock1, mockPrintSysMemUseAction
+    ):
+        """Use an example node with 50 GB of total physical memory and 10 CPUs while using 6 GB."""
+        mockCpuCount.return_value = 10
+        vMem = MagicMock()
+        vMem.total = (1024**3) * 50
+        mockVMem.return_value = vMem
+        self._setMemUseMock(mockPrintSysMemUseAction)
+        with mockRunLogs.BufferLog() as mockLogs:
+            csMock = MagicMock()
+            csMock.__getitem__.return_value = 2
+            self.memPro.cs = csMock
+            self.memPro.printCurrentMemoryState()
+            stdOut = mockLogs.getStdout()
+            self.assertIn("Currently using 6.0 GB of memory.", stdOut)
+            self.assertIn("There is 44.0 GB of memory left.", stdOut)
+            self.assertIn("There is a total allocation of 50.0 GB", stdOut)
+
+    def test_printCurrentMemoryState_noSetting(self):
+        """Test that the try/except works as it should."""
+        expectedStr = (
+            "To view memory consumed, remaining available, and total allocated for a case, "
+            "add the setting 'nTasksPerNode' to your application."
+        )
+        with mockRunLogs.BufferLog() as mockLogs:
+            self.memPro.printCurrentMemoryState()
+            self.assertIn(expectedStr, mockLogs.getStdout())
+
+    def _setMemUseMock(self, mockPrintSysMemUseAction):
+        class mockMemUse:
+            def __init__(self, mem: float):
+                self.processVirtualMemoryInMB = mem
+
+        instance = mockPrintSysMemUseAction.return_value
+        instance.gather.return_value = [
+            mockMemUse(1 * 1024),
+            mockMemUse(2 * 1024),
+            mockMemUse(3 * 1024),
+        ]
+
 
 class KlassCounterTests(unittest.TestCase):
     def get_containers(self):