# 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.
"""Assorted utilities to help with basic density calculations."""
from typing import Tuple, List, Dict
from armi.nucDirectory import nucDir, nuclideBases, elements
from armi.utils import units
from armi import runLog
[docs]def getNDensFromMasses(rho, massFracs, normalize=False):
"""
Convert density (g/cc) and massFracs vector into a number densities vector (#/bn-cm).
.. impl:: Number densities are retrievable from masses.
:id: I_ARMI_UTIL_MASS2N_DENS
:implements: R_ARMI_UTIL_MASS2N_DENS
Loops over all provided nuclides (given as keys in the ``massFracs`` vector) and calculates
number densities of each, at a given material ``density``. Mass fractions can be provided
either as normalized to 1, or as unnormalized with subsequent normalization calling
``normalizeNuclideList`` via the ``normalize`` flag.
Parameters
----------
rho : float
density in (g/cc)
massFracs : dict
vector of mass fractions -- normalized to 1 -- keyed by their nuclide name
Returns
-------
numberDensities : dict
vector of number densities (#/bn-cm) keyed by their nuclide name
"""
if normalize:
massFracs = normalizeNuclideList(massFracs, normalization=normalize)
numberDensities = {}
rho = rho * units.MOLES_PER_CC_TO_ATOMS_PER_BARN_CM
for nucName, massFrac in massFracs.items():
atomicWeight = nuclideBases.byName[nucName].weight
numberDensities[nucName] = massFrac * rho / atomicWeight
return numberDensities
[docs]def getMassFractions(numberDensities):
"""
Convert number densities (#/bn-cm) into mass fractions.
Parameters
----------
numberDensities : dict
number densities (#/bn-cm) keyed by their nuclide name
Returns
-------
massFracs : dict
mass fractions -- normalized to 1 -- keyed by their nuclide
name
"""
nucMassFracs = {}
totalWeight = 0.0
for nucName, numDensity in numberDensities.items():
weightI = numDensity * nucDir.getAtomicWeight(nucName)
nucMassFracs[nucName] = weightI # will be normalized at end
totalWeight += weightI
if totalWeight != 0:
for nucName in numberDensities:
nucMassFracs[nucName] /= totalWeight
else:
for nucName in numberDensities:
nucMassFracs[nucName] = 0.0
return nucMassFracs
[docs]def calculateMassDensity(numberDensities):
"""
Calculates the mass density.
Parameters
----------
numberDensities : dict
vector of number densities (atom/bn-cm) indexed by nuclides names
Returns
-------
rho : float
density in (g/cc)
"""
rho = 0
for nucName, nDensity in numberDensities.items():
atomicWeight = nuclideBases.byName[nucName].weight
rho += nDensity * atomicWeight / units.MOLES_PER_CC_TO_ATOMS_PER_BARN_CM
return rho
[docs]def calculateNumberDensity(nucName, mass, volume):
"""
Calculates the number density.
Parameters
----------
mass : float
volume : volume
nucName : armi nuclide name -- e.g. 'U235'
Returns
-------
number density : float
number density (#/bn-cm)
See Also
--------
armi.reactor.blocks.Block.setMass
"""
A = nucDir.getAtomicWeight(nucName)
try:
return units.MOLES_PER_CC_TO_ATOMS_PER_BARN_CM * mass / (volume * A)
except ZeroDivisionError:
if mass == 0 and volume == 0:
return 0
raise ValueError(
"Could not calculate number density with input.\n"
"mass : {}\nvolume : {}\natomic weight : {}\n".format(mass, volume, A)
)
[docs]def getMassInGrams(nucName, volume, numberDensity=None):
"""
Gets mass of a nuclide of a known volume and know number density.
Parameters
----------
nucName : str
name of nuclide -- e.g. 'U235'
volume : float
volume in (cm3)
numberDensity : float
number density in (at/bn-cm)
Returns
-------
mass : float
mass of nuclide (g)
"""
if not numberDensity:
return 0.0
A = nucDir.getAtomicWeight(nucName)
return numberDensity * volume * A / units.MOLES_PER_CC_TO_ATOMS_PER_BARN_CM
[docs]def filterNuclideList(nuclideVector, nuclides):
"""
Filter out nuclides not in the nuclide vector.
Parameters
----------
nuclideVector : dict
dictionary of values indexed by nuclide identifiers -- e.g. nucNames or nuclideBases
nuclides : list
list of nuclide identifiers
Returns
-------
nuclideVector : dict
dictionary of values indexed by nuclide identifiers -- e.g. nucNames or nuclideBases
"""
if not isinstance(list(nuclideVector.keys())[0], nuclides[0].__class__):
raise ValueError(
"nuclide vector is indexed by {} where as the nuclides list is {}".format(
nuclideVector.keys()[0].__class__, nuclides[0].__class__
)
)
for nucName in list(nuclideVector.keys()):
if nucName not in nuclides:
del nuclideVector[nucName]
return nuclideVector
[docs]def normalizeNuclideList(nuclideVector, normalization=1.0):
"""
Normalize the nuclide vector.
.. impl:: Normalize nuclide vector.
:id: I_ARMI_UTIL_DENS_TOOLS
:implements: R_ARMI_UTIL_DENS_TOOLS
Given a vector of nuclides ``nuclideVector`` indexed by nuclide identifiers (``nucNames`` or ``nuclideBases``),
normalizes to the provided ``normalization`` value.
Parameters
----------
nuclideVector : dict
dictionary of values -- e.g. floats, ints -- indexed by nuclide identifiers -- e.g. nucNames or nuclideBases
normalization : float
Returns
-------
nuclideVector : dict
dictionary of values indexed by nuclide identifiers -- e.g. nucNames or nuclideBases
"""
normalizationFactor = sum(nuclideVector.values()) / normalization
for nucName, mFrac in nuclideVector.items():
nuclideVector[nucName] = mFrac / normalizationFactor
return nuclideVector
[docs]def expandElementalMassFracsToNuclides(
massFracs: dict,
elementExpansionPairs: Tuple[elements.Element, List[nuclideBases.NuclideBase]],
):
"""
Expand elemental mass fractions to natural nuclides.
Modifies the input ``massFracs`` in place to contain nuclides.
Notes
-----
This indirectly updates number densities through mass fractions.
.. impl:: Expand mass fractions to nuclides.
:id: I_ARMI_UTIL_EXP_MASS_FRACS
:implements: R_ARMI_UTIL_EXP_MASS_FRACS
Given a vector of elements and nuclides with associated mass fractions (``massFracs``),
expands the elements in-place into a set of nuclides using
``expandElementalNuclideMassFracs``. Isotopes to expand into are provided for each element
by specifying them with ``elementExpansionPairs``, which maps each element to a list of
particular NuclideBases; if left unspecified, all naturally-occurring isotopes are included.
Explicitly specifying the expansion isotopes provides a way for particular
naturally-occurring isotopes to be excluded from the expansion, e.g. excluding O-18 from an
expansion of elemental oxygen.
Parameters
----------
massFracs : dict(str, float)
dictionary of nuclide or element names with mass fractions. Elements will be expanded in
place using natural isotopics.
elementExpansionPairs : (Element, [NuclideBase]) pairs
element objects to expand (from nuclidBase.element) and list of NuclideBases to expand into
(or None for all natural)
"""
# expand elements
for element, isotopicSubset in elementExpansionPairs:
massFrac = massFracs.pop(element.symbol, None)
if massFrac is None:
continue
expandedNucs = expandElementalNuclideMassFracs(
element, massFrac, isotopicSubset
)
massFracs.update(expandedNucs)
total = sum(expandedNucs.values())
if massFrac > 0.0 and abs(total - massFrac) / massFrac > 1e-6:
raise ValueError(
"Mass fractions not normalized properly {}!".format((total, massFrac))
)
[docs]def expandElementalNuclideMassFracs(
element: elements.Element,
massFrac: float,
isotopicSubset: List[nuclideBases.NuclideBase] = None,
):
"""
Return a dictionary of nuclide names to isotopic mass fractions.
If an isotopic subset is passed in, the mass fractions get scaled up
s.t. the total mass fraction remains constant.
Parameters
----------
element : Element
The element to expand to natural isotopics
massFrac : float
Mass fraction of the initial element
isotopicSubset : list of NuclideBases
Natural isotopes to include in the expansion. Useful e.g. for
excluding O18 from an expansion of Oxygen.
"""
elementNucBases = element.getNaturalIsotopics()
if isotopicSubset:
expandedNucBases = [nb for nb in elementNucBases if nb in isotopicSubset]
else:
expandedNucBases = elementNucBases
elementalWeightGperMole = sum(nb.weight * nb.abundance for nb in expandedNucBases)
if not any(expandedNucBases):
raise ValueError(
"Cannot expand element `{}` into isotopes: `{}`"
"".format(element, expandedNucBases)
)
expanded = {}
for nb in expandedNucBases:
expanded[nb.name] = (
massFrac * nb.abundance * nb.weight / elementalWeightGperMole
)
return expanded
[docs]def getChemicals(nuclideInventory):
"""
Groups the inventories of nuclides by their elements.
Parameters
----------
nuclideInventory : dict
nuclide inventories indexed by nuc -- either nucNames or nuclideBases
Returns
-------
chemicals : dict
inventory of elements indexed by element symbol -- e.g. 'U' or 'PU'
"""
chemicals = {}
for nuc, N in nuclideInventory.items():
nb = nuc if isinstance(nuc, nuclideBases.INuclide) else nuclideBases.byName[nuc]
if nb.element.symbol in chemicals:
chemicals[nb.element.symbol] += N
else:
chemicals[nb.element.symbol] = N
return chemicals
[docs]def applyIsotopicsMix(
material, enrichedMassFracs: Dict[str, float], fertileMassFracs: Dict[str, float]
):
"""
Update material heavy metal mass fractions based on its enrichment and two nuclide feeds.
This will remix the heavy metal in a Material object based on the object's
``class1_wt_frac`` parameter and the input nuclide information.
This can be used for inputting mixtures of two external custom isotopic feeds
as well as for fabricating assemblies from two closed-cycle collections
of material.
See Also
--------
armi.materials.material.FuelMaterial
Parameters
----------
material : material.Material
The object to modify. Must have a ``class1_wt_frac`` param set
enrichedMassFracs : dict
Nuclide names and weight fractions of the class 1 nuclides
fertileMassFracs : dict
Nuclide names and weight fractions of the class 2 nuclides
"""
total = sum(material.massFrac.values())
hm = 0.0
for nucName, massFrac in material.massFrac.items():
nb = nuclideBases.byName[nucName]
if nb.isHeavyMetal():
hm += massFrac
hmFrac = hm / total
hmEnrich = material.class1_wt_frac
for nucName in (
set(enrichedMassFracs.keys())
.union(set(fertileMassFracs.keys()))
.union(set(material.massFrac.keys()))
):
nb = nuclideBases.byName[nucName]
if nb.isHeavyMetal():
material.massFrac[nucName] = hmFrac * (
hmEnrich * enrichedMassFracs.get(nucName, 0.0)
+ (1 - hmEnrich) * fertileMassFracs.get(nucName, 0.0)
)