Source code for armi.physics.neutronics.fissionProductModel.lumpedFissionProduct

# 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.
"""
The lumped fission product (LFP)  module deals with representing LFPs and loading
them from files.

These are generally managed by the
:py:mod:`~armi.physics.neutronics.fissionProductModel.fissionProductModel.FissionProductModel`
"""
import os

from armi.nucDirectory import nuclideBases
from armi import runLog
from armi.nucDirectory import elements

from armi.physics.neutronics.fissionProductModel.fissionProductModelSettings import (
    CONF_LFP_COMPOSITION_FILE_PATH,
    CONF_FP_MODEL,
)


[docs]class LumpedFissionProduct: r""" Lumped fission product. The yields are in number fraction and they sum to 2.0 in general so a fission of an actinide results in one LFP, which represents 2 real FPs. This object is a data structure and works a lot like a dictionary in terms of accessing and modifying the data. The yields are indexed by nuclideBase -- in self.yld the yield fraction is indexed by nuclideBases of the individual fission product isotopes Examples -------- >>> fpd = FissionProductDefinitionFile(stream) >>> lfp = fpd.createSingleLFPFromFile('LFP39') >>> lfp[<nuclidebase for EU151>] 2.9773e-05 See Also -------- armi.reactor.blocks.Block.getLumpedFissionProductCollection : how you should access these. """ def __init__(self, name=None): """ Make an LFP. Parameters ---------- name : str, optional A name for the LFP. Will be overwritten if you load from file. Provide only if you are spinning your own custom LFPs. """ self.name = name self.yld = {}
[docs] def duplicate(self): """Make a copy of this w/o using deepcopy.""" new = self.__class__(self.name) for key, val in self.yld.items(): new.yld[key] = val return new
def __getitem__(self, fissionProduct): """ Return the yield of a particular fission product. This allows the LFP to be accessed via indexing, like this: ``lfp[fp]`` Returns ------- yld : yield of the fission product. """ return self.yld.get(fissionProduct, 0.0) def __setitem__(self, key, val): from armi.physics.neutronics.fissionProductModel.fissionProductModel import ( NUM_FISSION_PRODUCTS_PER_LFP, ) if val < 0.0: raise ValueError( f"Cannot set the yield of {key} in {self} to be " "less than zero as this is non-physical." ) if val > NUM_FISSION_PRODUCTS_PER_LFP: raise ValueError( f"Cannot set the yield of {key} in {self} to be " f"greater than {NUM_FISSION_PRODUCTS_PER_LFP}." ) self.yld[key] = val def __contains__(self, item): return item in self.yld def __repr__(self): return f"<Lumped Fission Product {self.name}>"
[docs] def keys(self): return self.yld.keys()
[docs] def values(self): return self.yld.values()
[docs] def items(self): for nuc in self.keys(): yield nuc, self[nuc]
[docs] def getGaseousYieldFraction(self): """Return the yield fraction of the gaseous nuclides.""" yld = 0.0 for nuc in self.keys(): if not isGas(nuc): continue yld += self[nuc] return yld
[docs] def getTotalYield(self): """ Get the fractional yield of all nuclides in this lumped fission product. Accounts for any fission gas that may be removed. Returns ------- total yield of all fps """ return sum([self[nuc] for nuc in self.yld])
[docs] def getMassFracs(self): """ Return a dictionary of mass fractions indexed by nuclide. Returns ------- massFracs : dict mass fractions (floats) of LFP masses """ massFracs = {} for nuc in self.keys(): massFracs[nuc] = self.getMassFrac(nuclideBase=nuc) return massFracs
[docs] def getMassFrac(self, nucName=None, nuclideBase=None): """ Return the mass fraction of the given nuclide. Returns ------- nuclide mass fraction (float) """ massFracDenom = self.getMassFracDenom() if not nuclideBase: nuclideBase = nuclideBases.byName[nucName] return self.__getitem__(nuclideBase) * (nuclideBase.weight / massFracDenom)
[docs] def getMassFracDenom(self): """ See Also -------- armi.physics.neutronics.fissionProductModel.lumpedFissionProduct.LumpedFissionProduct.getMassFrac """ massFracDenom = 0.0 for nuc in self.keys(): massFracDenom += self[nuc] * nuc.weight return massFracDenom
[docs]class LumpedFissionProductCollection(dict): """ A set of lumped fission products. Typically there would be one of these on a block or on a global level. """ def __init__(self): self.collapsible = False
[docs] def duplicate(self): new = self.__class__() for lfpName, lfp in self.items(): new[lfpName] = lfp.duplicate() return new
[docs] def getLumpedFissionProductNames(self): return self.keys()
[docs] def getAllFissionProductNames(self): """Gets names of all fission products in this collection.""" fpNames = set() for lfp in self.values(): for fp in lfp.keys(): fpNames.add(fp.name) return sorted(fpNames)
[docs] def getAllFissionProductNuclideBases(self): """Gets names of all fission products in this collection.""" nucs = set() for _lfpName, lfp in self.items(): for fp in lfp.keys(): nucs.add(fp) return sorted(nucs)
[docs] def getNumberDensities(self, objectWithParentDensities=None, densFunc=None): """ Gets all FP number densities in collection. Parameters ---------- objectWithParentDensities : ArmiObject object (probably block) that can be called with getNumberDensity('LFP35'), etc. to get densities of LFPs. densFunc : function, optional Optional method to extract LFP densities Returns ------- fpDensities : dict keys are fp names, vals are fission product number density in atoms/bn-cm. """ if not densFunc: densFunc = lambda lfpName: objectWithParentDensities.getNumberDensity( lfpName ) fpDensities = {} for lfpName, lfp in self.items(): lfpDens = densFunc(lfpName) for fp, fpFrac in lfp.items(): fpDensities[fp.name] = fpDensities.get(fp.name, 0.0) + fpFrac * lfpDens return fpDensities
[docs] def getMassFrac(self, oldMassFrac=None): """Returns the mass fraction vector of the collection of lumped fission products.""" if not oldMassFrac: raise ValueError("You must define a massFrac vector") massFrac = {} for lfpName, lfp in self.items(): lfpMFrac = oldMassFrac[lfpName] for nuc, mFrac in lfp.getMassFracs().items(): try: massFrac[nuc] += lfpMFrac * mFrac except KeyError: massFrac[nuc] = lfpMFrac * mFrac return massFrac
[docs]class FissionProductDefinitionFile: """ Reads a file that has definitions of one or more LFPs in it to produce LFPs. The format for this file is as follows:: LFP35 GE73 5.9000E-06 LFP35 GE74 1.4000E-05 LFP35 GE76 1.6000E-04 LFP35 AS75 8.9000E-05 and so on Examples -------- >>> fpd = FissionProductDefinitionFile(stream) >>> lfps = fpd.createLFPsFromFile() The path to this file is specified by the `lfpCompositionFilePath` user setting. """ def __init__(self, stream): self.stream = stream
[docs] def createLFPsFromFile(self): """ Read the file and create LFPs from the contents. Returns ------- lfps : list List of LumpedFissionProducts contained in the file """ lfps = LumpedFissionProductCollection() for lfpLines in self._splitIntoIndividualLFPLines(): lfp = self._readOneLFP(lfpLines) lfps[lfp.name] = lfp return lfps
[docs] def createSingleLFPFromFile(self, name): """Read one LFP from the file.""" lfpLines = self._splitIntoIndividualLFPLines(name) lfp = self._readOneLFP(lfpLines[0]) # only one LFP expected. Use it. return lfp
def _splitIntoIndividualLFPLines(self, lfpName=None): """ The lfp file can contain one or more LFPs. This splits them. Ignores DUMPs. Parameters ---------- lfpName : str, optional Restrict to just these names if desired. Returns ------- allLFPLines : list of list each entry is a list of lines that define one LFP """ lines = self.stream.readlines() allLFPLines = [] thisLFPLines = [] lastName = None for line in lines: name = line.split()[0] if "DUMP" in name or (lfpName and lfpName not in name): continue if lastName and name != lastName: allLFPLines.append(thisLFPLines) thisLFPLines = [] thisLFPLines.append(line) lastName = name if thisLFPLines: allLFPLines.append(thisLFPLines) return allLFPLines def _readOneLFP(self, linesOfOneLFP): lfp = LumpedFissionProduct() totalYield = 0.0 for line in linesOfOneLFP: data = line.split() parent = data[0] nucLibId = data[1] nuc = nuclideBases.byName[nucLibId] yld = float(data[2]) lfp.yld[nuc] = yld totalYield += yld lfp.name = parent # e.g. LFP38 runLog.debug( "Loaded {0} {1} nuclides for a total yield of {2}" "".format(len(lfp.yld), lfp.name, totalYield) ) return lfp
[docs]def lumpedFissionProductFactory(cs): """Build lumped fission products.""" if cs[CONF_FP_MODEL] == "explicitFissionProducts": return None if cs[CONF_FP_MODEL] == "MO99": return _buildMo99LumpedFissionProduct() lfpPath = cs[CONF_LFP_COMPOSITION_FILE_PATH] if not lfpPath or not os.path.exists(lfpPath): raise ValueError( "The fission product reference file does " f"not exist or is not a valid path. Path provided: {lfpPath}" ) runLog.extra(f"Loading global lumped fission products (LFPs) from {lfpPath}") with open(lfpPath) as lfpStream: lfpFile = FissionProductDefinitionFile(lfpStream) lfps = lfpFile.createLFPsFromFile() return lfps
def _buildMo99LumpedFissionProduct(): """ Build a dummy MO-99 LFP collection. This is a very bad FP approximation from a physics standpoint but can be very useful for rapid-running test cases. """ mo99 = nuclideBases.byName["MO99"] mo99LFPs = LumpedFissionProductCollection() for lfp in nuclideBases.where( lambda nb: isinstance(nb, nuclideBases.LumpNuclideBase) ): # Not all lump nuclides bases defined are fission products, so ensure that only fission # products are considered. if not ("FP" in lfp.name or "REGN" in lfp.name): continue mo99FP = LumpedFissionProduct(lfp.name) mo99FP[mo99] = 2.0 mo99LFPs[lfp.name] = mo99FP return mo99LFPs
[docs]def isGas(nuc): """True if nuclide is considered a gas.""" # ruff: noqa: SIM110 for element in elements.getElementsByChemicalPhase(elements.ChemicalPhase.GAS): if element == nuc.element: return True return False