Source code for armi.reactor.components.component

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

"""
Components represent geometric objects within an assembly such as fuel, bond, coolant, ducts, wires, etc.

This module contains the abstract definition of a Component.
"""
import re
import copy

import numpy
import six

from armi.materials import material
from armi.materials import custom
from armi import runLog
from armi.bookkeeping import report
from armi.reactor import composites
from armi.reactor import parameters
from armi.reactor.components import componentParameters
from armi.utils import densityTools
from armi.utils.units import C_TO_K
from armi.materials import void
from armi.nucDirectory import nuclideBases
from armi import materials
from armi.reactor.flags import Flags

COMPONENT_LINK_REGEX = re.compile(r"^\s*(.+?)\s*\.\s*(.+?)\s*$")


_NICE_DIM_NAMES = {
    "id": "Inner Diameter (cm)",
    "od": "Outer Diameter (cm)",
    "ip": "Inner Pitch (cm)",
    "op": "Outer Pitch (cm)",
    "mult": "Multiplicity",
    "axialPitch": "Axial Pitch (cm)",
    "helixDiameter": "Helix Diameter (cm)",
    "length": "Length (cm)",
    "height": "Height (cm)",
    "width": "Width (cm)",
    "areaMod": "Area Mod. Factor",
}


[docs]def componentTypeIsValid(component, name): """ Checks that the component assigned component type is valid Notes ----- - `Coolant` components are can no longer be defined as a general `Component` and should be specfied as a `DerivedShape` if the coolant dimensions are not provided. """ from armi.reactor.components import NullComponent if name.lower() == "coolant": invalidComponentTypes = [Component, NullComponent] if component.__class__ in invalidComponentTypes: raise ValueError( "Coolant components cannot be defined as a `Component`. Either define coolant as a " "`DerivedShape` or specify its dimensions explicitly using another component type." )
[docs]class ComponentType(composites.CompositeModelType): """ ComponetType is a metaclass for storing and initializing Component subclass types. The construction of Component subclasses is being done through factories for ease of user input. As a consequence, the ``__init__`` methods' arguments need to be known in order to conform them to the correct format. Additionally, the constructors arguments can be used to determine the Component subclasses dimensions. .. warning:: The import-time metaclass-based component subclass registration was a good idea, but in practice has caused significant confusion and trouble. We will replace this soon with an explicit plugin-based component subclass registration system. """ TYPES = dict() NON_DIMENSION_NAMES = ( "Tinput", "Thot", "isotopics", "mergeWith", "material", "name", "components", "area", ) def __new__(cls, name, bases, attrs): newType = composites.CompositeModelType.__new__(cls, name, bases, attrs) ComponentType.TYPES[name.lower()] = newType # the co_varnames attribute contains arguments and then locals so we must # restrict it to just the arguments. signature = newType.__init__.__code__.co_varnames[ 1 : newType.__init__.__code__.co_argcount ] # INIT_SIGNATURE and DIMENSION_NAMES are in the same order as the method signature newType.INIT_SIGNATURE = tuple(signature) newType.DIMENSION_NAMES = tuple( k for k in newType.INIT_SIGNATURE if k not in ComponentType.NON_DIMENSION_NAMES ) return newType
[docs]class Component(composites.Composite, metaclass=ComponentType): """ A primitive object in a reactor that has definite area/volume, material and composition. Could be fuel pins, cladding, duct, wire wrap, etc. One component object may represent multiple physical components via the ``multiplicity`` mechanism. Attributes ---------- temperatureInC : float Current temperature of component in celcius. inputTemperatureInC : float Reference temperature in C at which dimension definitions were input temperatureInC : float Temperature in C to which dimensions were thermally-expanded upon input. material : str or material.Material The material object that makes up this component and give it its thermo-mechanical properties. """ DIMENSION_NAMES = tuple() # will be assigned by ComponentType INIT_SIGNATURE = tuple() # will be assigned by ComponentType is3D = False # flag to show that area is 2D by default _COMP_REPORT_GROUPS = { "intercoolant": report.INTERCOOLANT_DIMS, "bond": report.BOND_DIMS, "duct": report.DUCT_DIMS, "coolant": report.COOLANT_DIMS, "clad": report.CLAD_DIMS, "fuel": report.FUEL_DIMS, "wire": report.WIRE_DIMS, "liner": report.LINER_DIMS, "gap": report.GAP_DIMS, } _TOLERANCE = 1e-10 THERMAL_EXPANSION_DIMS = set() pDefs = componentParameters.getComponentParameterDefinitions() def __init__( self, name, material, Tinput, Thot, area=None, isotopics="", mergeWith="", components=None, ): if components and name in components: raise ValueError( "Non-unique component name {} repeated in same block.".format(name) ) composites.Composite.__init__(self, str(name)) componentTypeIsValid(self, str(name)) self.p.area = area self.inputTemperatureInC = Tinput self.temperatureInC = Thot self.material = None self.setProperties(material) self.applyMaterialMassFracsToNumberDensities() # not necessary when duplicating... self.setType(name) self.p.mergeWith = mergeWith self.p.customIsotopicsName = isotopics @property def temperatureInC(self): """Return the hot temperature in Celsius.""" return self.p.temperatureInC @temperatureInC.setter def temperatureInC(self, value): """Set the hot temperature in Celsius.""" self.p.temperatureInC = value @property def temperatureInK(self): """Current hot temperature in Kelvin.""" return self.temperatureInC + C_TO_K
[docs] def __lt__(self, other): """ True if a circle encompassing this object has a smaller diameter than one encompassing another component. This allows sorting because the Python sort functions only use this method. """ thisOD = self.getBoundingCircleOuterDiameter(cold=True) thatOD = other.getBoundingCircleOuterDiameter(cold=True) try: return thisOD < thatOD except: raise ValueError( "Components 1 ({} with OD {}) and 2 ({} and OD {}) cannot be ordered because their " "bounding circle outer diameters are not comparable.".format( self, thisOD, other, thatOD ) )
def __setstate__(self, state): composites.Composite.__setstate__(self, state) self.material.parent = self
[docs] def _linkAndStoreDimensions(self, components, **dims): """Link dimensions to another component""" for key, val in dims.items(): self.setDimension(key, val) if components: self._resolveLinkedDims(components)
[docs] def _resolveLinkedDims(self, components): for dimName in self.DIMENSION_NAMES: value = self.p[dimName] if not isinstance(value, six.string_types): continue match = COMPONENT_LINK_REGEX.search(value) if match: try: name = match.group(1) comp = components[name] linkedKey = match.group(2) self.p[dimName] = _DimensionLink((comp, linkedKey)) except: raise KeyError( "Bad component link `{}` defined as `{}`".format(dimName, value) )
[docs] def setProperties(self, properties): """Apply thermo-mechanical properties of a Material.""" if isinstance(properties, str): mat = materials.resolveMaterialClassByName(properties)() # note that the material will not be expanded to natural isotopics # here because the user-input blueprints information is not available else: mat = properties self.material = mat self.material.parent = self self.clearLinkedCache()
[docs] def applyMaterialMassFracsToNumberDensities(self): """ Set initial (hot) number densities of this component based Material composition. Notes ----- We apply the hot-height density reduction here to account for pre-expanded block heights in blueprints. Future temperature changes can be handled by multiplications of 1/(1+dLL)**2 instead of 1/(1+dLL)**3 since we have pre-expanded in the axial direction. """ denistyIfNotPreExpandedAxially = self.material.getProperty( "density", self.temperatureInK ) # axial expansion factor must be applied because ARMI expects hot heights # to be entered on assemblies in the blueprints so that it doesn't have to # handle the problem of fuel axially expanding at a different rate than clad. axialExpansionFactor = 1.0 + self.material.linearExpansionFactor( self.temperatureInC, self.inputTemperatureInC ) self.p.numberDensities = densityTools.getNDensFromMasses( denistyIfNotPreExpandedAxially / axialExpansionFactor, self.material.p.massFrac, )
[docs] def getProperties(self): """Return the active Material object defining thermo-mechanical properties.""" return self.material
@property def liquidPorosity(self): return self.parent.p.liquidPorosity @liquidPorosity.setter def liquidPorosity(self, porosity): self.parent.p.liquidPorosity = porosity @property def gasPorosity(self): return self.parent.p.gasPorosity @gasPorosity.setter def gasPorosity(self, porosity): self.parent.p.gasPorosity = porosity
[docs] def __copy__(self): """ Duplicate a component, used for breaking fuel into separate components. """ linkedDims = self._getLinkedDimsAndValues() newC = copy.deepcopy(self) self._restoreLinkedDims(linkedDims) newC._restoreLinkedDims(linkedDims) return newC
[docs] def setLumpedFissionProducts(self, lfpCollection): """Sets lumped fission product collection on a lfp compatible material if possible""" try: self.getProperties().setLumpedFissionProducts(lfpCollection) except AttributeError: # This material doesn't setLumpedFissionProducts because it's a regular # material, not a lumpedFissionProductCompatable material pass
[docs] def getArea(self, cold=False): """ Get the area of a component in cm^2. See Also -------- block.getVolumeFractions: component coolant is typically the "leftover" and is calculated and set here """ area = self.getComponentArea(cold=cold) if self.p.get("modArea", None): # pylint: disable=no-member comp, arg = self.p.modArea if arg == "sub": area -= comp.getComponentArea(cold=cold) elif arg == "add": area += comp.getComponentArea(cold=cold) else: raise ValueError("Option {} does not exist".format(arg)) self._checkNegativeArea(area, cold) return area
[docs] def getVolume(self): """ Return the volume [cm^3] of the component. Notes ----- ``self.p.volume`` is not set until this method is called, so under most circumstances it is probably not safe to access ``self.p.volume`` directly. This is because not all components (e.g., ``DerivedShape``) can compute their volume during initialization. """ if self.p.volume is None: self._updateVolume() if self.p.volume is None: raise ValueError("{} has undefined volume.".format(self)) return self.p.volume
[docs] def clearCache(self): """ Invalidate the volume so that it will be recomputed from current dimensions upon next access. The updated value will be based on its shape and current dimensions. If there is a parent container and that container contains a DerivedShape, then that must be updated as well since its volume may be changing. See Also -------- clearLinkedCache: Clears cache of components that depend on this component's dimensions. """ self.p.volume = None if self.parent: self.parent.derivedMustUpdate = True
[docs] def _updateVolume(self): """Recompute and store volume.""" self.p.volume = self.computeVolume()
[docs] def computeVolume(self): """Compute volume.""" if not self.is3D: volume = self.getArea() * self.parent.getHeight() else: volume = self.getComponentVolume() self._checkNegativeVolume(volume) return volume
[docs] def _checkNegativeArea(self, area, cold): """ Check for negative area and warn/error when appropriate. Negative component area is allowed for Void materials (such as gaps) which may be placed between components that will overlap during thermal expansion (such as liners and cladding and annular fuel). Overlapping is allowed to maintain conservation of atoms while sticking close to the as-built geometry. Modules that need true geometries will have to handle this themselves. """ if numpy.isnan(area): return if area < 0.0: if ( cold and not self.containsVoidMaterial() ) or self.containsSolidMaterial(): negAreaFailure = ( "Component {} with {} has cold negative area of {} cm^2. " "This can be caused by component " "overlap with component dimension linking or by invalid inputs.".format( self, self.material, area ) ) raise ArithmeticError(negAreaFailure)
[docs] def _checkNegativeVolume(self, volume): """Check for negative volume See Also -------- self._checkNegativeArea """ if numpy.isnan(volume): return if volume < 0.0 and self.containsSolidMaterial(): negVolFailure = ( "Component {} with {} has cold negative volume of {} cm^3. " "This can be caused by component " "overlap with component dimension linking or by invalid inputs.".format( self, self.material, volume ) ) raise ArithmeticError(negVolFailure)
[docs] def containsVoidMaterial(self): """Returns True if component material is void.""" return isinstance(self.material, void.Void)
[docs] def containsSolidMaterial(self): """Returns True if the component material is a solid.""" return not isinstance(self.material, material.Fluid)
[docs] def getComponentArea(self, cold=False): """ Get the area of this component in cm^2. Parameters ---------- cold : bool, optional Compute the area with as-input dimensions instead of thermally-expanded """ raise NotImplementedError
[docs] def getComponentVolume(self): return self.p.volume
[docs] def setVolume(self, val): raise NotImplementedError
[docs] def setArea(self, val): raise NotImplementedError
[docs] def setTemperature(self, temperatureInC): r""" Adjust temperature of this component. This will cause thermal expansion or contraction of solid or liquid components and will accordingly adjust number densities to conserve mass. Liquids still have a number density adjustment, but some mass tends to expand in or out of the bounding area. Since some composites have multiple materials in them that thermally expand differently, the axial dimension is generally left unchanged. Hence, this a 2-D thermal expansion. Number density change is proportional to mass density change :math:`\frac{d\rho}{\rho}`. A multiplicative factor :math:`f_N` to apply to number densities when going from T to T' is as follows: .. math:: N^{\prime} = N \cdot f_N \\ \frac{dN}{N} = f_N - 1 Since :math:`\frac{dN}{N} \sim\frac{d\rho}{\rho}`, we have: .. math:: f_N = \frac{d\rho}{\rho} + 1 = \frac{\rho^{\prime}}{\rho} """ prevTemp, self.temperatureInC = self.temperatureInC, float(temperatureInC) f = self.material.getThermalExpansionDensityReduction( prevTemp, self.temperatureInC ) self.changeNDensByFactor(f) self.clearLinkedCache()
[docs] def getNuclides(self): """ Return nuclides in this component. This includes anything that has been specified in here, including trace nuclides. """ return list(self.p.numberDensities.keys())
[docs] def getNumberDensity(self, nucName): """ Get the number density of nucName, return zero if it does not exist here. Parameters ---------- nucName : str Nuclide name Returns ------- number density : float number density in atoms/bn-cm. """ return self.p.numberDensities.get(nucName, 0.0)
[docs] def getNuclideNumberDensities(self, nucNames): """Return a list of number densities for the nuc names requested.""" return [self.p.numberDensities.get(nucName, 0.0) for nucName in nucNames]
[docs] def _getNdensHelper(self): return dict(self.p.numberDensities)
[docs] def setName(self, name): """Components use name for type and name.""" composites.Composite.setName(self, name) self.setType(name)
[docs] def setNumberDensity(self, nucName, val): """ Set heterogeneous number density. Parameters ---------- nucName : str nuclide to modify val : float Number density to set in atoms/bn-cm (heterogeneous) """ self.p.numberDensities[nucName] = val self.p.assigned = parameters.SINCE_ANYTHING # necessary for syncMpiState parameters.ALL_DEFINITIONS[ "numberDensities" ].assigned = parameters.SINCE_ANYTHING
[docs] def setNumberDensities(self, numberDensities): """ Set one or more multiple number densities. Clears out any number density not listed. Parameters ---------- numberDensities : dict nucName: ndens pairs. Notes ----- We don't just call setNumberDensity for each nuclide because we don't want to call ``getVolumeFractions`` for each nuclide (it's inefficient). """ self.p.numberDensities = numberDensities
[docs] def updateNumberDensities(self, numberDensities): """ Set one or more multiple number densities. Leaves unlisted number densities alone. Parameters ---------- numberDensities : dict nucName: ndens pairs. """ self.p.numberDensities.update(numberDensities) # since we're updating the object the param points to but not the param itself, we have to inform # the param system to flag it as modified so it properly syncs during ``syncMpiState``. self.p.assigned = parameters.SINCE_ANYTHING self.p.paramDefs["numberDensities"].assigned = parameters.SINCE_ANYTHING
[docs] def getEnrichment(self): """Get the mass enrichment of this component, as defined by the material.""" return self.getMassEnrichment()
[docs] def getMassEnrichment(self): """ Get the mass enrichment of this component, as defined by the material. Notes ----- Getting mass enrichment on any level higher than this is ambiguous because you may have enriched boron in one pin and enriched uranium in another and blending those doesn't make sense. """ if self.material.enrichedNuclide is None: raise ValueError( "Cannot get enrichment of {} because `enrichedNuclide` is not defined." "".format(self.material) ) enrichedNuclide = nuclideBases.byName[self.material.enrichedNuclide] baselineNucNames = [nb.name for nb in enrichedNuclide.element.nuclideBases] massFracs = self.getMassFracs() massFracEnrichedElement = sum( massFrac for nucName, massFrac in massFracs.items() if nucName in baselineNucNames ) try: return ( massFracs.get(self.material.enrichedNuclide, 0.0) / massFracEnrichedElement ) except ZeroDivisionError: return 0.0
[docs] def getMass(self, nuclideNames=None): r""" Determine the mass in grams of nuclide(s) and/or elements in this object. .. math:: \text{mass} = \frac{\sum_i (N_i \cdot V \cdot A_i)}{N_A \cdot 10^{-24}} where :math:`N_i` is number density of nuclide i in (1/bn-cm), :math:`V` is the object volume in :math:`cm^3` :math:`N_A` is Avogadro's number in 1/moles, :math:`A_i` is the atomic weight of of nuclide i in grams/mole Parameters ---------- nuclideNames : str, optional The nuclide/element specifier to get the mass of in the object. If omitted, total mass is returned. Returns ------- mass : float The mass in grams. """ nuclideNames = self._getNuclidesFromSpecifier(nuclideNames) volume = self.getVolume() / ( self.parent.getSymmetryFactor() if self.parent else 1.0 ) densities = self.getNuclideNumberDensities(nuclideNames) return sum( densityTools.getMassInGrams(nucName, volume, numberDensity) for nucName, numberDensity in zip(nuclideNames, densities) )
[docs] def setDimension(self, key, val, retainLink=False, cold=True): """ Set a single dimension on the component. Parameters ---------- key : str The dimension key (op, ip, mult, etc.) val : float The value to set on the dimension retainLink : bool, optional If True, the val will be applied to the dimension of linked component which indirectly changes this component's dimensions. cold : book, optional If True sets the component to the dimension that would cause the hot dimension to be the specified value. """ if not key: return if retainLink and self.dimensionIsLinked(key): linkedComp, linkedDimName = self.p[key] linkedComp.setDimension(linkedDimName, val, cold=cold) else: expansionFactor = ( self.getThermalExpansionFactor() if key in self.THERMAL_EXPANSION_DIMS else 1.0 ) val = val / expansionFactor if not cold else val self.p[key] = val self.clearLinkedCache()
[docs] def getDimension(self, key, Tc=None, cold=False): """ Return a specific dimension at temperature as determined by key Parameters ---------- key : str The dimension key (op, ip, mult, etc.) Tc : float Temperature in C. If None, the current temperature of the component is used. cold : bool, optional If true, will return cold (input) value of the requested dimension """ dimension = self.p[key] if isinstance(dimension, _DimensionLink): return dimension.resolveDimension(Tc=Tc, cold=cold) if not dimension or cold or key not in self.THERMAL_EXPANSION_DIMS: return dimension return self.getThermalExpansionFactor(Tc) * dimension
[docs] def getBoundingCircleOuterDiameter(self, Tc=None, cold=False): """Abstract bounding circle method that should be overwritten by each shape subclass.""" raise NotImplementedError
[docs] def dimensionIsLinked(self, key): """True if a the specified dimension is linked to another dimension.""" return key in self.p and isinstance(self.p[key], _DimensionLink)
[docs] def getDimensionNamesLinkedTo(self, otherComponent): """Find dimension names linked to the other component in this component.""" dimNames = [] for dimName in self.DIMENSION_NAMES: isLinked = self.dimensionIsLinked(dimName) if isLinked and self.p[dimName].getLinkedComponent() is otherComponent: dimNames.append((dimName, self.p[dimName][1])) return dimNames
[docs] def clearLinkedCache(self): """Clear this cache and any other dependent volumes.""" self.clearCache() if self.parent: # pylint: disable=no-member # changes in dimensions can affect cached variables such as pitch self.parent.cached = {} for c in self.getLinkedComponents(): # no clearCache since parent already updated derivedMustUpdate in self.clearCache() c.p.volume = None
[docs] def getLinkedComponents(self): """Find other components that are linked to this component.""" dependents = [] for child in self.parent.getChildren(): for dimName in child.DIMENSION_NAMES: isLinked = child.dimensionIsLinked(dimName) if isLinked and child.p[dimName].getLinkedComponent() is self: dependents.append(child) return dependents
[docs] def getThermalExpansionFactor(self, Tc=None, T0=None): """ Retrieves the material thermal expansion fraction. Parameters ---------- Tc : float, optional Adjusted temperature to get the thermal expansion factor at relative to the reference temperature Returns ------- Thermal expansion factor as a percentage (1.0 + dLL), where dLL is the linear expansion factor. """ if isinstance(self.material, (material.Fluid, custom.Custom)): return 1.0 # No thermal expansion of fluids or custom materials if T0 is None: T0 = self.inputTemperatureInC if Tc is None: Tc = self.temperatureInC dLL = self.material.linearExpansionFactor(Tc=Tc, T0=T0) if not dLL and abs(Tc - T0) > self._TOLERANCE: runLog.error( "Linear expansion percent may not be implemented in the {} material class.\n" "This method needs to be implemented on the material to allow thermal expansion." ".\nReference temperature: {}, Adjusted temperature: {}, Temperature difference: {}, " "Specified tolerance: {}".format( self.material, T0, Tc, (Tc - T0), self._TOLERANCE, single=True ) ) raise RuntimeError( "Linear expansion percent may not be implemented in the {} material " "class.".format(self.material) ) return 1.0 + dLL
[docs] def printContents(self, includeNuclides=True): """Print a listing of the dimensions and composition of this component.""" runLog.important(self) runLog.important(self.setDimensionReport()) if includeNuclides: for nuc in self.getNuclides(): runLog.important( "{0:10s} {1:.7e}".format(nuc, self.getNumberDensity(nuc)) )
[docs] def setDimensionReport(self): """Gives a report of the dimensions of this component.""" reportGroup = None for componentType, componentReport in self._COMP_REPORT_GROUPS.items(): if componentType in self.getName(): reportGroup = componentReport break if not reportGroup: return "No report group designated for {} component.".format(self.getName()) reportGroup.header = [ "", "Tcold ({0})".format(self.inputTemperatureInC), "Thot ({0})".format(self.temperatureInC), ] dimensions = { k: self.p[k] for k in self.DIMENSION_NAMES if k not in ("modArea", "area") and self.p[k] is not None } # py3 cannot format None # Set component name and material report.setData("Name", [self.getName(), ""], reportGroup) report.setData("Material", [self.getProperties().name, ""], reportGroup) for dimName in dimensions: niceName = _NICE_DIM_NAMES.get(dimName, dimName) refVal = self.getDimension(dimName, cold=True) hotVal = self.getDimension(dimName) try: report.setData(niceName, [refVal, hotVal], reportGroup) except ValueError: runLog.warning( "{0} has an invalid dimension for {1}. refVal: {2} hotVal: {3}".format( self, dimName, refVal, hotVal ) ) # calculate thickness if applicable. suffix = None if "id" in dimensions: suffix = "d" elif "ip" in dimensions: suffix = "p" if suffix: coldIn = self.getDimension("i{0}".format(suffix), cold=True) hotIn = self.getDimension("i{0}".format(suffix)) coldOut = self.getDimension("o{0}".format(suffix), cold=True) hotOut = self.getDimension("o{0}".format(suffix)) if suffix and coldIn > 0.0: hotThick = (hotOut - hotIn) / 2.0 coldThick = (coldOut - coldIn) / 2.0 vals = ( "Thickness (cm)", "{0:.7f}".format(coldThick), "{0:.7f}".format(hotThick), ) report.setData(vals[0], [vals[1], vals[2]], reportGroup) return report.ALL[reportGroup]
[docs] def updateDims(self, key="", val=None): self.setDimension(key, val)
[docs] def mergeNuclidesInto(self, compToMergeWith): """ Set another component's number densities to reflect this one merged into it. You must also modify the geometry of the other component and remove this component to conserve atoms. """ # record pre-merged number densities and areas aMe = self.getArea() aMerge = compToMergeWith.getArea() meNDens = { nucName: aMe / aMerge * self.getNumberDensity(nucName) for nucName in self.getNuclides() } mergeNDens = { nucName: compToMergeWith.getNumberDensity(nucName) for nucName in compToMergeWith.getNuclides() } # set the new homogenized number densities from both. Allow # overlapping nuclides. for nucName in set(meNDens) | set(mergeNDens): compToMergeWith.setNumberDensity( nucName, (meNDens.get(nucName, 0.0) + mergeNDens.get(nucName, 0.0)) )
[docs] def iterComponents(self, typeSpec=None, exact=False): if self.hasFlags(typeSpec, exact): yield self
[docs] def backUp(self): """ Create and store a backup of the state. This needed to be overridden due to linked components which actually have a parameter value of another ARMI component. """ linkedDims = self._getLinkedDimsAndValues() composites.Composite.backUp(self) self._restoreLinkedDims(linkedDims)
[docs] def restoreBackup(self, paramsToApply): r""" Restore the parameters from perviously created backup. This needed to be overridden due to linked components which actually have a parameter value of another ARMI component. """ linkedDims = self._getLinkedDimsAndValues() composites.Composite.restoreBackup(self, paramsToApply) self._restoreLinkedDims(linkedDims)
[docs] def _getLinkedDimsAndValues(self): linkedDims = [] for dimName in self.DIMENSION_NAMES: # backUp and restore are called in tight loops, getting the value and # checking here is faster than calling self.dimensionIsLinked because that # requires and extra p.__getitem__ try: val = self.p[dimName] except: raise RuntimeError( "Could not find parameter {} defined for {}. Is the desired " "Component class?".format(dimName, self) ) if isinstance(val, _DimensionLink): linkedDims.append((self.p.paramDefs[dimName].fieldName, val)) del self.p[dimName] return linkedDims
[docs] def _restoreLinkedDims(self, linkedDims): # force update without setting the ".assigned" flag for fieldName, value in linkedDims: setattr(self.p, fieldName, value)
[docs] def adjustMassEnrichment(self, massFraction): """ Change the mass fraction of this component. The nuclides to adjust are defined by the material. This changes whichever nuclides are to be enriched vs. the baseline nuclides of that element while holding mass constant. For example it might adjust boron or uranium enrichment. Conceptually, you could hold number of atoms, volume, or mass constant during this operation. Historically ARMI adjusted mass fractions which was meant to keep mass constant. If you have 20 mass % Uranium and adjust the enrichment, you will still have 20% Uranium mass. But, the actual mass actually might change a bit because the enriched nuclide weighs less. See Also -------- Material.enrichedNuclide """ if self.material.enrichedNuclide is None: raise ValueError( "Cannot adjust enrichment of {} because `enrichedNuclide` is not defined." "".format(self.material) ) enrichedNuclide = nuclideBases.byName[self.material.enrichedNuclide] baselineNucNames = [nb.name for nb in enrichedNuclide.element.nuclideBases] massFracsBefore = self.getMassFracs() massFracEnrichedElement = sum( massFrac for nucName, massFrac in massFracsBefore.items() if nucName in baselineNucNames ) adjustedMassFracs = { self.material.enrichedNuclide: massFracEnrichedElement * massFraction } baselineNucNames.remove(self.material.enrichedNuclide) massFracTotalUnenriched = ( massFracEnrichedElement - massFracsBefore[self.material.enrichedNuclide] ) for baseNucName in baselineNucNames: # maintain relative mass fractions of baseline nuclides. frac = massFracsBefore.get(baseNucName, 0.0) / massFracTotalUnenriched if not frac: continue adjustedMassFracs[baseNucName] = ( massFracEnrichedElement * (1 - massFraction) * frac ) self.setMassFracs(adjustedMassFracs)
[docs] def getIntegratedMgFlux(self, adjoint=False, gamma=False): """ Return the multigroup neutron tracklength in [n-cm/s] The first entry is the first energy group (fastest neutrons). Each additional group is the next energy group, as set in the ISOTXS library. Parameters ---------- adjoint : bool, optional Return adjoint flux instead of real gamma : bool, optional Whether to return the neutron flux or the gamma flux. Returns ------- integratedFlux : multigroup neutron tracklength in [n-cm/s] """ if self.p.pinNum is None: # no pin-level flux is available if not self.parent: return numpy.zeros(1) volumeFraction = self.getVolume() / self.parent.getVolume() return volumeFraction * self.parent.getIntegratedMgFlux(adjoint, gamma) # pin-level flux is available. Note that it is NOT integrated on the param level. if gamma: if adjoint: raise ValueError("Adjoint gamma flux is currently unsupported.") else: pinFluxes = self.parent.p.pinMgFluxesGamma else: if adjoint: pinFluxes = self.parent.p.pinMgFluxesAdj else: pinFluxes = self.parent.p.pinMgFluxes return pinFluxes[self.p.pinNum - 1] * self.getVolume()
[docs] def density(self): """Returns the mass density of the object in g/cc.""" density = composites.Composite.density(self) if not density: # possible that there are no nuclides in this component yet. In that case, defer to Material. density = self.material.density(Tc=self.temperatureInC) return density
[docs] def getLumpedFissionProductCollection(self): """ Get collection of LFP objects. Will work for global or block-level LFP models. Returns ------- lfps : LumpedFissionProduct lfpName keys , lfp object values See Also -------- armi.physics.neutronics.fissionProductModel.lumpedFissionProduct.LumpedFissionProduct : LFP object """ if self.parent: return self.parent.getLumpedFissionProductCollection() else: return composites.ArmiObject.getLumpedFissionProductCollection(self)
[docs] def getReactionRates(self, nucName, nDensity=None): """ Parameters ---------- nucName - str nuclide name -- e.g. 'U235' nDensity - float number Density Returns ------- rxnRates - dict dictionary of reaction rates (rxn/s) for nG, nF, n2n, nA and nP Note ---- if you set nDensity to 1/CM2_PER_BARN this makes 1 group cross section generation easier """ if nDensity is None: nDensity = self.getNumberDensity(nucName) try: return getReactionRateDict( nucName, self.getAncestorWithFlags(Flags.CORE).lib, self.parent.p.xsType, self.getIntegratedMgFlux(), nDensity, ) except (AttributeError, KeyError): # AttributeError because there was no library because no parent.r -- this is a armiObject without flux so # send it some zeros # KeyError because nucName was not in the library return {"nG": 0, "nF": 0, "n2n": 0, "nA": 0, "nP": 0}
[docs] def getMicroSuffix(self): return self.parent.getMicroSuffix()
[docs] def getPitchData(self): """ Return the pitch data that should be used to determine block pitch. Notes ----- This pitch data should only be used if this is the pitch defining component in a block. The block is responsible for determining which component in it is the pitch defining component. """ raise NotImplementedError( f"Method not implemented on component {self}. " "Please implement if this component type can be a pitch defining component." )
[docs]class ShapedComponent(Component): """A component with well-defined dimensions.""" pass
[docs]def getReactionRateDict(nucName, lib, xsType, mgFlux, nDens): """ Parameters ---------- nucName - str nuclide name -- e.g. 'U235' lib - isotxs cross section library xsType - str cross section type -- e.g. - 'A' mgFlux - numpy.nArray integrated mgFlux (n-cm/s) nDens - float number density (at/bn-cm) Returns ------- rxnRates - dict dictionary of reaction rates (rxn/s) for nG, nF, n2n, nA and nP Note ---- assume there is no n3n cross section in ISOTXS """ key = "{}{}A".format(nucName, xsType) libNuc = lib[key] rxnRates = {"n3n": 0} for rxName, mgXSs in [ ("nG", libNuc.micros.nGamma), ("nF", libNuc.micros.fission), ("n2n", libNuc.micros.n2n), ("nA", libNuc.micros.nalph), ("nP", libNuc.micros.np), ]: rxnRates[rxName] = nDens * sum(mgXSs * mgFlux) return rxnRates