Skip to content

Commit

Permalink
Add basic support of component groups.
Browse files Browse the repository at this point in the history
This allows the user to input component groups intended to mix different
free components in different fractions. It is a follow-up to terrapower#505 as
part of the larger terrapower#504 task related to improving input flexibility.

This implements the addition of groups of components to blocks, which
can be used to create more complex and flexible models.

The code changes solved a few residual issues where iterating over the
children of a block were assuming all children would be Components. Now
they can be Composites with multiple Components or Components.

Some upgrades to DerivedShapes were necessary so that they could work
with volumetric components in addition to pure 2D ones.

These are additional steps toward terrapower#504, but true practical usage of
these groups is not quite done yet. More followups are needed.

Added a new test covering negative volume in derived shapes
  • Loading branch information
ntouran authored and scottyak committed Oct 27, 2022
1 parent ac57b32 commit 64f0bac
Show file tree
Hide file tree
Showing 16 changed files with 306 additions and 74 deletions.
4 changes: 2 additions & 2 deletions armi/bookkeeping/db/database3.py
Original file line number Diff line number Diff line change
Expand Up @@ -1149,7 +1149,7 @@ def _compose(self, comps, cs, parent=None):

# Need to keep a collection of Component instances for linked dimension
# resolution, before they can be add()ed to their parents. Not just filtering
# out of `children`, since _resolveLinkedDims() needs a dict
# out of `children`, since resolveLinkedDims() needs a dict
childComponents = collections.OrderedDict()
children = []

Expand All @@ -1160,7 +1160,7 @@ def _compose(self, comps, cs, parent=None):
childComponents[child.name] = child

for _childName, child in childComponents.items():
child._resolveLinkedDims(childComponents)
child.resolveLinkedDims(childComponents)

for child in children:
comp.add(child)
Expand Down
16 changes: 10 additions & 6 deletions armi/reactor/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -834,7 +834,7 @@ def completeInitialLoading(self, bolBlock=None):
self.p.enrichmentBOL = self.getFissileMassEnrich()
massHmBOL = 0.0
sf = self.getSymmetryFactor()
for child in self:
for child in self.iterComponents():
child.p.massHmBOL = child.getHMMass() * sf # scale to full block
massHmBOL += child.p.massHmBOL
self.p.massHmBOL = massHmBOL
Expand Down Expand Up @@ -910,10 +910,14 @@ def add(self, c):

self.derivedMustUpdate = True
self.clearCache()
mult = int(c.getDimension("mult"))
if self.p.percentBuByPin is None or len(self.p.percentBuByPin) < mult:
# this may be a little wasteful, but we can fix it later...
self.p.percentBuByPin = [0.0] * mult
try:
mult = int(c.getDimension("mult"))
if self.p.percentBuByPin is None or len(self.p.percentBuByPin) < mult:
# this may be a little wasteful, but we can fix it later...
self.p.percentBuByPin = [0.0] * mult
except AttributeError:
# maybe adding a Composite of components rather than a single
pass
self._updatePitchComponent(c)

def removeAll(self, recomputeAreaFractions=True):
Expand Down Expand Up @@ -1972,7 +1976,7 @@ def autoCreateSpatialGrids(self):
"""

# Check multiplicities...
mults = {c.getDimension("mult") for c in self}
mults = {c.getDimension("mult") for c in self.iterComponents()}

if len(mults) != 2 or 1 not in mults:
raise ValueError(
Expand Down
4 changes: 4 additions & 0 deletions armi/reactor/blueprints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
from armi.reactor.blueprints.assemblyBlueprint import AssemblyKeyedList
from armi.reactor.blueprints.blockBlueprint import BlockKeyedList
from armi.reactor.blueprints.componentBlueprint import ComponentKeyedList
from armi.reactor.blueprints.componentBlueprint import ComponentGroups
from armi.reactor.blueprints import isotopicOptions
from armi.reactor.blueprints.gridBlueprint import Grids, Triplet

Expand Down Expand Up @@ -189,6 +190,9 @@ class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector):
componentDesigns = yamlize.Attribute(
key="components", type=ComponentKeyedList, default=None
)
componentGroups = yamlize.Attribute(
key="component groups", type=ComponentGroups, default=None
)

# These are used to set up new attributes that come from plugins. Defining its
# initial state here to make pylint happy
Expand Down
7 changes: 5 additions & 2 deletions armi/reactor/blueprints/blockBlueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,9 @@ def construct(
# learn mult from grid definition
c.setDimension("mult", len(c.spatialLocator))

# Resolve linked dims after all components in the block are created
for c in components.values():
c._resolveLinkedDims(components)
c.resolveLinkedDims(components)

boundingComp = sorted(components.values())[-1]
# give a temporary name (will be updated by b.makeName as real blocks populate systems)
Expand Down Expand Up @@ -237,7 +238,9 @@ def _getGridDesign(self, blueprint):

@staticmethod
def _mergeComponents(b):
solventNamesToMergeInto = set(c.p.mergeWith for c in b if c.p.mergeWith)
solventNamesToMergeInto = set(
c.p.mergeWith for c in b.iterComponents() if c.p.mergeWith
)

if solventNamesToMergeInto:
runLog.warning(
Expand Down
129 changes: 99 additions & 30 deletions armi/reactor/blueprints/componentBlueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,18 @@
Special logic is required for handling component links.
"""
import six
import yamlize

from armi import runLog
from armi import materials
from armi.reactor import components
from armi.reactor import composites
from armi.reactor.flags import Flags
from armi.utils import densityTools
from armi.nucDirectory import nuclideBases

COMPONENT_GROUP_SHAPE = "group"


class ComponentDimension(yamlize.Object):
"""
Expand All @@ -38,7 +40,7 @@ class ComponentDimension(yamlize.Object):
def __init__(self, value):
# note: yamlizable does not call an __init__ method, instead it uses __new__ and setattr
self.value = value
if isinstance(value, six.string_types):
if isinstance(value, str):
if not components.COMPONENT_LINK_REGEX.search(value):
raise ValueError(
"Bad component link `{}`, must be in form `name.dimension`".format(
Expand Down Expand Up @@ -124,20 +126,30 @@ class ComponentBlueprint(yamlize.Object):

@name.validator
def name(self, name): # pylint: disable=no-self-use; reason=yamlize requirement
if name in {"cladding"}:
raise ValueError("Cannot set ComponentBlueprint.name to {}".format(name))
"""Validate component names."""
if name == "cladding":
# many users were mixing cladding and clad and it caused issues downstream
# where physics plugins checked for clad.
raise ValueError(
f"Cannot set ComponentBlueprint.name to {name}. Prefer 'clad'."
)

shape = yamlize.Attribute(type=str)

@shape.validator
def shape(self, shape): # pylint: disable=no-self-use; reason=yamlize requirement
normalizedShape = shape.strip().lower()
if normalizedShape not in components.ComponentType.TYPES:
raise ValueError("Cannot set ComponentBlueprint.shape to {}".format(shape))
if (
normalizedShape not in components.ComponentType.TYPES
and normalizedShape != COMPONENT_GROUP_SHAPE
):
raise ValueError(
f"Cannot set ComponentBlueprint.shape to unknown shape: {shape}"
)

material = yamlize.Attribute(type=str)
Tinput = yamlize.Attribute(type=float)
Thot = yamlize.Attribute(type=float)
material = yamlize.Attribute(type=str, default=None)
Tinput = yamlize.Attribute(type=float, default=None)
Thot = yamlize.Attribute(type=float, default=None)
isotopics = yamlize.Attribute(type=str, default=None)
latticeIDs = yamlize.Attribute(type=list, default=None)
origin = yamlize.Attribute(type=list, default=None)
Expand All @@ -146,28 +158,27 @@ def shape(self, shape): # pylint: disable=no-self-use; reason=yamlize requireme
area = yamlize.Attribute(type=float, default=None)

def construct(self, blueprint, matMods):
"""Construct a component"""
"""Construct a component or group"""
runLog.debug("Constructing component {}".format(self.name))
kwargs = self._conformKwargs(blueprint, matMods)
component = components.factory(self.shape.strip().lower(), [], kwargs)
shape = self.shape.lower().strip()
if shape == COMPONENT_GROUP_SHAPE:
group = blueprint.componentGroups[self.name]
constructedObject = composites.Composite(self.name)
for groupedComponent in group:
componentDesign = blueprint.componentDesigns[groupedComponent.name]
component = componentDesign.construct(blueprint, matMods=dict())
# override free component multiplicity if it's set based on the group definition
component.setDimension("mult", groupedComponent.mult)
_setComponentFlags(component, self.flags, blueprint)
_insertDepletableNuclideKeys(component, blueprint)
constructedObject.add(component)

# the component __init__ calls setType(), which gives us our initial guess at
# what the flags should be.
if self.flags is not None:
# override the flags from __init__ with the ones from the blueprint
component.p.flags = Flags.fromString(self.flags)
else:
# potentially add the DEPLETABLE flag. Don't do this if we set flags
# explicitly. WARNING: If you add flags explicitly, it will
# turn off depletion so be sure to add depletable to your list of flags
# if you expect depletion
if any(nuc in blueprint.activeNuclides for nuc in component.getNuclides()):
component.p.flags |= Flags.DEPLETABLE

if component.hasFlags(Flags.DEPLETABLE):
# depletable components, whether auto-derived or explicitly flagged need expanded nucs
_insertDepletableNuclideKeys(component, blueprint)
return component
constructedObject = components.factory(shape, [], kwargs)
_setComponentFlags(constructedObject, self.flags, blueprint)
_insertDepletableNuclideKeys(constructedObject, blueprint)
return constructedObject

def _conformKwargs(self, blueprint, matMods):
"""This method gets the relevant kwargs to construct the component"""
Expand Down Expand Up @@ -288,9 +299,11 @@ def _insertDepletableNuclideKeys(c, blueprint):
armi.physics.neutronics.isotopicDepletion.isotopicDepletionInterface.isDepletable :
contains design docs describing the ``DEPLETABLE`` flagging situation
"""
nuclideBases.initReachableActiveNuclidesThroughBurnChain(
c.p.numberDensities, blueprint.activeNuclides
)
if c.hasFlags(Flags.DEPLETABLE):
# depletable components, whether auto-derived or explicitly flagged need expanded nucs
nuclideBases.initReachableActiveNuclidesThroughBurnChain(
c.p.numberDensities, blueprint.activeNuclides
)


class ComponentKeyedList(yamlize.KeyedList):
Expand All @@ -307,6 +320,46 @@ class ComponentKeyedList(yamlize.KeyedList):
key_attr = ComponentBlueprint.name


class GroupedComponent(yamlize.Object):
"""
A pointer to a component with a multiplicity to be used in a ComponentGroup.
Multiplicity can be a fraction (e.g. to set volume fractions)
"""

name = yamlize.Attribute(type=str)
mult = yamlize.Attribute(type=float)


class ComponentGroup(yamlize.KeyedList):
"""
A single component group containing multiple GroupedComponents
Example
-------
triso:
kernel:
mult: 0.7
buffer:
mult: 0.3
"""

group_name = yamlize.Attribute(type=str)
key_attr = GroupedComponent.name
item_type = GroupedComponent


class ComponentGroups(yamlize.KeyedList):
"""
A list of component groups.
This is used in the top-level blueprints file.
"""

key_attr = ComponentGroup.group_name
item_type = ComponentGroup


# This import-time magic requires all possible components
# be imported before this module imports. The intent
# was to make registration basically automatic. This has proven
Expand All @@ -324,3 +377,19 @@ class ComponentKeyedList(yamlize.KeyedList):
dimName,
yamlize.Attribute(name=dimName, type=ComponentDimension, default=None),
)


def _setComponentFlags(component, flags, blueprint):
"""Update component flags based on user input in blueprint"""
# the component __init__ calls setType(), which gives us our initial guess at
# what the flags should be.
if flags is not None:
# override the flags from __init__ with the ones from the blueprint
component.p.flags = Flags.fromString(flags)
else:
# potentially add the DEPLETABLE flag. Don't do this if we set flags
# explicitly. WARNING: If you add flags explicitly, it will
# turn off depletion so be sure to add depletable to your list of flags
# if you expect depletion
if any(nuc in blueprint.activeNuclides for nuc in component.getNuclides()):
component.p.flags |= Flags.DEPLETABLE
Loading

0 comments on commit 64f0bac

Please sign in to comment.