Source code for armi.reactor.blueprints

# 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.

"""
Blueprints describe the geometric and composition details of the objects in the reactor
(e.g. fuel assemblies, control rods, etc.).

Inputs captured within this blueprints module pertain to major design criteria like
custom material properties or basic structures like the assemblies in use.

This is essentially a wrapper for a yaml loader.
The given yaml file is expected to rigidly adhere to given key:value pairings.

See the :doc:`blueprints documentation </user/inputs/blueprints>` for more details.

The file structure is expectation is::

    nuclide flags:
        AM241: {burn: true, xs: true}
        ...

    custom isotopics: {} # optional

    blocks:
        name:
            component name:
                component dimensions
        ...

    assemblies:
        name:
            specifier: ABC
            blocks: [...]
            height: [...]
            axial mesh points: [...]
            xs types: [...]

            # optional
            myMaterialModification1: [...]
            myMaterialModification2: [...]

            # optionally extra settings (note this is probably going to be a removed feature)
            #    hotChannelFactors: TWRPclad

Examples
--------
>>> design = blueprints.Blueprints.load(self.yamlString)
>>> print(design.gridDesigns)

Notes
-----
The blueprints system was built to enable round trip translations between
text representations of input and objects in the code.
"""
import copy
import os
import pathlib
import traceback
import typing

from ruamel.yaml import CLoader, RoundTripLoader
import ordered_set
import tabulate
import yamlize
import yamlize.objects

from armi import context
from armi import getPluginManager, getPluginManagerOrFail
from armi import migration
from armi import plugins
from armi import runLog
from armi.nucDirectory import nuclideBases
from armi.physics.neutronics.settings import CONF_LOADING_FILE
from armi.reactor import assemblies
from armi.reactor import geometry
from armi.reactor import systemLayoutInput
from armi.reactor.blueprints import isotopicOptions
from armi.reactor.blueprints.assemblyBlueprint import AssemblyKeyedList
from armi.reactor.blueprints.blockBlueprint import BlockKeyedList
from armi.reactor.blueprints.componentBlueprint import ComponentGroups
from armi.reactor.blueprints.componentBlueprint import ComponentKeyedList
from armi.reactor.blueprints.gridBlueprint import Grids, Triplet
from armi.reactor.blueprints.reactorBlueprint import Systems, SystemBlueprint
from armi.reactor.converters import axialExpansionChanger
from armi.reactor.flags import Flags
from armi.settings.fwSettings.globalSettings import (
    CONF_DETAILED_AXIAL_EXPANSION,
    CONF_ASSEM_FLAGS_SKIP_AXIAL_EXP,
    CONF_INPUT_HEIGHTS_HOT,
    CONF_NON_UNIFORM_ASSEM_FLAGS,
    CONF_ACCEPTABLE_BLOCK_AREA_ERROR,
    CONF_GEOM_FILE,
)
from armi.utils import textProcessors
from armi.utils.customExceptions import InputError

context.BLUEPRINTS_IMPORTED = True
context.BLUEPRINTS_IMPORT_CONTEXT = "".join(traceback.format_stack())


[docs]def loadFromCs(cs, roundTrip=False): """Function to load Blueprints based on supplied ``Settings``.""" from armi.utils import directoryChangers with directoryChangers.DirectoryChanger(cs.inputDirectory, dumpOnException=False): with open(cs[CONF_LOADING_FILE], "r") as bpYaml: root = pathlib.Path(cs[CONF_LOADING_FILE]).parent.absolute() bpYaml = textProcessors.resolveMarkupInclusions(bpYaml, root) try: bp = Blueprints.load(bpYaml, roundTrip=roundTrip) except yamlize.yamlizing_error.YamlizingError as err: if "cross sections" in err.args[0]: runLog.error( "The loading file {} contains invalid `cross sections` input. " "Please run the `modify` entry point on this case to automatically convert." "".format(cs[CONF_LOADING_FILE]) ) raise return bp
class _BlueprintsPluginCollector(yamlize.objects.ObjectType): """ Simple metaclass for adding yamlize.Attributes from plugins to Blueprints. This calls the defineBlueprintsSections() plugin hook to discover new class attributes to add before the yamlize code fires off to make the root yamlize.Object. Since yamlize.Object itself uses a metaclass to define the attributes to turn into yamlize.Attributes, these need to be folded in early. """ def __new__(mcs, name, bases, attrs): pm = getPluginManager() if pm is None: runLog.warning( "Blueprints were instantiated before the framework was " "configured with plugins. Blueprints cannot be imported before " "ARMI has been configured." ) else: pluginSections = pm.hook.defineBlueprintsSections() for plug in pluginSections: for (attrName, section, resolver) in plug: assert isinstance(section, yamlize.Attribute) if attrName in attrs: raise plugins.PluginError( "There is already a section called '{}' in the reactor " "blueprints".format(attrName) ) attrs[attrName] = section attrs["_resolveFunctions"].append(resolver) newType = yamlize.objects.ObjectType.__new__(mcs, name, bases, attrs) return newType
[docs]class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector): """Base Blueprintsobject representing all the subsections in the input file.""" nuclideFlags = yamlize.Attribute( key="nuclide flags", type=isotopicOptions.NuclideFlags, default=None ) customIsotopics = yamlize.Attribute( key="custom isotopics", type=isotopicOptions.CustomIsotopics, default=None ) blockDesigns = yamlize.Attribute(key="blocks", type=BlockKeyedList, default=None) assemDesigns = yamlize.Attribute( key="assemblies", type=AssemblyKeyedList, default=None ) systemDesigns = yamlize.Attribute(key="systems", type=Systems, default=None) gridDesigns = yamlize.Attribute(key="grids", type=Grids, default=None) 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. _resolveFunctions = [] def __new__(cls): # yamlizable does not call __init__, so attributes that are not defined above # need to be initialized here self = yamlize.Object.__new__(cls) self.assemblies = {} self._prepped = False self._assembliesBySpecifier = {} # Better for performance since these are used for lookups self.allNuclidesInProblem = ordered_set.OrderedSet() self.activeNuclides = ordered_set.OrderedSet() self.inertNuclides = ordered_set.OrderedSet() self.nucsToForceInXsGen = ordered_set.OrderedSet() self.elementsToExpand = [] return self def __init__(self): # Yamlize does not call __init__, instead we use Blueprints.load which # creates and instance of a Blueprints object and initializes it with values # using setattr. self._assembliesBySpecifier = {} self._prepped = False self.systemDesigns = Systems() self.assemDesigns = AssemblyKeyedList() self.blockDesigns = BlockKeyedList() self.assemblies = {} self.grids = Grids() self.elementsToExpand = [] def __repr__(self): return "<{} Assemblies:{} Blocks:{}>".format( self.__class__.__name__, len(self.assemDesigns), len(self.blockDesigns) )
[docs] def constructAssem(self, cs, name=None, specifier=None): """ Construct a new assembly instance from the assembly designs in this Blueprints object. Parameters ---------- cs : Settings Used to apply various modeling options when constructing an assembly. name : str (optional, and should be exclusive with specifier) Name of the assembly to construct. This should match the key that was used to define the assembly in the Blueprints YAML file. specifier : str (optional, and should be exclusive with name) Identifier of the assembly to construct. This should match the identifier that was used to define the assembly in the Blueprints YAML file. Raises ------ ValueError If neither name nor specifier are passed Notes ----- There is some possibility for "compiling" the logic with closures to make constructing an assembly / block / component faster. At this point is is pretty much irrelevant because we are currently just deepcopying already constructed assemblies. Currently, this method is backward compatible with other code in ARMI and generates the `.assemblies` attribute (the BOL assemblies). Eventually, this should be removed. """ self._prepConstruction(cs) # TODO: this should be migrated assembly designs instead of assemblies if name is not None: assem = self.assemblies[name] elif specifier is not None: assem = self._assembliesBySpecifier[specifier] else: raise ValueError("Must supply assembly name or specifier to construct") a = copy.deepcopy(assem) # since a deepcopy has the same assembly numbers and block id's, we need to make it unique a.makeUnique() return a
def _prepConstruction(self, cs): """ This method initializes a bunch of information within a Blueprints object such as assigning assembly and block type numbers, resolving the nuclides in the problem, and pre-populating assemblies. Ideally, it would not be necessary at all, but the ``cs`` currently contains a bunch of information necessary to create the applicable model. If it were possible, it would be terrific to override the Yamlizable.from_yaml method to run this code after the instance has been created, but we need additional information in order to build the assemblies that is not within the YAML file. This method should not be called directly, but it is used in testing. """ if not self._prepped: self._assignTypeNums() for func in self._resolveFunctions: func(self, cs) self._resolveNuclides(cs) self._assembliesBySpecifier.clear() self.assemblies.clear() for aDesign in self.assemDesigns: a = aDesign.construct(cs, self) self._assembliesBySpecifier[aDesign.specifier] = a self.assemblies[aDesign.name] = a runLog.header("=========== Verifying Assembly Configurations ===========") self._checkAssemblyAreaConsistency(cs) if not cs[CONF_DETAILED_AXIAL_EXPANSION]: # this is required to set up assemblies so they know how to snap # to the reference mesh. They wont know the mesh to conform to # otherwise.... axialExpansionChanger.makeAssemsAbleToSnapToUniformMesh( self.assemblies.values(), cs[CONF_NON_UNIFORM_ASSEM_FLAGS] ) if not cs[CONF_INPUT_HEIGHTS_HOT]: runLog.header( "=========== Axially expanding all assemblies from Tinput to Thot ===========" ) # expand axial heights from cold to hot so dims and masses are consistent # with specified component hot temperatures. assemsToSkip = [ Flags.fromStringIgnoreErrors(t) for t in cs[CONF_ASSEM_FLAGS_SKIP_AXIAL_EXP] ] assemsToExpand = list( a for a in list(self.assemblies.values()) if not any(a.hasFlags(f) for f in assemsToSkip) ) axialExpansionChanger.expandColdDimsToHot( assemsToExpand, cs[CONF_DETAILED_AXIAL_EXPANSION], ) getPluginManagerOrFail().hook.afterConstructionOfAssemblies( assemblies=self.assemblies.values(), cs=cs ) self._prepped = True def _assignTypeNums(self): if self.blockDesigns is None: # this happens when directly defining assemblies. self.blockDesigns = BlockKeyedList() for aDesign in self.assemDesigns: for bDesign in aDesign.blocks: if bDesign not in self.blockDesigns: self.blockDesigns.add(bDesign) def _resolveNuclides(self, cs): """ Process elements and determine how to expand them to natural isotopics. Also builds meta-data about which nuclides are in the problem. This system works by building a dictionary in the ``elementsToExpand`` attribute with ``Element`` keys and list of ``NuclideBase`` values. The actual expansion of elementals to isotopics occurs during :py:meth:`Component construction <armi.reactor.blueprints.componentBlueprint. ComponentBlueprint._constructMaterial>`. """ from armi import utils actives = set() inerts = set() nuclideFlags = self.nuclideFlags or isotopicOptions.genDefaultNucFlags() nucsToForceInXsGen = set() # just expanding flags now. ndense gets expanded in comp blueprints self.elementsToExpand = [] for nucFlag in nuclideFlags: # this returns any nuclides that are flagged specifically for expansion by input ( expandedElements, undefBurnChainActiveNuclides, ) = nucFlag.fileAsActiveOrInert( actives, inerts, ) self.elementsToExpand.extend(expandedElements) inerts -= actives self.customIsotopics = self.customIsotopics or isotopicOptions.CustomIsotopics() eleKeep, eleExpand = isotopicOptions.eleExpandInfoBasedOnCodeENDF(cs) # Flag all elementals for expansion unless they've been flagged otherwise by # user input or automatic lattice/datalib rules. for nucBase in nuclideBases.instances: isAlreadyIsotopic = not isinstance(nucBase, nuclideBases.NaturalNuclideBase) if isAlreadyIsotopic: # `elemental` may be a NaturalNuclideBase or a NuclideBase # skip all NuclideBases (isotopics) continue # we now know its an elemental elemental = nucBase if elemental in eleKeep: continue if elemental.name in actives: currentSet = actives elif elemental.name in inerts: currentSet = inerts else: # This was not specified in the nuclide flags at all as burn or xs. # If a material with this in its composition is brought in # it's nice from a user perspective to allow it. # But current behavior is that all nuclides in problem # must be declared up front. continue self.elementsToExpand.append(elemental.element) if ( elemental.name in nuclideFlags and nuclideFlags[elemental.element.symbol].expandTo ): # user-input expandTo has precedence newNuclides = [ nuclideBases.byName[nn] for nn in nuclideFlags[elemental.element.symbol].expandTo ] elif elemental in eleExpand and elemental.element.symbol in nuclideFlags: # code-specific expansion required based on code and ENDF newNuclides = eleExpand[elemental] # overlay code details onto nuclideFlags for other parts of the code # that will use them. # TODO: would be better if nuclideFlags did this upon reading s.t. # order didn't matter. On the other hand, this is the only place in # the code where NuclideFlags get built and have user settings around # (hence "resolve"). # This must be updated because the operative expansion code just uses the flags # # Also, if this element is not in nuclideFlags at all, we just don't add it nuclideFlags[elemental.element.symbol].expandTo = [ nb.name for nb in newNuclides ] else: # expand to all possible natural isotopics newNuclides = elemental.element.getNaturalIsotopics() # remove the elemental and add the isotopic currentSet.remove(elemental.name) for nb in newNuclides: currentSet.add(nb.name) # force everything asked for in xsGen nucsToForceInXsGen = ordered_set.OrderedSet(sorted(actives.union(inerts))) # add all detailed isotopes in ENDF if requested isotopicOptions.autoUpdateNuclideFlags(cs, nuclideFlags, inerts) self.nuclideFlags = nuclideFlags if self.elementsToExpand: runLog.info( "Will expand {} elementals to have natural isotopics".format( ", ".join(element.symbol for element in self.elementsToExpand) ) ) self.activeNuclides = ordered_set.OrderedSet(sorted(actives)) self.inertNuclides = ordered_set.OrderedSet(sorted(inerts)) self.allNuclidesInProblem = ordered_set.OrderedSet( sorted(actives.union(inerts)) ) self.nucsToForceInXsGen = ordered_set.OrderedSet(sorted(nucsToForceInXsGen)) # Inform user which nuclides are truncating the burn chain. if undefBurnChainActiveNuclides and nuclideBases.burnChainImposed: runLog.info( tabulate.tabulate( [ [ "Nuclides truncating the burn-chain:", utils.createFormattedStrWithDelimiter( list(undefBurnChainActiveNuclides) ), ] ], tablefmt="plain", ), single=True, ) def _checkAssemblyAreaConsistency(self, cs): references = None for a in self.assemblies.values(): if references is None: references = (a, a.getArea()) continue assemblyArea = a.getArea() if isinstance(a, assemblies.RZAssembly): # R-Z assemblies by definition have different areas, so skip the check continue if abs(references[1] - assemblyArea) > 1e-9: runLog.error("REFERENCE COMPARISON ASSEMBLY:") references[0][0].printContents() runLog.error("CURRENT COMPARISON ASSEMBLY:") a[0].printContents() raise InputError( "Assembly {} has a different area {} than assembly {} {}. Check inputs for accuracy".format( a, assemblyArea, references[0], references[1] ) ) blockArea = a[0].getArea() for b in a[1:]: if ( abs(b.getArea() - blockArea) / blockArea > cs[CONF_ACCEPTABLE_BLOCK_AREA_ERROR] ): runLog.error("REFERENCE COMPARISON BLOCK:") a[0].printContents(includeNuclides=False) runLog.error("CURRENT COMPARISON BLOCK:") b.printContents(includeNuclides=False) for c in b.getChildren(): runLog.error( "{0} area {1} effective area {2}" "".format(c, c.getArea(), c.getVolume() / b.getHeight()) ) raise InputError( "Block {} has a different area {} than block {} {}. Check inputs for accuracy".format( b, b.getArea(), a[0], blockArea ) )
[docs] @classmethod def migrate(cls, inp: typing.TextIO): """Given a stream representation of a blueprints file, migrate it. Parameters ---------- inp : typing.TextIO Input stream to migrate. """ for migI in migration.ACTIVE_MIGRATIONS: if issubclass(migI, migration.base.BlueprintsMigration): mig = migI(stream=inp) inp = mig.apply() return inp
[docs] @classmethod def load(cls, stream, roundTrip=False): """This method is a wrapper around the `yamlize.Object.load()` method. The reason for the wrapper is to allow us to default to `Cloader`. Essentially, the `CLoader` class is 10x faster, but doesn't allow for "round trip" (read- write) access to YAMLs; for that we have the `RoundTripLoader`. """ loader = RoundTripLoader if roundTrip else CLoader return super().load(stream, Loader=loader)
[docs] def addDefaultSFP(self): """Create a default SFP if it's not in the blueprints.""" if self.systemDesigns is not None: if not any(structure.typ == "sfp" for structure in self.systemDesigns): sfp = SystemBlueprint("Spent Fuel Pool", "sfp", Triplet()) sfp.typ = "sfp" self.systemDesigns["Spent Fuel Pool"] = sfp else: runLog.warning( f"Can't add default SFP to {self}, there are no systemDesigns!" )
[docs]def migrate(bp: Blueprints, cs): """ Apply migrations to the input structure. This is a good place to perform migrations that address changes to the system design description (settings, blueprints, geom file). We have access to all three here, so we can even move stuff between files. Namely, this: * creates a grid blueprint to represent the core layout from the old ``geomFile`` setting, and applies that grid to a ``core`` system. * moves the radial and azimuthal submesh values from the ``geomFile`` to the assembly designs, but only if they are uniform (this is limiting, but could be made more sophisticated in the future, if there is need) This allows settings-driven core map to still be used for backwards compatibility. At some point once the input stabilizes, we may wish to move this out to the dedicated migration portion of the code, and not perform the migration so implicitly. """ from armi.reactor.blueprints import gridBlueprint if bp.systemDesigns is None: bp.systemDesigns = Systems() if bp.gridDesigns is None: bp.gridDesigns = gridBlueprint.Grids() if "core" in [rd.name for rd in bp.gridDesigns]: raise ValueError("Cannot auto-create a 2nd `core` grid. Adjust input.") geom = systemLayoutInput.SystemLayoutInput() geom.readGeomFromFile(os.path.join(cs.inputDirectory, cs[CONF_GEOM_FILE])) gridDesigns = geom.toGridBlueprints("core") for design in gridDesigns: bp.gridDesigns[design.name] = design if "core" in [rd.name for rd in bp.systemDesigns]: raise ValueError( "Core map is defined in both the ``geometry`` setting and in " "the blueprints file. Only one definition may exist. " "Update inputs." ) bp.systemDesigns["core"] = SystemBlueprint("core", "core", Triplet()) if geom.geomType in (geometry.GeomType.RZT, geometry.GeomType.RZ): aziMeshes = {indices[4] for indices, _ in geom.assemTypeByIndices.items()} radMeshes = {indices[5] for indices, _ in geom.assemTypeByIndices.items()} if len(aziMeshes) > 1 or len(radMeshes) > 1: raise ValueError( "The system layout described in {} has non-uniform " "azimuthal and/or radial submeshing. This migration is currently " "only smart enough to handle a single radial and single azimuthal " "submesh for all assemblies.".format(cs[CONF_GEOM_FILE]) ) radMesh = next(iter(radMeshes)) aziMesh = next(iter(aziMeshes)) for _, aDesign in bp.assemDesigns.items(): aDesign.radialMeshPoints = radMesh aDesign.azimuthalMeshPoints = aziMesh
# TODO: write out the migrated file. At the moment this messes up the case # title and doesn't yet have the other systems in place so this isn't the right place.