Source code for armi.reactor.blueprints.isotopicOptions

# 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 component definition, and code for constructing an ARMI ``Component``.
"""
import yamlize
from armi.utils import units

from armi import materials
from armi import runLog
from armi.localization import exceptions
from armi.nucDirectory import elements
from armi.nucDirectory import nucDir
from armi.nucDirectory import nuclideBases
from armi.utils import densityTools


ALLOWED_KEYS = set(nuclideBases.byName.keys()) | set(elements.bySymbol.keys())


[docs]class NuclideFlag(yamlize.Object): """ This class defines a nuclide options for use within the ARMI simulation, defining whether or not it should be included in the burn chain and cross sections. """ nuclideName = yamlize.Attribute(type=str) @nuclideName.validator def nuclideName(self, value): if value not in ALLOWED_KEYS: raise ValueError( "`{}` is not a valid nuclide name, must be one of: {}".format( value, ALLOWED_KEYS ) ) burn = yamlize.Attribute(type=bool) xs = yamlize.Attribute(type=bool) def __init__(self, nuclideName, burn, xs): # note: yamlize does not call an __init__ method, instead it uses __new__ and setattr self.nuclideName = nuclideName self.burn = burn self.xs = xs def __repr__(self): return "<NuclideFlag name:{} burn:{} xs:{}>".format( self.nuclideName, self.burn, self.xs )
[docs] def prepForCase(self, activeSet, inertSet, undefinedBurnChainActiveNuclides): """Take in the string nuclide or element name, try to expand it out to its bases correctly.""" actualNuclides = nucDir.getNuclidesFromInputName(self.nuclideName) for actualNuclide in actualNuclides: if self.burn: if not actualNuclide.trans and not actualNuclide.decays: undefinedBurnChainActiveNuclides.add(actualNuclide.name) activeSet.add(actualNuclide.name) if self.xs: inertSet.add(actualNuclide.name)
[docs]class NuclideFlags(yamlize.KeyedList): """ An OrderedDict of ``NuclideFlags``, keyed by their ``nuclideName``. """ item_type = NuclideFlag key_attr = NuclideFlag.nuclideName
[docs]class CustomIsotopic(yamlize.Map): """ User specified, custom isotopics input defined by a name (such as MOX), and key/pairs of nuclide names and numeric values consistent with the ``input format``. """ key_type = yamlize.Typed(str) value_type = yamlize.Typed(float) name = yamlize.Attribute(type=str) inputFormat = yamlize.Attribute(key="input format", type=str) @inputFormat.validator def inputFormat(self, value): if value not in self._allowedFormats: raise ValueError( "Cannot set `inputFormat` to `{}`, must be one of: {}".format( value, self._allowedFormats ) ) _density = yamlize.Attribute(key="density", type=float, default=None) _allowedFormats = {"number fractions", "number densities", "mass fractions"} def __new__(cls, *args): self = yamlize.Map.__new__(cls, *args) # the density as computed by source number densities self._computedDensity = None return self def __init__(self, name, inputFormat, density): # note: yamlize does not call an __init__ method, instead it uses __new__ and setattr self._name = None self.name = name self._inputFormat = None self.inputFormat = inputFormat self.density = density self.massFracs = {} def __setitem__(self, key, value): if key not in ALLOWED_KEYS: raise ValueError( "Key `{}` is not valid, must be one of: {}".format(key, ALLOWED_KEYS) ) yamlize.Map.__setitem__(self, key, value) @property def density(self): return self._computedDensity or self._density @density.setter def density(self, value): if self._computedDensity is not None: raise AttributeError( "Density was computed from number densities, and should not be " "set directly." ) self._density = value if value is not None and value < 0: raise ValueError( "Cannot set `density` to `{}`, must greater than 0".format(value) )
[docs] @classmethod def from_yaml(cls, loader, node, rtd): """ Override the ``Yamlizable.from_yaml`` to inject custom data validation logic, and complete initialization of the object. """ self = yamlize.Map.from_yaml.__func__(cls, loader, node, rtd) try: self._initializeMassFracs() self._expandElementMassFracs() except Exception as ex: # use a YamlizingError to line/column of erroneous input raise yamlize.YamlizingError(str(ex), node) return self
[docs] @classmethod def from_yaml_key_val(cls, loader, key_node, val_node, key_attr, rtd): """ Override the ``Yamlizable.from_yaml`` to inject custom data validation logic, and complete initialization of the object. """ self = yamlize.Map.from_yaml_key_val.__func__( cls, loader, key_node, val_node, key_attr, rtd ) try: self._initializeMassFracs() self._expandElementMassFracs() except Exception as ex: # use a YamlizingError to line/column of erroneous input raise yamlize.YamlizingError(str(ex), val_node) return self
def _initializeMassFracs(self): self.massFracs = dict() # defaults to 0.0, __init__ is not called if any(v < 0.0 for v in self.values()): raise ValueError( "Custom isotopic input for {} is negative".format(self.name) ) valSum = sum(self.values()) if not abs(valSum - 1.0) < 1e-5 and "fractions" in self.inputFormat: raise ValueError( "Fractional custom isotopic input values must sum to 1.0 in: {}".format( self.name ) ) if self.inputFormat == "number fractions": sumNjAj = 0.0 for nuc, nj in self.items(): if nj: sumNjAj += nj * nucDir.getAtomicWeight(nuc) for nuc, value in self.items(): massFrac = value * nucDir.getAtomicWeight(nuc) / sumNjAj self.massFracs[nuc] = massFrac elif self.inputFormat == "number densities": if self._density is not None: raise exceptions.InputError( "Custom isotopic `{}` is over-specified. It was provided as number " "densities, and but density ({}) was also provided. Is the input format " "correct?".format(self.name, self.density) ) M = { nuc: Ni / units.MOLES_PER_CC_TO_ATOMS_PER_BARN_CM * nucDir.getAtomicWeight(nuc) for nuc, Ni in self.items() } densityTotal = sum(M.values()) if densityTotal < 0: raise ValueError("Computed density is negative") for nuc, Mi in M.items(): self.massFracs[nuc] = Mi / densityTotal self._computedDensity = densityTotal elif self.inputFormat == "mass fractions": self.massFracs = dict(self) # as input else: raise ValueError( "Unrecognized custom isotopics input format {}.".format( self.inputFormat ) ) def _expandElementMassFracs(self): """ Expand the massFrac dictionary element inputs to isotopics inputs (keys are strings) when the element name is not a elemental nuclide. Most everywhere else expects Nuclide objects (or nuclide names). This input allows a user to enter "U" which would expand to the naturally occurring uranium isotopics. This is different than the isotopic expansion done for meeting user-specified modeling options (such as an MC**2, or MCNP expecting elements or isotopes), because it translates the user input into something that can be used later on. """ elementsToExpand = [] for nucName in self.massFracs: if nucName not in nuclideBases.byName: element = elements.bySymbol.get(nucName) if element is not None: runLog.info( "Expanding custom isotopic `{}` element `{}` to natural isotopics".format( self.name, nucName ) ) elementsToExpand.append(element) else: raise exceptions.InputError( "Unrecognized nuclide/isotope/element in input: {}".format( nucName ) ) densityTools.expandElementalMassFracsToNuclides( self.massFracs, elementsToExpand )
[docs] def apply(self, material): """ Apply specific isotopic compositions to a component. Generically, materials have composition-dependent bulk properties such as mass density. Note that this operation does not update these material properties. Use with care. Parameters ---------- material : Material An ARMI Material instance. """ material.p.massFrac = dict(self.massFracs) if self.density is not None: if not isinstance(material, materials.Custom): runLog.warning( "You specified a custom mass density on `{}` with custom isotopics `{}`. " "This has no effect; you can only set this on `Custom` " "materials. Continuing to use {} mass density.".format( material, self.name, material ) ) return # specifically, non-Custom materials only use refDensity and dLL, .p.density has no effect material.p.density = self.density
[docs]class CustomIsotopics(yamlize.KeyedList): """ OrderedDict of CustomIsotopic objects, keyed by their name. """ item_type = CustomIsotopic key_attr = CustomIsotopic.name # note: yamlize does not call an __init__ method, instead it uses __new__ and setattr
[docs] def apply(self, material, customIsotopicsName): """ Apply specific isotopic compositions to a component. Generically, materials have composition-dependent bulk properties such as mass density. Note that this operation does not update these material properties. Use with care. Parameters ---------- material : Material Material instance to adjust. customIsotopicName : str String corresponding to the ``CustomIsoptopic.name``. """ if customIsotopicsName not in self: raise KeyError( "The input custom isotopics do not include {}. " "The only present specifications are {}".format( customIsotopicsName, self.keys() ) ) custom = self[customIsotopicsName] custom.apply(material)