# 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.
"""
Definitions of top-level reactor arrangements like the Core (default), SFP, etc.
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']``
setting. Until the geom file-based input is completely removed, this system will attempt to migrate
the core layout from geom files. When geom files are used, explicitly specifying a ``core`` system
will result in an error.
System Blueprints are a big step in the right direction to generalize user input, but was still
mostly adapted from the old Core layout input. As such, they still only really support Core-like
systems. Future work should generalize the concept of "system" to more varied scenarios.
See Also
--------
armi.reactor.blueprints.gridBlueprints : Method for storing system assembly layouts.
armi.reactor.systemLayoutInput.SystemLayoutInput : Deprecated method for reading the individual
face-map xml files.
"""
import yamlize
from armi import context, getPluginManagerOrFail, runLog
from armi.reactor import geometry, grids
from armi.reactor.blueprints.gridBlueprint import Triplet
from armi.utils import tabulate
[docs]
class SystemBlueprint(yamlize.Object):
"""
The reactor-level structure input blueprint.
.. impl:: Build core and spent fuel pool from blueprints
:id: I_ARMI_BP_SYSTEMS
:implements: R_ARMI_BP_SYSTEMS, R_ARMI_BP_CORE
This class creates a yaml interface for the user to define systems with
grids, such as cores or spent fuel pools, each having their own name,
type, grid, and position in space. It is incorporated into the "systems"
section of a blueprints file by being included as key-value pairs within
the :py:class:`~armi.reactor.blueprints.reactorBlueprint.Systems` class,
which is in turn included into the overall blueprints within
:py:class:`~armi.reactor.blueprints.Blueprints`.
This class includes a
:py:meth:`~armi.reactor.blueprints.reactorBlueprint.SystemBlueprint.construct` method, which
is typically called from within :py:func:`~armi.reactor.reactors.factory` during the
initialization of the reactor object to instantiate the core and/or spent fuel pool objects.
During that process, a spatial grid is constructed based on the grid blueprints specified in
the "grids" section of the blueprints (see :need:`I_ARMI_BP_GRID`) and the assemblies needed
to fill the lattice are built from blueprints using
:py:meth:`~armi.reactor.blueprints.Blueprints.constructAssem`.
Notes
-----
We use string keys to link grids to objects that use them. This differs from how blocks/
assembies are specified, which use YAML anchors. YAML anchors have proven to be problematic and
difficult to work with.
"""
name = yamlize.Attribute(key="name", type=str)
typ = yamlize.Attribute(key="type", type=str, default="core")
gridName = yamlize.Attribute(key="grid name", type=str)
origin = yamlize.Attribute(key="origin", type=Triplet, default=None)
def __init__(self, name=None, gridName=None, origin=None):
"""
A Reactor-level structure like a core, or ex-core like SFP.
Notes
-----
yamlize does not call an __init__ method, instead it uses __new__ and setattr this is only
needed for when you want to make this object from a non-YAML source.
"""
self.name = name
self.gridName = gridName
self.origin = origin
@staticmethod
def _resolveSystemType(typ: str):
"""Loop over all plugins that could be attached and determine if any tell us how to build a
specific systems attribute.
"""
manager = getPluginManagerOrFail()
# Only need this to handle the case we don't find the system we expect
seen = set()
for options in manager.hook.defineSystemBuilders():
for key, builder in options.items():
# Take the first match we find. This would allow other plugins to define a new core
# builder before finding those defined by the ReactorPlugin
if key == typ:
return builder
seen.add(key)
raise ValueError(
"Could not determine an appropriate class for handling a "
"system of type `{}`. Supported types are {}.".format(typ, sorted(seen))
)
[docs]
def construct(self, cs, bp, reactor, geom=None, loadComps=True):
"""Build a core or ex-core grid and fill it with children.
Parameters
----------
cs : :py:class:`Settings <armi.settings.Settings>`
armi settings to apply
bp : :py:class:`Reactor <armi.reactor.blueprints.Blueprints>`
armi blueprints to apply
reactor : :py:class:`Reactor <armi.reactor.reactors.Reactor>`
reactor to fill
geom : optional
loadComps : bool, optional
whether to fill reactor with assemblies, as defined in blueprints, or not. Is False in
:py:class:`UniformMeshGeometryConverter <armi.reactor.converters.uniformMesh.UniformMeshGeometryConverter>`
within the initNewReactor() method.
Returns
-------
Composite
A Composite object with a grid, like a Spent Fuel Pool or other ex-core structure.
Raises
------
ValueError
input error, no grid design provided
ValueError
objects were added to non-existant grid locations
"""
runLog.info(f"Constructing the `{self.name}`")
if geom is not None and self.name == "core":
gridDesign = geom.toGridBlueprints("core")[0]
elif geom is not None and self.name == "Spent Fuel Pool":
gridDesign = geom.toGridBlueprints("Spent Fuel Pool")[0]
else:
if not bp.gridDesigns:
raise ValueError(
"The input must define grids to construct a reactor, but does not. Update input."
)
gridDesign = bp.gridDesigns.get(self.gridName, None)
system = self._resolveSystemType(self.typ)(self.name)
# Some systems may not require a prescribed grid design. Only use one if provided
if gridDesign is not None:
spatialGrid = gridDesign.construct()
system.spatialGrid = spatialGrid
system.spatialGrid.armiObject = system
reactor.add(system) # ensure the reactor is the parent
spatialLocator = grids.CoordinateLocation(
self.origin.x, self.origin.y, self.origin.z, None
)
system.spatialLocator = spatialLocator
if context.MPI_RANK != 0:
# Non-primary nodes get the reactor via DistributeState.
return None
system = self._constructComposites(cs, bp, loadComps, system, gridDesign)
return system
def _constructComposites(self, cs, bp, loadComps, system, gridDesign):
"""Fill a grid with composities, if there are any to fill.
Parameters
----------
cs : Settings object.
armi settings to apply
bp : Blueprints object.
armi blueprints to apply
loadComps : bool
whether to fill reactor with composities, as defined in blueprints, or not
system : Composite
The composite we are building.
gridDesign : GridBlueprint
The defintion of the grid on the object.
Returns
-------
Composite
A Composite object with a grid, like a Spent Fuel Pool or other ex-core structure.
"""
from armi.reactor.reactors import Core # avoid circular import
if loadComps and gridDesign is not None:
self._loadComposites(cs, system, gridDesign.gridContents, bp)
if isinstance(system, Core):
summarizeMaterialData(system)
self._modifyGeometry(system, gridDesign)
system.processLoading(cs)
return system
def _loadComposites(self, cs, container, gridContents, bp):
runLog.header(f"=========== Adding Composites to {container} ===========")
badLocations = set()
for locationInfo, aTypeID in gridContents.items():
# TODO: We should allow for non-Assembly objects/geometries to be loaded into the grid.
# For instance, an ex-core grid may define ducts, not just Assemblies.
newAssembly = bp.constructAssem(cs, specifier=aTypeID)
i, j = locationInfo
loc = container.spatialGrid[i, j, 0]
try:
container.add(newAssembly, loc)
except LookupError:
badLocations.add(loc)
if badLocations:
raise ValueError(
f"Attempted to add objects to non-existant locations on the grid: {badLocations}."
)
def _modifyGeometry(self, container, gridDesign):
"""Perform post-load geometry conversions like full core, edge assems."""
# all cases should have no edge assemblies. They are added ephemerally when needed
from armi.reactor.converters import (
geometryConverters,
)
runLog.header("=========== Applying Geometry Modifications ===========")
converter = geometryConverters.EdgeAssemblyChanger()
converter.scaleParamsRelatedToSymmetry(container)
converter.removeEdgeAssemblies(container)
# now update the spatial grid dimensions based on the populated children
# (unless specified on input)
if not gridDesign.latticeDimensions:
runLog.info(
f"Updating spatial grid pitch data for {container.geomType} geometry"
)
if container.geomType == geometry.GeomType.HEX:
container.spatialGrid.changePitch(container[0][0].getPitch())
elif container.geomType == geometry.GeomType.CARTESIAN:
xw, yw = container[0][0].getPitch()
container.spatialGrid.changePitch(xw, yw)
[docs]
class Systems(yamlize.KeyedList):
item_type = SystemBlueprint
key_attr = SystemBlueprint.name
[docs]
def summarizeMaterialData(container):
"""
Create a summary of the material objects and source data for a reactor container.
Parameters
----------
container : Core object
Any Core object with Blocks and Components defined.
"""
runLog.header(
f"=========== Summarizing Source of Material Data for {container} ==========="
)
materialNames = set()
materialData = []
for c in container.iterComponents():
if c.material.name in materialNames:
continue
materialData.append((c.material.name, c.material.DATA_SOURCE))
materialNames.add(c.material.name)
materialData = sorted(materialData)
runLog.info(
tabulate.tabulate(
data=materialData,
headers=["Material Name", "Source Location"],
tableFmt="armi",
)
)
return materialData