# 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.
"""This module defines the ARMI input for a block definition, and code for constructing an ARMI ``Block``."""
import collections
from inspect import signature
from typing import Iterable, Set, Iterator
import yamlize
from armi import getPluginManagerOrFail, runLog
from armi.materials.material import Material
from armi.reactor import blocks
from armi.reactor.composites import Composite
from armi.reactor import parameters
from armi.reactor.flags import Flags
from armi.reactor.blueprints import componentBlueprint
from armi.reactor.components.component import Component
from armi.reactor.converters import blockConverters
from armi.settings.fwSettings import globalSettings
def _configureGeomOptions():
blockTypes = dict()
pm = getPluginManagerOrFail()
for pluginBlockTypes in pm.hook.defineBlockTypes():
for compType, blockType in pluginBlockTypes:
blockTypes[compType] = blockType
return blockTypes
[docs]class BlockBlueprint(yamlize.KeyedList):
"""Input definition for Block.
.. impl:: Create a Block from blueprint file.
:id: I_ARMI_BP_BLOCK
:implements: R_ARMI_BP_BLOCK
Defines a yaml construct that allows the user to specify attributes of a block from within
their blueprints file, including a name, flags, a radial grid to specify locations of pins,
and the name of a component which drives the axial expansion of the block (see
:py:mod:`~armi.reactor.converters.axialExpansionChanger`).
In addition, the user may specify key-value pairs to specify the components contained within
the block, where the keys are component names and the values are component blueprints (see
:py:class:`~armi.reactor.blueprints.ComponentBlueprint.ComponentBlueprint`).
Relies on the underlying infrastructure from the ``yamlize`` package for reading from text
files, serialization, and internal storage of the data.
Is implemented into a blueprints file by being imported and used as an attribute within the
larger :py:class:`~armi.reactor.blueprints.Blueprints` class.
Includes a ``construct`` method, which instantiates an instance of
:py:class:`~armi.reactor.blocks.Block` with the characteristics as specified in the
blueprints.
"""
item_type = componentBlueprint.ComponentBlueprint
key_attr = componentBlueprint.ComponentBlueprint.name
name = yamlize.Attribute(key="name", type=str)
gridName = yamlize.Attribute(key="grid name", type=str, default=None)
flags = yamlize.Attribute(type=str, default=None)
axialExpTargetComponent = yamlize.Attribute(
key="axial expansion target component", type=str, default=None
)
_geomOptions = _configureGeomOptions()
def _getBlockClass(self, outerComponent):
"""
Get the ARMI ``Block`` class for the specified outerComponent.
Parameters
----------
outerComponent : Component
Largest component in block.
"""
for compCls, blockCls in self._geomOptions.items():
if isinstance(outerComponent, compCls):
return blockCls
raise ValueError(
"Block input for {} has outer component {} which is "
" not a supported Block geometry subclass. Update geometry."
"".format(self.name, outerComponent)
)
[docs] def construct(
self, cs, blueprint, axialIndex, axialMeshPoints, height, xsType, materialInput
):
"""
Construct an ARMI ``Block`` to be placed in an ``Assembly``.
Parameters
----------
cs : Settings
Settings object for the appropriate simulation.
blueprint : Blueprints
Blueprints object containing various detailed information, such as nuclides to model
axialIndex : int
The Axial index this block exists within the parent assembly
axialMeshPoints : int
number of mesh points for use in the neutronics kernel
height : float
initial height of the block
xsType : str
String representing the xsType of this block.
materialInput : dict
Double-layered dict.
Top layer groups the by-block material modifications under the `byBlock` key
and the by-component material modifications under the component's name.
The inner dict under each key contains material modification names and values.
"""
runLog.debug("Constructing block {}".format(self.name))
components = collections.OrderedDict()
# build grid before components so you can load
# the components into the grid.
gridDesign = self._getGridDesign(blueprint)
if gridDesign:
spatialGrid = gridDesign.construct()
else:
spatialGrid = None
self._checkByComponentMaterialInput(materialInput)
for componentDesign in self:
filteredMaterialInput, byComponentMatModKeys = self._filterMaterialInput(
materialInput, componentDesign
)
c = componentDesign.construct(blueprint, filteredMaterialInput)
components[c.name] = c
# check that the mat mods for this component are valid options
# this will only examine by-component mods, block mods are done later
if isinstance(c, Component):
# there are other things like composite groups that don't get
# material modifications -- skip those
validMatModOptions = self._getMaterialModsFromBlockChildren(c)
for key in byComponentMatModKeys:
if key not in validMatModOptions:
raise ValueError(
f"{c} in block {self.name} has invalid material modification: {key}"
)
if spatialGrid:
componentLocators = gridDesign.getMultiLocator(
spatialGrid, componentDesign.latticeIDs
)
if componentLocators:
# this component is defined in the block grid
# We can infer the multiplicity from the grid.
# Otherwise it's a component that is in a block
# with grids but that's not in the grid itself.
c.spatialLocator = componentLocators
mult = c.getDimension("mult")
if mult and mult != 1.0 and mult != len(c.spatialLocator):
raise ValueError(
f"Conflicting ``mult`` input ({mult}) and number of "
f"lattice positions ({len(c.spatialLocator)}) for {c}. "
"Recommend leaving off ``mult`` input when using grids."
)
elif not mult or mult == 1.0:
# learn mult from grid definition
c.setDimension("mult", len(c.spatialLocator))
# check that the block level mat mods use valid options in the same way
# as we did for the by-component mods above
validMatModOptions = self._getBlockwiseMaterialModifierOptions(
components.values()
)
if "byBlock" in materialInput:
for key in materialInput["byBlock"]:
if key not in validMatModOptions:
raise ValueError(
f"Block {self.name} has invalid material modification key: {key}"
)
# Resolve linked dims after all components in the block are created
for c in components.values():
c.resolveLinkedDims(components)
boundingComp = sorted(components.values())[-1]
# give a temporary name (will be updated by b.makeName as real blocks populate systems)
b = self._getBlockClass(boundingComp)(name=f"block-bol-{axialIndex:03d}")
for paramDef in b.p.paramDefs.inCategory(
parameters.Category.assignInBlueprints
):
val = getattr(self, paramDef.name)
if val is not None:
b.p[paramDef.name] = val
flags = None
if self.flags is not None:
flags = Flags.fromString(self.flags)
b.setType(self.name, flags)
if self.axialExpTargetComponent is not None:
try:
b.setAxialExpTargetComp(components[self.axialExpTargetComponent])
except KeyError as noMatchingComponent:
raise RuntimeError(
f"Block {b} --> axial expansion target component {self.axialExpTargetComponent} "
"specified in the blueprints does not match any component names. "
"Revise axial expansion target component in blueprints "
"to match the name of a component and retry."
) from noMatchingComponent
for c in components.values():
b.add(c)
b.p.nPins = b.getNumPins()
b.p.axMesh = _setBlueprintNumberOfAxialMeshes(
axialMeshPoints, cs["axialMeshRefinementFactor"]
)
b.p.height = height
b.p.heightBOL = height # for fuel performance
b.p.xsType = xsType
b.setBuLimitInfo()
b = self._mergeComponents(b)
b.verifyBlockDims()
b.spatialGrid = spatialGrid
if b.spatialGrid is None and cs[globalSettings.CONF_BLOCK_AUTO_GRID]:
try:
b.autoCreateSpatialGrids()
except (ValueError, NotImplementedError) as e:
runLog.warning(str(e), single=True)
return b
def _getBlockwiseMaterialModifierOptions(
self, children: Iterable[Composite]
) -> Set[str]:
"""Collect all the material modifiers that exist on a block."""
validMatModOptions = set()
for c in children:
perChildModifiers = self._getMaterialModsFromBlockChildren(c)
validMatModOptions.update(perChildModifiers)
return validMatModOptions
def _getMaterialModsFromBlockChildren(self, c: Composite) -> Set[str]:
"""Collect all the material modifiers from a child of a block."""
perChildModifiers = set()
for material in self._getMaterialsInComposite(c):
for materialParentClass in material.__class__.__mro__:
# we must loop over parents as well, since applyInputParams
# could call to Parent.applyInputParams()
if issubclass(materialParentClass, Material):
perChildModifiers.update(
signature(
materialParentClass.applyInputParams
).parameters.keys()
)
# self is a parameter to methods, so it gets picked up here
# but that's obviously not a real material modifier
perChildModifiers.discard("self")
return perChildModifiers
def _getMaterialsInComposite(self, child: Composite) -> Iterator[Material]:
"""Collect all the materials in a composite."""
# Leaf node, no need to traverse further down
if isinstance(child, Component):
yield child.material
return
# Don't apply modifications to other things that could reside
# in a block e.g., component groups
def _checkByComponentMaterialInput(self, materialInput):
for component in materialInput:
if component != "byBlock":
if component not in [componentDesign.name for componentDesign in self]:
if materialInput[component]: # ensure it is not empty
raise ValueError(
f"The component '{component}' used to specify a by-component"
f" material modification is not in block '{self.name}'."
)
@staticmethod
def _filterMaterialInput(materialInput, componentDesign):
"""
Get the by-block material modifications and those specifically for this
component.
If a material modification is specified both by-block and by-component
for a given component, the by-component value will be used.
"""
filteredMaterialInput = {}
byComponentMatModKeys = set()
# first add the by-block modifications without question
if "byBlock" in materialInput:
for modName, modVal in materialInput["byBlock"].items():
filteredMaterialInput[modName] = modVal
# then get the by-component modifications as appropriate
for component, mod in materialInput.items():
if component == "byBlock":
pass # we already added these
else:
# these are by-component mods, first test if the component matches
# before adding. if component matches, add the modifications,
# overwriting any by-block modifications of the same type
if component == componentDesign.name:
for modName, modVal in mod.items():
byComponentMatModKeys.add(modName)
filteredMaterialInput[modName] = modVal
return filteredMaterialInput, byComponentMatModKeys
def _getGridDesign(self, blueprint):
"""
Get the appropriate grid design.
This happens when a lattice input is provided on the block. Otherwise all
components are ambiguously defined in the block.
"""
if self.gridName:
if self.gridName not in blueprint.gridDesigns:
raise KeyError(
f"Lattice {self.gridName} defined on {self} is not "
"defined in the blueprints `lattices` section."
)
return blueprint.gridDesigns[self.gridName]
return None
@staticmethod
def _mergeComponents(b):
solventNamesToMergeInto = set(
c.p.mergeWith for c in b.iterComponents() if c.p.mergeWith
)
if solventNamesToMergeInto:
runLog.warning(
"Component(s) {} in block {} has merged components inside it. The merge was valid at hot "
"temperature, but the merged component only has the basic thermal expansion factors "
"of the component(s) merged into. Expansion properties or dimensions of non hot "
"temperature may not be representative of how the original components would have acted had "
"they not been merged. It is recommended that merging happen right before "
"a physics calculation using a block converter to avoid this."
"".format(solventNamesToMergeInto, b.name),
single=True,
)
for solventName in solventNamesToMergeInto:
soluteNames = []
for c in b:
if c.p.mergeWith == solventName:
soluteNames.append(c.name)
converter = blockConverters.MultipleComponentMerger(
b, soluteNames, solventName
)
b = converter.convert()
return b
for paramDef in parameters.forType(blocks.Block).inCategory(
parameters.Category.assignInBlueprints
):
setattr(
BlockBlueprint,
paramDef.name,
yamlize.Attribute(name=paramDef.name, default=None),
)
def _setBlueprintNumberOfAxialMeshes(meshPoints, factor):
"""Set the blueprint number of axial mesh based on the axial mesh refinement factor."""
if factor <= 0:
raise ValueError(
"A positive axial mesh refinement factor "
f"must be provided. A value of {factor} is invalid."
)
if factor != 1:
runLog.important(
"An axial mesh refinement factor of {} is applied "
"to blueprint based on setting specification.".format(factor),
single=True,
)
return int(meshPoints) * factor
[docs]class BlockKeyedList(yamlize.KeyedList):
"""
An OrderedDict of BlockBlueprints keyed on the name. Utilizes yamlize for serialization to and from YAML.
This is used within the ``blocks:`` main entry of the blueprints.
"""
item_type = BlockBlueprint
key_attr = BlockBlueprint.name
[docs]class BlockList(yamlize.Sequence):
"""
A list of BlockBlueprints keyed on the name. Utilizes yamlize for serialization to and from YAML.
This is used to define the ``blocks:`` attribute of the assembly definitions.
"""
item_type = BlockBlueprint