text: # 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.
  
  from typing import Tuple, List, Dict
  
  from armi.nucDirectory import nucDir, nuclideBases, elements
  from armi.utils import units
  from armi import runLog
  
  
  def getNDensFromMasses(rho, massFracs, normalize=False):
      """
      Convert density (g/cc) and massFracs vector into a number densities vector (#/bn-cm).
  
      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
  
  
  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
  
  
  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
  
  
  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)
          )
  
  
  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
  
  
  def formatMaterialCard(
      densities,
      matNum=0,
      minDens=1e-15,
      sigFigs=8,
      mcnp6Compatible=False,
      mcnpLibrary=None,
  ):
      """
      Formats nuclides and densities into a MCNP material card.
  
      Parameters
      ----------
      densities : dict
          number densities indexed by nuclideBase
  
      matNum : int
          mcnp material number
  
      minDens : float
          minimum density
  
      sigFigs : int
          significant figures for the material card
  
      Returns
      -------
      mCard : list
          list of material card strings
      """
      if all(
          isinstance(nuc, (nuclideBases.LumpNuclideBase, nuclideBases.DummyNuclideBase))
          for nuc in densities
      ):
          return []  # no valid nuclides to write
      mCard = ["m{matNum}\n".format(matNum=matNum)]
      for nuc, dens in sorted(densities.items()):
          # skip LFPs and Dummies.
          if isinstance(nuc, (nuclideBases.LumpNuclideBase)):
              runLog.important(
                  "The material card returned will ignore LFPs.", single=True
              )
              continue
          elif isinstance(nuc, nuclideBases.DummyNuclideBase):
              runLog.info("Omitting dummy nuclides such as {}".format(nuc), single=True)
              continue
          mcnpNucName = nuc.getMcnpId()
          newEntry = ("      {nucName:5d} {ndens:." + str(sigFigs) + "e}\n").format(
              nucName=int(mcnpNucName), ndens=max(dens, minDens)
          )  # 0 dens is invalid
          mCard.append(newEntry)
  
      if mcnp6Compatible:
          mCard.append("      nlib={lib}c\n".format(lib=mcnpLibrary))
      return mCard
  
  
  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
  
  
  def normalizeNuclideList(nuclideVector, normalization=1.0):
      """
      normalize the nuclide vector.
  
      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
  
  
  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.
  
      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))
              )
  
  
  def expandElementalNuclideMassFracs(
      element: elements.Element,
      massFrac: dict,
      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
  
  
  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
  
  
  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.p.massFrac.values())
      hm = 0.0
      for nucName, massFrac in material.p.massFrac.items():
          nb = nuclideBases.byName[nucName]
          if nb.isHeavyMetal():
              hm += massFrac
      hmFrac = hm / total
      hmEnrich = material.p.class1_wt_frac
      for nucName in (
          set(enrichedMassFracs.keys())
          .union(set(fertileMassFracs.keys()))
          .union(set(material.p.massFrac.keys()))
      ):
          nb = nuclideBases.byName[nucName]
          if nb.isHeavyMetal():
              material.p.massFrac[nucName] = hmFrac * (
                  hmEnrich * enrichedMassFracs.get(nucName, 0.0)
                  + (1 - hmEnrich) * fertileMassFracs.get(nucName, 0.0)
              )
  