# Copyright 2019 TerraPower, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
A collection of miscellaneous functions used by ReportInterface to generate
various reports.
"""
from copy import copy
import collections
import os
import pathlib
import re
import subprocess
import sys
import textwrap
import time
import numpy
from armi import context
from armi import interfaces
from armi import runLog
from armi.bookkeeping import report
from armi.operators import RunTypes
from armi.reactor.components import ComponentType
from armi.reactor.flags import Flags
from armi.utils import getFileSHA1Hash
from armi.utils import iterables
from armi.utils import plotting
from armi.utils import tabulate
from armi.utils import textProcessors
from armi.utils import units
# Set to prevent the image and text from being too small to read.
MAX_ASSEMS_PER_ASSEM_PLOT = 6
# String constants
Operator_CaseTitle = "Case Title:"
Operator_TypeOfRun = "Run Type:"
Operator_NumProcessors = "Number of Processors:"
Operator_WorkingDirectory = "Working Directory:"
Operator_CurrentUser = "Current User:"
Operator_PythonInterperter = "Python Interpreter:"
Operator_ArmiCodebase = "ARMI Location:"
Operator_MasterMachine = "Master Machine:"
Operator_Date = "Date and Time:"
Operator_CaseDescription = "Case Description:"
[docs]def getNodeName():
"""Get the name of this compute node.
First, look in context.py. Then try various Linux tools. Then try Windows commands.
Returns
-------
str
Compute node name.
"""
hostNames = [
context.MPI_NODENAME,
context.MPI_NODENAMES[0],
subprocess.run("hostname", capture_output=True, text=True, shell=True).stdout,
subprocess.run("uname -n", capture_output=True, text=True, shell=True).stdout,
os.environ.get("COMPUTERNAME", context.LOCAL),
]
for nodeName in hostNames:
if nodeName and nodeName != context.LOCAL:
return nodeName
return context.LOCAL
def _getSystemInfoWindows():
"""Get system information, assuming the system is Windows.
Returns
-------
str
Basic system information: OS name, OS version, basic processor information
Examples
--------
Example results:
OS Name: Microsoft Windows 10 Enterprise
OS Version: 10.0.19041 N/A Build 19041
Processor(s): 1 Processor(s) Installed.
[01]: Intel64 Family 6 Model 142 Stepping 12 GenuineIntel ~801 Mhz
"""
cmd = (
'systeminfo | findstr /B /C:"OS Name" /B /C:"OS Version" /B '
'/C:"Processor" && systeminfo | findstr /E /C:"Mhz"'
)
return subprocess.run(cmd, capture_output=True, text=True, shell=True).stdout
def _getSystemInfoMac():
"""Get system information, assuming the system is MacOS.
Returns
-------
str
Basic system information: OS name, OS version, basic processor information
Examples
--------
Example results:
System Software Overview:
System Version: macOS 12.1 (21C52)
Kernel Version: Darwin 21.2.0
...
Hardware Overview:
Model Name: MacBook Pro
...
"""
cmd = "system_profiler SPSoftwareDataType SPHardwareDataType"
return subprocess.check_output(cmd, shell=True).decode("utf-8")
def _getSystemInfoLinux():
"""Get system information, assuming the system is Linux.
This method uses multiple, redundant variations on common Linux command utilities to get the
information necessary. While it is not possible to guarantee what programs or files will be
available on "all Linux operating system", this collection of tools is widely supported and
should provide a reasonably broad-distribution coverage.
Returns
-------
str
Basic system information: OS name, OS version, basic processor information
Examples
--------
Example results:
OS Info: Ubuntu 22.04.3 LTS
Processor(s):
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 126
model name : Intel(R) Core(TM) i5-1035G1 CPU @ 1.00GHz
...
"""
# get OS name / version
linuxOsCommands = [
'cat /etc/os-release | grep "^PRETTY_NAME=" | cut -d = -f 2',
"uname -a",
"lsb_release -d | cut -d : -f 2",
'hostnamectl | grep "Operating System" | cut -d : -f 2',
]
osInfo = ""
for cmd in linuxOsCommands:
osInfo = subprocess.run(
cmd, capture_output=True, text=True, shell=True
).stdout.strip()
if osInfo:
break
if not osInfo:
runLog.warning("Linux OS information not found.")
return ""
# get processor information
linuxProcCommands = ["cat /proc/cpuinfo", "lscpu", "lshw -class CPU"]
procInfo = ""
for cmd in linuxProcCommands:
procInfo = subprocess.run(
cmd, capture_output=True, text=True, shell=True
).stdout
if procInfo:
break
if not procInfo:
runLog.warning("Linux processor information not found.")
return ""
# build output string
out = "OS Info: "
out += osInfo.strip()
out += "\nProcessor(s):\n "
out += procInfo.strip().replace("\n", "\n ")
out += "\n"
return out
[docs]def getSystemInfo():
"""Get system information, assuming the system is Windows or Linux.
Notes
-----
The format of the system information will be different on Windows vs Linux.
Returns
-------
str
Basic system information: OS name, OS version, basic processor information
"""
# Get basic system information (on Windows and Linux)
if "win" in sys.platform:
return _getSystemInfoWindows()
elif "linux" in sys.platform:
return _getSystemInfoLinux()
elif "darwin" in sys.platform:
return _getSystemInfoMac()
else:
runLog.warning(
f"Cannot get system information for {sys.platform} because ARMI only "
+ "supports Linux, Windows, and MacOS."
)
return ""
[docs]def getInterfaceStackSummary(o):
data = []
for ii, i in enumerate(o.interfaces, start=1):
data.append(
(
"{:02d}".format(ii),
i.__class__.__name__.replace("Interface", ""),
i.name,
i.function,
"Yes" if i.enabled() else "No",
"Reversed" if i.reverseAtEOL else "Normal",
"Yes" if i.bolForce() else "No",
)
)
text = tabulate.tabulate(
data,
headers=(
"Index",
"Type",
"Name",
"Function",
"Enabled",
"EOL order",
"BOL forced",
),
tableFmt="armi",
)
text = text
return text
[docs]def writeTightCouplingConvergenceSummary(convergenceSummary):
runLog.info("Tight Coupling Convergence Summary")
runLog.info(
tabulate.tabulate(
convergenceSummary, headers="keys", showIndex=True, tableFmt="armi"
)
)
[docs]def writeAssemblyMassSummary(r):
"""Print out things like Assembly weights to the runLog.
Parameters
----------
r : armi.reactor.reactors.Reactor
"""
massSum = []
for a in r.blueprints.assemblies.values():
mass = 0.0
hmMass = 0.0
fissileMass = 0.0
coolantMass = 0.0 # to calculate wet vs. dry weight.
types = []
for b in a:
# get masses in kg
# skip stationary blocks (grid plate doesn't count)
if b.hasFlags(Flags.GRID_PLATE):
continue
mass += b.getMass() / 1000.0
hmMass += b.getHMMass() / 1000.0
fissileMass += b.getFissileMass() / 1000.0
coolants = b.getComponents(Flags.COOLANT, exact=True) + b.getComponents(
Flags.INTERCOOLANT, exact=True
)
coolantMass += sum(coolant.getMass() for coolant in coolants) / 1000.0
blockType = b.getType()
if blockType not in types:
types.append(blockType)
# if the BOL fuel assem is in the center of the core, its area is 1/3 of the full area b/c it's a sliced assem.
# bug: mass came out way high for a case once. 265 MT vs. 92 MT hm.
# count assemblies
core = r.core
thisTypeList = core.getChildrenOfType(a.getType())
count = 0
for t in thisTypeList:
ring, _pos = t.spatialLocator.getRingPos()
if ring == 1:
# only count center location once.
count += 1
else:
# add 3 if it's 1/3 core, etc.
count += core.powerMultiplier
# Get the dominant materials
pinMaterialKey = "pinMaterial"
pinMaterialObj = a.getDominantMaterial([Flags.FUEL, Flags.CONTROL])
if pinMaterialObj is None:
pinMaterialObj = a.getDominantMaterial()
pinMaterialKey = "dominantMaterial"
pinMaterial = pinMaterialObj.name
else:
pinMaterial = pinMaterialObj.name
struct = a.getDominantMaterial([Flags.CLAD, Flags.DUCT, Flags.SHIELD])
if struct:
structuralMaterial = struct.name
else:
structuralMaterial = "[None]"
cool = a.getDominantMaterial([Flags.COOLANT])
if cool:
coolantMaterial = cool.name
else:
coolantMaterial = "[None]"
# Get pins per assembly
pinsPerAssembly = 0
for candidate in (Flags.FUEL, Flags.CONTROL, Flags.SHIELD):
b = a.getFirstBlock(candidate)
if b:
pinsPerAssembly = b.getNumPins()
if pinsPerAssembly:
break
massSum.append(
{
"type": a.getType(),
"wetMass": mass,
"hmMass": hmMass,
"fissileMass": fissileMass,
"dryMass": mass - coolantMass,
"count": count,
"components": types,
pinMaterialKey: pinMaterial,
"structuralMaterial": structuralMaterial,
"coolantMaterial": coolantMaterial,
"pinsPerAssembly": pinsPerAssembly,
}
)
runLog.important(_makeBOLAssemblyMassSummary(massSum))
runLog.important(_makeTotalAssemblyMassSummary(massSum))
def _makeBOLAssemblyMassSummary(massSum):
str_ = ["--- BOL Assembly Mass Summary (kg) ---"]
dataLabels = ["wetMass", "dryMass", "fissileMass", "hmMass", "count"]
# print header for the printout of each assembly type
str_.append(" " * 12 + "".join(["{0:25s}".format(s["type"]) for s in massSum]))
for val in dataLabels:
line = ""
for s in massSum:
line += "{0:<25.3f}".format(s[val])
str_.append("{0:12s}{1}".format(val, line))
# print blocks in this assembly up to 10
for i in range(10):
line = " " * 12
for s in massSum:
try:
line += "{0:25s}".format(s["components"][i])
except IndexError:
line += " " * 25
if re.search(r"\S", line): # \S matches any non-whitespace character.
str_.append(line)
return "\n".join(str_)
def _makeTotalAssemblyMassSummary(massSum):
massLabels = ["wetMass", "dryMass", "fissileMass", "hmMass"]
totals = {}
count = 0
str_ = ["--Totals--"]
for label in massLabels:
totals[label] = 0.0
for assemSum in massSum:
totals[label] += assemSum[label] * assemSum["count"]
count += assemSum["count"]
str_.append("{0:12s} {1:.2f} MT".format(label, totals[label] / 1000.0))
str_.append("Total assembly count: {0}".format(count // len(massLabels)))
return "\n".join(str_)
[docs]def writeCycleSummary(core):
"""Prints a cycle summary to the runLog.
Parameters
----------
core: armi.reactor.reactors.Core
cs: armi.settings.caseSettings.Settings
"""
# Would io be worth considering for this?
cycle = core.r.p.cycle
str_ = []
runLog.important("Cycle {0} Summary:".format(cycle))
avgBu = core.calcAvgParam("percentBu", typeSpec=Flags.FUEL, generationNum=2)
str_.append("Core Average Burnup: {0}".format(avgBu))
str_.append("Idealized Outlet Temperature {}".format(core.p.THoutletTempIdeal))
str_.append("End of Cycle {0:02d}. Timestamp: {1} ".format(cycle, time.ctime()))
runLog.info("\n".join(str_))
[docs]def setNeutronBalancesReport(core):
"""Determines the various neutron balances over the full core.
Parameters
----------
core : armi.reactor.reactors.Core
"""
if not core.getFirstBlock().p.rateCap:
runLog.warning(
"No rate information (rateCap, rateAbs, etc.) available "
"on the blocks. Skipping balance summary."
)
return
cap = core.calcAvgParam("rateCap", volumeAveraged=False, generationNum=2)
absorb = core.calcAvgParam("rateAbs", volumeAveraged=False, generationNum=2)
fis = core.calcAvgParam("rateFis", volumeAveraged=False, generationNum=2)
n2nProd = core.calcAvgParam("rateProdN2n", volumeAveraged=False, generationNum=2)
fisProd = core.calcAvgParam("rateProdFis", volumeAveraged=False, generationNum=2)
leak = n2nProd + fisProd - absorb
report.setData(
"Fission",
"{0:.5e} ({1:.2%})".format(fisProd, fisProd / (fisProd + n2nProd)),
report.NEUT_PROD,
)
report.setData(
"n, 2n",
"{0:.5e} ({1:.2%})".format(n2nProd, n2nProd / (fisProd + n2nProd)),
report.NEUT_PROD,
)
report.setData(
"Capture",
"{0:.5e} ({1:.2%})".format(cap, cap / (absorb + leak)),
report.NEUT_LOSS,
)
report.setData(
"Fission",
"{0:.5e} ({1:.2%})".format(fis, fis / (absorb + leak)),
report.NEUT_LOSS,
)
report.setData(
"Absorption",
"{0:.5e} ({1:.2%})".format(absorb, absorb / (absorb + leak)),
report.NEUT_LOSS,
)
report.setData(
"Leakage",
"{0:.5e} ({1:.2%})".format(leak, leak / (absorb + leak)),
report.NEUT_LOSS,
)
runLog.info(report.ALL[report.NEUT_PROD])
runLog.info(report.ALL[report.NEUT_LOSS])
[docs]def summarizePinDesign(core):
"""Prints out some information about the pin assembly/duct design.
Handles multiple types of dimensions simplistically by taking the average.
Parameters
----------
core : armi.reactor.reactors.Core
"""
designInfo = collections.defaultdict(list)
try:
for b in core.getBlocks(Flags.FUEL):
fuel = b.getComponent(Flags.FUEL)
duct = b.getComponent(Flags.DUCT)
clad = b.getComponent(Flags.CLAD)
wire = b.getComponent(Flags.WIRE)
designInfo["hot sd"].append(b.getSmearDensity(cold=False))
designInfo["sd"].append(b.getSmearDensity())
designInfo["ductThick"].append(
(duct.getDimension("op") - duct.getDimension("ip")) * 5.0
) # convert to mm and divide by 2
designInfo["cladThick"].append(
(clad.getDimension("od") - clad.getDimension("id")) * 5.0
)
pinOD = clad.getDimension("od") * 10.0
wireOD = wire.getDimension("od") * 10.0
pitch = pinOD + wireOD # pitch has half a wire on each side.
assemPitch = b.getPitch() * 10 # convert cm to mm.
designInfo["pinOD"].append(pinOD)
designInfo["wireOD"].append(wireOD)
designInfo["pin pitch"].append(pitch)
pinToDuctGap = b.getPinToDuctGap()
if pinToDuctGap is not None:
designInfo["pinToDuct"].append(b.getPinToDuctGap() * 10.0)
designInfo["assemPitch"].append(assemPitch)
designInfo["duct gap"].append(assemPitch - duct.getDimension("op") * 10.0)
designInfo["nPins"].append(b.p.nPins)
designInfo["zrFrac"].append(fuel.getMassFrac("ZR"))
# assumption made that all lists contain only numerical data
designInfo = {key: numpy.average(data) for key, data in designInfo.items()}
dimensionless = {"sd", "hot sd", "zrFrac", "nPins"}
for key, average_value in designInfo.items():
dim = "{0:10s}".format(key)
val = "{0:.4f}".format(average_value)
if key not in dimensionless:
val += " mm"
report.setData(dim, val, report.PIN_ASSEM_DESIGN)
a = core.refAssem
report.setData(
"Fuel Height (cm):",
"{0:.2f}".format(a.getHeight(Flags.FUEL)),
report.PIN_ASSEM_DESIGN,
)
report.setData(
"Plenum Height (cm):",
"{0:.2f}".format(a.getHeight(Flags.PLENUM)),
report.PIN_ASSEM_DESIGN,
)
runLog.info(report.ALL[report.PIN_ASSEM_DESIGN])
first_fuel_block = core.getFirstBlock(Flags.FUEL)
runLog.info(
"Design & component information for first fuel block {}".format(
first_fuel_block
)
)
runLog.info(first_fuel_block.setAreaFractionsReport())
for component_ in sorted(first_fuel_block):
runLog.info(component_.setDimensionReport())
except Exception as error:
runLog.warning("Pin summarization failed to work")
runLog.warning(error)
[docs]def summarizePowerPeaking(core):
"""Prints reactor Fz, Fxy, Fq.
Parameters
----------
core : armi.reactor.reactors.Core
"""
# Fz is the axial peaking of the highest power assembly
_maxPow, maxPowBlock = core.getMaxParam("power", returnObj=True, generationNum=2)
maxPowAssem = maxPowBlock.parent
avgPDens = maxPowAssem.calcAvgParam("pdens")
peakPDens = maxPowAssem.getMaxParam("pdens")
if not avgPDens:
# protect against divide-by-zero. Peaking doesnt make sense if there is no
# power.
return
axPeakF = peakPDens / avgPDens
# Fxy is the radial peaking factor, looking at ALL assemblies with axially integrated powers.
power = 0.0
n = 0
for n, a in enumerate(core):
power += a.calcTotalParam("power", typeSpec=Flags.FUEL)
avgPow = power / (n + 1)
radPeakF = maxPowAssem.calcTotalParam("power", typeSpec=Flags.FUEL) / avgPow
runLog.important(
"Power Peaking: Fz= {0:.3f} Fxy= {1:.3f} Fq= {2:.3f}".format(
axPeakF, radPeakF, axPeakF * radPeakF
)
)
[docs]def summarizePower(core):
"""Provide an edit showing where the power is based on assembly types.
Parameters
----------
core : armi.reactor.reactors.Core
"""
sums = collections.defaultdict(lambda: 0.0)
pmult = core.powerMultiplier
for a in core:
sums[a.getType()] += a.calcTotalParam("power") * pmult
# calculate total power
tot = sum(sums.values()) or float("inf")
# NOTE: if tot is 0.0, set to infinity to prevent ZeroDivisionError
runLog.important("Power summary")
for atype, val in sums.items():
runLog.important(
" Power in {0:35s}: {1:0.3E} Watts, {2:0.5f}%".format(
atype, val, val / tot * 100
)
)
[docs]def makeCoreDesignReport(core, cs):
"""Builds report to summarize core design inputs.
Parameters
----------
core: armi.reactor.reactors.Core
cs: armi.settings.caseSettings.Settings
"""
coreDesignTable = report.data.Table(
"SUMMARY OF CORE: {}".format(cs.caseTitle.upper())
)
coreDesignTable.header = ["", "Input Parameter"]
# Change the ordering of the core design table in the report relative to the other data
report.data.Report.groupsOrderFirst.insert(0, coreDesignTable)
report.data.Report.componentWellGroups.insert(0, coreDesignTable)
_setGeneralCoreDesignData(cs, coreDesignTable)
_setGeneralCoreParametersData(core, cs, coreDesignTable)
_setGeneralSimulationData(core, cs, coreDesignTable)
def _setGeneralCoreDesignData(cs, coreDesignTable):
from armi.physics.fuelCycle.settings import CONF_SHUFFLE_LOGIC
from armi.physics.neutronics.settings import CONF_LOADING_FILE
report.setData(
"Case Title", "{}".format(cs.caseTitle), coreDesignTable, report.DESIGN
)
report.setData(
"Run Type", "{}".format(cs["runType"]), coreDesignTable, report.DESIGN
)
report.setData(
"Geometry File", "{}".format(cs["geomFile"]), coreDesignTable, report.DESIGN
)
report.setData(
"Loading File",
"{}".format(cs[CONF_LOADING_FILE]),
coreDesignTable,
report.DESIGN,
)
report.setData(
"Fuel Shuffling Logic File",
"{}".format(cs[CONF_SHUFFLE_LOGIC]),
coreDesignTable,
report.DESIGN,
)
report.setData(
"Reactor State Loading",
"{}".format(cs["loadStyle"]),
coreDesignTable,
report.DESIGN,
)
if cs["loadStyle"] == "fromDB":
report.setData(
"Database File",
"{}".format(cs["reloadDBName"]),
coreDesignTable,
report.DESIGN,
)
report.setData(
"Starting Cycle",
"{}".format(cs["startCycle"]),
coreDesignTable,
report.DESIGN,
)
report.setData(
"Starting Node",
"{}".format(cs["startNode"]),
coreDesignTable,
report.DESIGN,
)
def _setGeneralCoreParametersData(core, cs, coreDesignTable):
blocks = core.getBlocks()
totalMass = sum(b.getMass() for b in blocks)
fissileMass = sum(b.getFissileMass() for b in blocks)
heavyMetalMass = sum(b.getHMMass() for b in blocks)
totalVolume = sum(b.getVolume() for b in blocks)
report.setData(" ", "", coreDesignTable, report.DESIGN)
report.setData(
"Core Power",
"{:.2f} MWth".format(cs["power"] / units.WATTS_PER_MW),
coreDesignTable,
report.DESIGN,
)
report.setData(
"Base Capacity Factor",
"{}".format(cs["availabilityFactor"]),
coreDesignTable,
report.DESIGN,
) # note this doesn't consider availabilityFactors
report.setData(
"Cycle Length",
"{} days".format(cs["cycleLength"]),
coreDesignTable,
report.DESIGN,
) # note this doesn't consider cycleLengths
report.setData(
"Burnup Cycles", "{}".format(cs["nCycles"]), coreDesignTable, report.DESIGN
)
report.setData(
"Burnup Steps per Cycle",
"{}".format(cs["burnSteps"]),
coreDesignTable,
report.DESIGN,
) # note this doesn't consider the detailed cycle input option
corePowerMult = int(core.powerMultiplier)
report.setData(
"Core Total Volume",
"{:.2f} cc".format(totalVolume * corePowerMult),
coreDesignTable,
report.DESIGN,
)
report.setData(
"Core Fissile Mass",
"{:.2f} kg".format(fissileMass / units.G_PER_KG * corePowerMult),
coreDesignTable,
report.DESIGN,
)
report.setData(
"Core Heavy Metal Mass",
"{:.2f} kg".format(heavyMetalMass / units.G_PER_KG * corePowerMult),
coreDesignTable,
report.DESIGN,
)
report.setData(
"Core Total Mass",
"{:.2f} kg".format(totalMass / units.G_PER_KG * corePowerMult),
coreDesignTable,
report.DESIGN,
)
report.setData(
"Number of Assembly Rings",
"{}".format(core.getNumRings()),
coreDesignTable,
report.DESIGN,
)
report.setData(
"Number of Assemblies",
"{}".format(len(core.getAssemblies() * corePowerMult)),
coreDesignTable,
report.DESIGN,
)
report.setData(
"Number of Fuel Assemblies",
"{}".format(len(core.getAssemblies(Flags.FUEL) * corePowerMult)),
coreDesignTable,
report.DESIGN,
)
report.setData(
"Number of Control Assemblies",
"{}".format(len(core.getAssemblies(Flags.CONTROL) * corePowerMult)),
coreDesignTable,
report.DESIGN,
)
report.setData(
"Number of Reflector Assemblies",
"{}".format(len(core.getAssemblies(Flags.REFLECTOR) * corePowerMult)),
coreDesignTable,
report.DESIGN,
)
report.setData(
"Number of Shield Assemblies",
"{}".format(len(core.getAssemblies(Flags.SHIELD) * corePowerMult)),
coreDesignTable,
report.DESIGN,
)
def _setGeneralSimulationData(core, cs, coreDesignTable):
from armi.physics.neutronics.settings import CONF_GEN_XS
from armi.physics.neutronics.settings import CONF_GLOBAL_FLUX_ACTIVE
report.setData(" ", "", coreDesignTable, report.DESIGN)
report.setData(
"Full Core Model", "{}".format(core.isFullCore), coreDesignTable, report.DESIGN
)
report.setData(
"Tight Physics Coupling Enabled",
"{}".format(bool(cs["tightCoupling"])),
coreDesignTable,
report.DESIGN,
)
report.setData(
"Lattice Physics Enabled for",
"{}".format(cs[CONF_GEN_XS]),
coreDesignTable,
report.DESIGN,
)
report.setData(
"Neutronics Enabled for",
"{}".format(cs[CONF_GLOBAL_FLUX_ACTIVE]),
coreDesignTable,
report.DESIGN,
)
[docs]def makeBlockDesignReport(r):
"""Summarize the block designs from the loading file.
Parameters
----------
r : armi.reactor.reactors.Reactor
"""
for bDesign in r.blueprints.blockDesigns:
loadingFileTable = report.data.Table(
"SUMMARY OF BLOCK: {}".format(bDesign.name)
)
loadingFileTable.header = ["", "Input Parameter"]
# Change the ordering of the loading file table in the report relative to the other data
report.data.Report.groupsOrderFirst.append(loadingFileTable)
report.data.Report.componentWellGroups.append(loadingFileTable)
report.setData(
"Number of Components", [len(bDesign)], loadingFileTable, report.DESIGN
)
for i, cDesign in enumerate(bDesign):
cType = cDesign.name
componentSplitter = (i + 1) * " " + "\n"
report.setData(componentSplitter, [""], loadingFileTable, report.DESIGN)
dimensions = _getComponentInputDimensions(cDesign)
for label, values in dimensions.items():
value, unit = values
report.setData(
"{} {}".format(cType, label),
"{} {}".format(value, unit),
loadingFileTable,
report.DESIGN,
)
def _getComponentInputDimensions(cDesign):
"""Get the input dimensions of a component and place them in a dictionary with labels and units."""
dims = collections.OrderedDict()
dims["Shape"] = (cDesign.shape, "")
dims["Material"] = (cDesign.material, "")
dims["Cold Temperature"] = (cDesign.Tinput, "C")
dims["Hot Temperature"] = (cDesign.Thot, "C")
if cDesign.isotopics is not None:
dims["Custom Isotopics"] = (cDesign.isotopics, "")
for dimName in ComponentType.TYPES[cDesign.shape.lower()].DIMENSION_NAMES:
value = getattr(cDesign, dimName)
if value is not None:
# if not default, add it to the report
dims[dimName] = (getattr(cDesign, dimName).value, "cm")
return dims
[docs]def makeCoreAndAssemblyMaps(r, cs, generateFullCoreMap=False, showBlockAxMesh=True):
"""Create core and assembly design plots.
Parameters
----------
r : armi.reactor.reactors.Reactor
cs: armi.settings.caseSettings.Settings
generateFullCoreMap : bool, default False
showBlockAxMesh : bool, default True
"""
assemsInCore = list(r.blueprints.assemblies.values())
core = r.core
for plotNum, assemBatch in enumerate(
iterables.chunk(assemsInCore, MAX_ASSEMS_PER_ASSEM_PLOT), start=1
):
assemPlotImage = copy(report.ASSEM_TYPES)
assemPlotImage.title = assemPlotImage.title + " ({})".format(plotNum)
report.data.Report.groupsOrderFirst.insert(-1, assemPlotImage)
report.data.Report.componentWellGroups.insert(-1, assemPlotImage)
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,
)
# Create radial core map
if generateFullCoreMap:
core.growToFullCore(cs)
counts = {
assemDesign.name: len(core.getChildrenOfType(assemDesign.name))
for assemDesign in r.blueprints.assemDesigns
}
# assemDesigns.keys is ordered based on input, assemOrder only contains types that are in the core
assemOrder = [
aType for aType in r.blueprints.assemDesigns.keys() if counts[aType] > 0
]
data = [assemOrder.index(a.p.type) for a in core]
labels = [r.blueprints.assemDesigns[a.p.type].specifier for a in core]
legendMap = [
(
ai,
assemDesign.specifier,
"{} ({})".format(assemDesign.name, counts[assemDesign.name]),
)
for ai, assemDesign in enumerate(r.blueprints.assemDesigns)
if counts[assemDesign.name] > 0
]
fName = "".join([cs.caseTitle, "RadialCoreMap.", cs["outputFileExtension"]])
plotting.plotFaceMap(
core,
title="{} Radial Core Map".format(cs.caseTitle),
fName=fName,
cmapName="RdYlBu",
data=data,
labels=labels,
legendMap=legendMap,
axisEqual=True,
bare=True,
titleSize=10,
fontSize=8,
)
report.setData(
"Radial Core Map", os.path.abspath(fName), report.FACE_MAP, report.DESIGN
)
COMPONENT_INFO = "Component Information"