Repository: terrapower/armi

Issue:
Make example showing how to make blueprints objects in a script
To potentially help with #223, we should make a new example (in the gallery? in blueprints docs?) that shows how to build reactor inputs from a script by using Blueprints objects. This is likely a better option than writing out yaml code directly for script-generated ARMI inputs. 

For example, let's make a script that builds a few basic reactor parts and then either:
* Generates the input files
* Builds the ARMI objects

Discussion:
ntouran: FYI here's what it ends up looking like in the gallery 
![image](https://user-images.githubusercontent.com/53948397/102410859-feaf7f00-3fa5-11eb-8292-31470f7095fe.png)

ntouran: The build failure regarding XS settings is a new one and it's caused by a change in a dependency version, probably in voluptuous, which is treating things differently. I can replicate it on a fresh install of ARMI. 
youngmit: > Yeah this commit added sorted which causes this because there is a `None` in the list getting sorted. [alecthomas/voluptuous@997c8db](https://github.com/alecthomas/voluptuous/commit/997c8dbba92fec3f58b081141c9a4a90e046ada8)

Good find!
youngmit: Sweet!

Patch:
diff --git a/armi/bookkeeping/report/reportingUtils.py b/armi/bookkeeping/report/reportingUtils.py
index a63435ac..c8259e4f 100644
--- a/armi/bookkeeping/report/reportingUtils.py
+++ b/armi/bookkeeping/report/reportingUtils.py
@@ -936,15 +936,13 @@ def makeCoreAndAssemblyMaps(r, cs, generateFullCoreMap=False, showBlockAxMesh=Tr
         assemPlotImage.title = assemPlotImage.title + " ({})".format(plotNum)
         report.data.Report.groupsOrderFirst.insert(-1, assemPlotImage)
         report.data.Report.componentWellGroups.insert(-1, assemPlotImage)
-        assemPlotName = os.path.abspath(
-            plotting.plotAssemblyTypes(
-                core.parent.blueprints,
-                core.name,
-                assemBatch,
-                plotNum,
-                maxAssems=MAX_ASSEMS_PER_ASSEM_PLOT,
-                showBlockAxMesh=showBlockAxMesh,
-            )
+        assemPlotName = os.path.abspath(f"{core.name}AssemblyTypes{plotNum}.png")
+        plotting.plotAssemblyTypes(
+            core.parent.blueprints,
+            assemPlotName,
+            assemBatch,
+            maxAssems=MAX_ASSEMS_PER_ASSEM_PLOT,
+            showBlockAxMesh=showBlockAxMesh,
         )
         report.setData(
             "Assem Types {}".format(plotNum),
diff --git a/armi/physics/neutronics/crossSectionSettings.py b/armi/physics/neutronics/crossSectionSettings.py
index 7a0a473e..4ab6cb28 100644
--- a/armi/physics/neutronics/crossSectionSettings.py
+++ b/armi/physics/neutronics/crossSectionSettings.py
@@ -35,7 +35,7 @@ from armi.physics.neutronics.crossSectionGroupManager import (
 )
 
 # define conf and schema here since this is closest to where the objects live
-XS_GEOM_TYPES = {"0D", "2D hex", "1D slab", "1D cylinder", None}
+XS_GEOM_TYPES = {"0D", "2D hex", "1D slab", "1D cylinder"}
 
 CONF_GEOM = "geometry"
 CONF_BLOCK_REPRESENTATION = "blockRepresentation"
diff --git a/armi/reactor/converters/uniformMesh.py b/armi/reactor/converters/uniformMesh.py
index f39d6ee5..f27049dc 100644
--- a/armi/reactor/converters/uniformMesh.py
+++ b/armi/reactor/converters/uniformMesh.py
@@ -223,11 +223,11 @@ class UniformMeshGeometryConverter(GeometryConverter):
     def plotConvertedReactor(self):
         assemsToPlot = self.convReactor.core[:12]
         for plotNum, assemBatch in enumerate(iterables.chunk(assemsToPlot, 6), start=1):
+            assemPlotName = f"{self.convReactor.core.name}AssemblyTypes{plotNum}.png"
             plotting.plotAssemblyTypes(
                 self.convReactor.core.parent.blueprints,
-                self.convReactor.core.name,
+                assemPlotName,
                 assemBatch,
-                plotNum,
                 maxAssems=6,
                 showBlockAxMesh=True,
             )
diff --git a/armi/utils/plotting.py b/armi/utils/plotting.py
index a4ae6a7d..f123a955 100644
--- a/armi/utils/plotting.py
+++ b/armi/utils/plotting.py
@@ -632,39 +632,41 @@ class DepthSlider(Slider):
 
 def plotAssemblyTypes(
     blueprints,
-    coreName,
+    fileName=None,
     assems=None,
-    plotNumber=1,
     maxAssems=None,
     showBlockAxMesh=True,
-):
+) -> plt.Figure:
     """
     Generate a plot showing the axial block and enrichment distributions of each assembly type in the core.
 
     Parameters
     ----------
-    bluepprints: Blueprints
+    blueprints: Blueprints
         The blueprints to plot assembly types of.
 
+    fileName : str or None
+        Base for filename to write, or None for just returning the fig
+
     assems: list
         list of assembly objects to be plotted.
 
-    plotNumber: integer
-        number of uniquely identify the assembly plot from others and to prevent plots from being overwritten.
-
     maxAssems: integer
         maximum number of assemblies to plot in the assems list.
 
     showBlockAxMesh: bool
         if true, the axial mesh information will be displayed on the right side of the assembly plot.
+
+    Returns
+    -------
+    fig : plt.Figure
+        The figure object created
     """
 
     if assems is None:
         assems = list(blueprints.assemblies.values())
     if not isinstance(assems, (list, set, tuple)):
         assems = [assems]
-    if not isinstance(plotNumber, int):
-        raise TypeError("Plot number should be an integer")
     if maxAssems is not None and not isinstance(maxAssems, int):
         raise TypeError("Maximum assemblies should be an integer")
 
@@ -718,17 +720,18 @@ def plotAssemblyTypes(
     ax.set_yticks([0.0] + list(set(numpy.cumsum(yBlockHeightDiffs))))
     ax.xaxis.set_visible(False)
 
-    ax.set_title("Assembly Designs for {}".format(coreName), y=1.03)
+    ax.set_title("Assembly Designs", y=1.03)
     ax.set_ylabel("Thermally Expanded Axial Heights (cm)".upper(), labelpad=20)
     ax.set_xlim([0.0, 0.5 + maxAssems * (assemWidth + assemSeparation)])
 
     # Plot and save figure
     ax.plot()
-    figName = coreName + "AssemblyTypes{}.png".format(plotNumber)
-    runLog.debug("Writing assem layout {} in {}".format(figName, os.getcwd()))
-    fig.savefig(figName)
-    plt.close(fig)
-    return figName
+    if fileName:
+        fig.savefig(fileName)
+        runLog.debug("Writing assem layout {} in {}".format(fileName, os.getcwd()))
+        plt.close(fig)
+
+    return fig
 
 
 def _plotBlocksInAssembly(
diff --git a/doc/gallery-src/framework/run_programmaticReactorDefinition.py b/doc/gallery-src/framework/run_programmaticReactorDefinition.py
new file mode 100644
index 00000000..a9b031d0
--- /dev/null
+++ b/doc/gallery-src/framework/run_programmaticReactorDefinition.py
@@ -0,0 +1,222 @@
+"""
+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. 
+In ARMI you can either make the ARMI reactor objects directly,
+or you can define Blueprints objects. The benefit of making Blueprints
+objects is that they can in turn be used to create both ARMI reactor
+objects as well as textual input itself. This is nice when you want to
+have traceable input files associated with a run that was developed
+programmatically (e.g. for parameter sweeps). 
+
+This example shows how to make Blueprints objects programmatically completely
+from scratch.
+
+"""
+import matplotlib.pyplot as plt
+import armi
+
+armi.configure(permissive=True)
+# pylint: disable=wrong-import-position
+from armi.reactor import blueprints
+from armi.settings import caseSettings
+from armi.reactor.blueprints import isotopicOptions
+from armi.reactor.blueprints import assemblyBlueprint
+from armi.reactor.blueprints import blockBlueprint
+from armi.reactor.blueprints import componentBlueprint
+from armi.reactor.blueprints import gridBlueprint
+from armi.reactor.blueprints import reactorBlueprint
+from armi.utils import plotting
+from armi import cases
+
+
+def buildCase():
+    """Build input components and a case."""
+    bp = blueprints.Blueprints()
+    bp.customIsotopics = isotopicOptions.CustomIsotopics()
+    bp.nuclideFlags = isotopicOptions.genDefaultNucFlags()
+
+    components = buildComponents()
+    bp.blockDesigns = buildBlocks(components)
+    bp.assemDesigns = buildAssemblies(bp.blockDesigns)
+    bp.gridDesigns = buildGrids()
+    bp.systemDesigns = buildSystems()
+
+    cs = caseSettings.Settings()
+    cs.path = None
+    cs.caseTitle = "scripted-case"
+    case = cases.Case(cs=cs, bp=bp)
+
+    return case
+
+
+def buildComponents():
+    ISOTHERMAL_TEMPERATURE_IN_C = 450.0
+    fuel = componentBlueprint.ComponentBlueprint()
+    fuel.name = "fuel"
+    fuel.shape = "Circle"
+    fuel.mult = 217
+    fuel.material = "Custom"
+    fuel.Tinput = ISOTHERMAL_TEMPERATURE_IN_C
+    fuel.Thot = ISOTHERMAL_TEMPERATURE_IN_C
+    fuel.id = 0.0
+    fuel.od = 0.4
+
+    clad = componentBlueprint.ComponentBlueprint()
+    clad.name = "clad"
+    clad.mult = "fuel.mult"
+    clad.shape = "Circle"
+    clad.material = "HT9"
+    clad.Tinput = ISOTHERMAL_TEMPERATURE_IN_C
+    clad.Thot = ISOTHERMAL_TEMPERATURE_IN_C
+    clad.id = 0.508
+    clad.od = 0.5842
+
+    gap = componentBlueprint.ComponentBlueprint()
+    gap.name = "gap"
+    gap.shape = "Circle"
+    gap.mult = "fuel.mult"
+    gap.material = "Void"
+    gap.Tinput = ISOTHERMAL_TEMPERATURE_IN_C
+    gap.Thot = ISOTHERMAL_TEMPERATURE_IN_C
+    gap.id = "fuel.od"
+    gap.od = "clad.id"
+
+    wire = componentBlueprint.ComponentBlueprint()
+    wire.name = "wire"
+    wire.mult = "fuel.mult"
+    wire.shape = "Helix"
+    wire.material = "HT9"
+    wire.Tinput = ISOTHERMAL_TEMPERATURE_IN_C
+    wire.Thot = ISOTHERMAL_TEMPERATURE_IN_C
+    wire.id = 0.0
+    wire.od = 0.14224
+    wire.axialPitch = 30.48
+    wire.helixDiameter = 0.72644
+
+    duct = componentBlueprint.ComponentBlueprint()
+    duct.name = "duct"
+    duct.mult = 1
+    duct.shape = "Hexagon"
+    duct.material = "HT9"
+    duct.Tinput = ISOTHERMAL_TEMPERATURE_IN_C
+    duct.Thot = ISOTHERMAL_TEMPERATURE_IN_C
+    duct.ip = 11.0109
+    duct.op = 11.6205
+
+    intercoolant = componentBlueprint.ComponentBlueprint()
+    intercoolant.name = "intercoolant"
+    intercoolant.mult = 1
+    intercoolant.shape = "Hexagon"
+    intercoolant.material = "Sodium"
+    intercoolant.Tinput = ISOTHERMAL_TEMPERATURE_IN_C
+    intercoolant.Thot = ISOTHERMAL_TEMPERATURE_IN_C
+    intercoolant.ip = "duct.op"
+    intercoolant.op = 12.01420
+
+    coolant = componentBlueprint.ComponentBlueprint()
+    coolant.name = "coolant"
+    coolant.shape = "DerivedShape"
+    coolant.material = "Sodium"
+    coolant.Tinput = ISOTHERMAL_TEMPERATURE_IN_C
+    coolant.Thot = ISOTHERMAL_TEMPERATURE_IN_C
+
+    componentBlueprints = {
+        c.name: c for c in [fuel, gap, clad, wire, duct, intercoolant, coolant]
+    }
+
+    return componentBlueprints
+
+
+def buildBlocks(components):
+    """Build block blueprints"""
+    blocks = blockBlueprint.BlockKeyedList()
+    fuel = blockBlueprint.BlockBlueprint()
+    fuel.name = "fuel"
+    for cname, c in components.items():
+        fuel[cname] = c
+    blocks[fuel.name] = fuel
+
+    reflector = blockBlueprint.BlockBlueprint()
+    reflector.name = "reflector"
+    reflector["coolant"] = components["coolant"]
+    reflector["duct"] = components["duct"]
+    blocks[reflector.name] = reflector
+
+    return blocks
+
+
+def buildAssemblies(blockDesigns):
+    """Build assembly blueprints"""
+    fuelBock, reflectorBlock = blockDesigns["fuel"], blockDesigns["reflector"]
+
+    assemblies = assemblyBlueprint.AssemblyKeyedList()
+
+    fuelAssem = assemblyBlueprint.AssemblyBlueprint()
+    fuelAssem.name = "Fuel"
+    fuelAssem.specifier = "IC"
+
+    fuelAssem.blocks = blockBlueprint.BlockList()
+    fuelAssem.blocks.extend(
+        [reflectorBlock, fuelBock, fuelBock, fuelBock, reflectorBlock]
+    )
+    fuelAssem.height = [10, 20, 20, 20, 10]
+    fuelAssem.xsTypes = ["A"] * 5
+    fuelAssem.axialMeshPoints = [1] * 5
+
+    assemblies[fuelAssem.name] = fuelAssem
+
+    reflectorAssem = assemblyBlueprint.AssemblyBlueprint()
+    reflectorAssem.name = "Reflector"
+    reflectorAssem.specifier = "RR"
+    reflectorAssem.blocks = blockBlueprint.BlockList()
+    reflectorAssem.blocks.extend([reflectorBlock] * 5)
+    reflectorAssem.height = [10, 20, 20, 20, 10]
+    reflectorAssem.xsTypes = ["A"] * 5
+    reflectorAssem.axialMeshPoints = [1] * 5
+    assemblies[reflectorAssem.name] = reflectorAssem
+
+    return assemblies
+
+
+def buildGrids():
+    """Build the core map grid"""
+
+    coreGrid = gridBlueprint.GridBlueprint("core")
+    coreGrid.geom = "hex"
+    coreGrid.symmetry = "third periodic"
+    coreGrid.origin = gridBlueprint.Triplet()
+
+    coreGrid.latticeMap = """
+         RR   RR
+           IC   RR
+         IC   IC   RR"""
+
+    grids = gridBlueprint.Grids()
+    grids["core"] = coreGrid
+    return grids
+
+
+def buildSystems():
+    """Build the core system"""
+    systems = reactorBlueprint.Systems()
+    core = reactorBlueprint.SystemBlueprint("core", "core", gridBlueprint.Triplet())
+    systems["core"] = core
+    return systems
+
+
+if __name__ == "__main__":
+    case = buildCase()
+    # build ARMI objects
+    o = case.initializeOperator()
+    fig = plotting.plotAssemblyTypes(
+        case.bp,
+        None,
+        showBlockAxMesh=True,
+    )
+    plt.show()
+
+    # also write input files
+    case.writeInputs()


Test patch:
diff --git a/armi/cli/tests/test_runEntryPoint.py b/armi/cli/tests/test_runEntryPoint.py
new file mode 100644
index 00000000..ad8a7614
--- /dev/null
+++ b/armi/cli/tests/test_runEntryPoint.py
@@ -0,0 +1,22 @@
+"""
+Test for run cli entry point
+"""
+import unittest
+
+import sys
+
+from armi.__main__ import main
+
+
+class TestRun(unittest.TestCase):
+    def test_runCommand(self):
+        """Ensure main entry point with no args completes."""
+        with self.assertRaises(SystemExit) as excinfo:
+            sys.argv = [""]  # have to override the pytest args
+            main()
+        self.assertEqual(excinfo.exception.code, 0)
+
+
+if __name__ == "__main__":
+    # import sys;sys.argv = ['', 'Test.testName']
+    unittest.main()
diff --git a/armi/utils/tests/test_plotting.py b/armi/utils/tests/test_plotting.py
index 5e1252db..435215a1 100644
--- a/armi/utils/tests/test_plotting.py
+++ b/armi/utils/tests/test_plotting.py
@@ -50,10 +50,10 @@ class TestPlotting(unittest.TestCase):
         self._checkExists(fName)
 
     def test_plotAssemblyTypes(self):
-        fName = plotting.plotAssemblyTypes(
-            self.r.core.parent.blueprints, self.r.core.name
+        plotting.plotAssemblyTypes(
+            self.r.core.parent.blueprints, "coreAssemblyTypes1.png"
         )
-        self._checkExists(fName)
+        self._checkExists("coreAssemblyTypes1.png")
 
     def _checkExists(self, fName):
         self.assertTrue(os.path.exists(fName))