Source code for armi.nuclearDataIO.xsCollections

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

"""
Cross section collections contain cross sections for a single nuclide or region.

Specifically, they are used as attributes of :py:class:`~armi.nuclearDataIO.xsNuclides.XSNuclide`, which
then are combined as a :py:class:`~armi.nuclearDataIO.xsLibraries.XSLibrary`.

These may represent microscopic or macroscopic neutron or photon cross sections. When they are macroscopic,
they generally represent a whole region with many nuclides, though this is not required.

See Also
--------
armi.nuclearDataIO.xsCollection.XSCollection : object that gets created.

Examples
--------
# creating a MicroscopicXSCollection by loading one from ISOTXS.
microLib = armi.nuclearDataIO.ISOTXS('ISOTXS')
micros = myLib.nuclides['U235AA'].micros

# creating macroscopic XS:
mc = MacroscopicCrossSectionCreator()
macroCollection = mc.createMacrosFromMicros(microLib, block)
blocksWithMacros = mc.createMacrosOnBlocklist(microLib, blocks)

"""
import numpy
from scipy import sparse

from armi import runLog
from armi.utils import properties
from armi.utils import units

# Basic cross-section types that are represented by a 1-D vector in the multigroup approximation
# No one is particularly proud of these names...we can claim
# they have some origin in the ISOTXS file format card 04 definition
# fmt: off
NGAMMA = "nGamma"      # radiative capture
NAPLHA = "nalph"       # (n, alpha)
NP = "np"              # (n, proton)
ND = "nd"              # (n, deuteron)
NT = "nt"              # (n, triton)
FISSION_XS = "fission" # (n, fission)
N2N_XS = "n2n"         # (n,2n)
NUSIGF = "nuSigF"      
NU = "neutronsPerFission"
# fmt: on
CAPTURE_XS = [NGAMMA, NAPLHA, NP, ND, NT]

# Cross section types that are represented by 2-D matrices in the multigroup approximation
BASIC_SCAT_MATRIX = ["elasticScatter", "inelasticScatter", "n2nScatter"]
OTHER_SCAT_MATRIX = ["totalScatter", "elasticScatter1stOrder"]
HIGHORDER_SCATTER = "higherOrderScatter"

# Subset of vector xs used to evaluate absorption cross-section
ABSORPTION_XS = CAPTURE_XS + [FISSION_XS, N2N_XS]

# Subset of vector xs evaluated by _convertBasicXS
BASIC_XS = ABSORPTION_XS + [NUSIGF]

# Subset vector xs that are derived from basic cross sections
DERIVED_XS = ["absorption", "removal"]

# Total and transport are treated differently since they are 2D (can have multiple moments)
TOTAL_XS = ["total", "transport"]

# Subset of all basic cross sections that include removal and scattering
ALL_XS = BASIC_XS + BASIC_SCAT_MATRIX + OTHER_SCAT_MATRIX + DERIVED_XS + TOTAL_XS

# All xs collection data
ALL_COLLECTION_DATA = ALL_XS + [
    "chi",
    NU,
    "strpd",
    HIGHORDER_SCATTER,
    "diffusionConstants",
]

E_CAPTURE = "ecapt"
E_FISSION = "efiss"


[docs]class XSCollection: """A cross section collection.""" _zeroes = {} """ A dict of numpy arrays set to the size of XSLibrary.numGroups. This is used to initialize cross sections which may not exist for the specific nuclide. Consequently, there should never be a situation where a cross section does not exist. In addition, they are all pointers to the same array, so we're not generating too much unnecessary data. Notes ----- This is a dict so that it can store multiple 0_g "matricies", i.e. vectors. Realistically, during any given run there will only be a set of groups, e.g. 33. """
[docs] @classmethod def getDefaultXs(cls, numGroups): default = cls._zeroes.get(numGroups, None) if default is None: default = numpy.zeros(numGroups) cls._zeroes[numGroups] = default return default
def __init__(self, parent): """ Construct a NuclideCollection. Parameters ---------- parent : object The parent container, which may be a region, a nuclide, a block, etc. """ self.numGroups = None self.transport = None self.total = None self.nGamma = None self.fission = None self.neutronsPerFission = None self.chi = None self.nalph = None self.np = None self.n2n = None self.nd = None self.nt = None self.strpd = None self.elasticScatter = None self.inelasticScatter = None self.n2nScatter = None self.elasticScatter1stOrder = None self.totalScatter = None self.absorption = None self.diffusionConstants = None self.removal = None self.nuSigF = None self.higherOrderScatter = {} self.source = "{}".format(parent) def __getitem__(self, key): """ Access cross sections by key string (e.g. micros['fission'] = micros.fission. Notes ----- These containers were originally dicts, but upgraded to objects with numpy values as specialization was needed. This access method could/should be phased out. """ return self.__dict__[key] def __setitem__(self, key, value): self.__dict__[key] = value
[docs] def get(self, key, default): try: return self[key] except (IndexError, KeyError, TypeError): return default
[docs] def getAbsorptionXS(self): """Return total absorption XS, which is the sum of capture + fission + others.""" absXS = [ self.nGamma, self.fission, self.nalph, self.np, self.nd, self.nt, self.n2n, ] return absXS
[docs] def getTotalScatterMatrix(self): """ Sum up scatter matrices to produce total scatter matrix. Multiply reaction-based n2n scatter matrix by 2.0 to convert to production-based. .. warning:: Not all lattice codes store (n,2n) matrices consistently. Some are production-based and some are absorption-based. If you use an absorption-based one, your scatter matrix will be off, generally leading to about a percent error in your neutron balance. Notes ----- The total scattering matrix is produced by summing the elastic, inelastic, and n2n scattering matrices. If a specific scattering matrix does not exist for a composition (nuclide or region) then it is skipped and a warning is displayed stating that the scattering reaction is not available and is not included in the total scattering matrix. Example: When producing macroscopic cross sections in MC2-3 the code internally merges the elastic and inelastic scattering matrices into a single elastic scattering matrix. """ scatters = [] totalScatterComponents = { "elastic": self.elasticScatter, "inelastic": self.inelasticScatter, "n2n": self.n2nScatter * 2.0, } for sType, sMatrix in totalScatterComponents.items(): if sMatrix is not None: scatters.append(sMatrix) else: runLog.warning( "{} scattering matrix in {} is not defined. Generating total scattering matrix" " without this data".format(sType.title(), self), single=True, ) return sum(scatters)
[docs] def clear(self): """Zero out all the cross sections; this is useful for creating dummy cross sections.""" for xsAttr in ALL_XS: value = getattr(self, xsAttr) # it should either be a list, a numpy array, or a sparse matrix if isinstance(value, list): value = [0.0] * len(value) elif isinstance(value, numpy.ndarray): value = numpy.zeros(value.shape) elif value is None: # assume it is scipy.sparse pass elif value.nnz >= 0: value = sparse.csr_matrix(value.shape) setattr(self, xsAttr, value) # need to do the same thing for the higherOrderScatter for kk, currentMatrix in self.higherOrderScatter.items(): self.higherOrderScatter[kk] = sparse.csr_matrix(currentMatrix.shape)
[docs] @staticmethod def collapseCrossSection(crossSection, weights): r""" Collapse a cross section into 1-group. This is extremely useful for many analyses such as doing a shielding efficacy survey or computing one-group reaction rates. .. math:: \bar{\sigma} = \frac{\sum_g{\sigma_g \phi_g}}{\sum_g{\phi_g}} Parameters ---------- crossSection : list Multigroup cross section values weights : list energy group weights to apply (usually the multigroup flux) Returns ------- oneGroupXS : float The one group cross section in the same units as the input cross section. """ mult = numpy.array(crossSection) * numpy.array(weights) return sum(mult) / sum(weights)
[docs] def compare(self, other, flux, relativeTolerance=0, verbose=False): """Compare the cross sections between two XSCollections objects.""" equal = True for xsName in ALL_COLLECTION_DATA: myXsData = self.__dict__[xsName] theirXsData = other.__dict__[xsName] if xsName == HIGHORDER_SCATTER: for actualList, expectedList in zip(myXsData, theirXsData): if actualList != expectedList: equal = False runLog.important( " {} {:<30} cross section is different.".format( self.source, xsName ) ) elif sparse.issparse(myXsData) and sparse.issparse(theirXsData): if not numpy.allclose( myXsData.todense(), theirXsData.todense(), rtol=relativeTolerance, atol=0.0, ): verboseData = ( "" if not verbose else "\n{},\n\n{}".format(myXsData, theirXsData) ) runLog.important( " {} {:<30} cross section is different.{}".format( self.source, xsName, verboseData ) ) equal = False elif isinstance(myXsData, dict) and myXsData != theirXsData: # there are no dicts currently so code is untested raise NotImplementedError("there are no dicts") elif not properties.areEqual(myXsData, theirXsData, relativeTolerance): verboseData = ( "" if not verbose else "\n{},\n\n{}".format(myXsData, theirXsData) ) runLog.important( " {} {:<30} cross section is different.{}".format( self.source, xsName, verboseData ) ) equal = False return equal
[docs] def merge(self, other): """ Merge the cross sections of two collections. Notes ----- 1. This can only merge if one hasn't been assigned at all, because it doesn't try to figure out how to account for overlapping cross sections. 2. Update the current library (self) with values from the other library if all attributes in the library except ones in `attributesToIgnore` are None. 3. Libraries are already merged if all attributes in the other library are None (This is nothing to merge!). """ attributesToIgnore = ["source", HIGHORDER_SCATTER] if all( v is None for k, v in self.__dict__.items() if k not in attributesToIgnore ): self.__dict__.update(other.__dict__) # See note 2 elif all( v is None for k, v in other.__dict__.items() if k not in attributesToIgnore ): pass # See note 3 else: overlappingAttrs = set( k for k, v in self.__dict__.items() if v is not None and k != "source" ) overlappingAttrs &= set( k for k, v in other.__dict__.items() if v is not None and k != "source" ) raise AttributeError( "Cannot merge {} and {}.\n Cross sections overlap in " "attributes: {}.".format( self.source, other.source, ", ".join(overlappingAttrs) ) )
[docs]class MacroscopicCrossSectionCreator: """ Create macroscopic cross sections from micros and number density. Object encapsulating all high-level methods related to the creation of macroscopic cross sections. """ def __init__( self, buildScatterMatrix=True, buildOnlyCoolant=False, minimumNuclideDensity=0.0 ): self.densities = None self.macros = None self.micros = None self.minimumNuclideDensity = minimumNuclideDensity self.buildScatterMatrix = buildScatterMatrix self.buildOnlyCoolant = ( buildOnlyCoolant # TODO: this is not implemented yet. is it needed? ) self.block = None
[docs] def createMacrosOnBlocklist( self, microLibrary, blockList, nucNames=None, libType="micros" ): for block in blockList: block.macros = self.createMacrosFromMicros( microLibrary, block, nucNames, libType=libType ) return blockList
[docs] def createMacrosFromMicros( self, microLibrary, block, nucNames=None, libType="micros" ): """ Creates a macroscopic cross section set based on a microscopic XS library using a block object. Micro libraries have lots of nuclides, but macros only have 1. Parameters ---------- microLibrary : xsCollection.XSCollection Input micros block : Block Object whos number densities should be used to generate macros nucNames : list, optional List of nuclides to include in the macros. Defaults to all in block. libType : str, optional The block attribute containing the desired microscopic XS for this block: either "micros" for neutron XS or "gammaXS" for gamma XS. Returns ------- macros : xsCollection.XSCollection A new XSCollection full of macroscopic cross sections """ runLog.debug("Building macroscopic cross sections for {0}".format(block)) if nucNames is None: nucNames = block.getNuclides() self.microLibrary = microLibrary self.block = block self.xsSuffix = block.getMicroSuffix() self.macros = XSCollection(parent=block) self.densities = dict( filter( lambda x: x[1] > self.minimumNuclideDensity, zip(nucNames, block.getNuclideNumberDensities(nucNames)), ) ) self.ng = getattr(self.microLibrary, "numGroups" + _getLibTypeSuffix(libType)) self._initializeMacros() self._convertBasicXS(libType=libType) self._computeAbsorptionXS() self._convertScatterMatrices(libType=libType) self._computeDiffusionConstants() self._buildTotalScatterMatrix() self._computeRemovalXS() self.macros.chi = computeBlockAverageChi( b=self.block, isotxsLib=self.microLibrary ) return self.macros
def _initializeMacros(self): m = self.macros for xsName in BASIC_XS + DERIVED_XS: setattr(m, xsName, numpy.zeros(self.ng)) for matrixName in BASIC_SCAT_MATRIX: # lil_matrices are good for indexing but bad for certain math operations. # use csr for faster math setattr(m, matrixName, sparse.csr_matrix((self.ng, self.ng))) def _convertBasicXS(self, libType="micros"): """ Converts basic XS such as fission, nGamma, etc. Parameters ---------- libType : str, optional The block attribute containing the desired microscopic XS for this block: either "micros" for neutron XS or "gammaXS" for gamma XS. """ reactions = BASIC_XS + TOTAL_XS if NUSIGF in reactions: reactions.remove(NUSIGF) self.macros[NUSIGF] = computeMacroscopicGroupConstants( FISSION_XS, self.densities, self.microLibrary, self.xsSuffix, libType=libType, multConstant=NU, ) for reaction in reactions: self.macros[reaction] = computeMacroscopicGroupConstants( reaction, self.densities, self.microLibrary, self.xsSuffix, libType=libType, ) def _convertScatterMatrices(self, libType="micros"): """ Build macroscopic scatter matrices. Parameters ---------- libType : str, optional The block attribute containing the desired microscopic XS for this block: either "micros" for neutron XS or "gammaXS" for gamma XS. """ if not self.buildScatterMatrix: return for nuclide in self.microLibrary.getNuclides(self.xsSuffix): microCollection = getattr(nuclide, libType) nDens = self.densities.get(nuclide.name, 0.0) if microCollection.elasticScatter is not None: self.macros.elasticScatter += microCollection.elasticScatter * nDens if microCollection.inelasticScatter is not None: self.macros.inelasticScatter += microCollection.inelasticScatter * nDens if microCollection.n2nScatter is not None: self.macros.n2nScatter += microCollection.n2nScatter * nDens def _computeAbsorptionXS(self): """ Absorption = sum of all absorption reactions. Must be called after :py:meth:`_convertBasicXS`. """ for absXS in self.macros.getAbsorptionXS(): self.macros.absorption += absXS def _computeDiffusionConstants(self): self.macros.diffusionConstants = 1.0 / (3.0 * self.macros.transport) def _buildTotalScatterMatrix(self): self.macros.totalScatter = self.macros.getTotalScatterMatrix() def _computeRemovalXS(self): """ Compute removal cross section (things that remove a neutron from this phase space). This includes all absorptions and outscattering. Outscattering is represented by columns of the total scatter matrix. Self-scattering (e.g. when g' == g) is not be included. This can be handled by summing the columns and then subtracting the diagonal. within-group n2n is accounted for by simply not including n2n in the removal xs. """ self.macros.removal = self.macros.absorption - self.macros.n2n # columnSum = self.macros.totalScatter.columnSum(self.ng) # convert to ndarray columnSum = self.macros.totalScatter.sum(axis=0).getA1() # convert to ndarray # diags = self.macros.totalScatter.diagonal(self.ng) diags = self.macros.totalScatter.diagonal() self.macros.removal += columnSum - diags
[docs]def computeBlockAverageChi(b, isotxsLib): r""" Return the block average total chi vector based on isotope chi vectors. This is defined by eq 3.4b in DIF3D manual [DIF3D]_, which corresponds to 1 in A.HMG4C card. .. math:: \chi_g = \frac{\sum_{n} \chi_{g,n} N_n V \sum_{g'}(\nu_{g'}*\sigma_{f,g'})}{\sum_n N_n V \sum_{g'}(\nu_{g'}*\sigma_{f,g'} )} To evaluate efficiently, assume that if :math:`\chi_{g,n}=0`, there will be no contributions Volume is not used b/c it is already homogenized in the block. Parameters ---------- b : object Block object isotxsLib : object ISOTXS library object Notes ----- This methodology is based on option 1 in the HMG4C utility (named total fission source weighting). """ numGroups = isotxsLib.numGroups numerator = numpy.zeros(numGroups) denominator = 0.0 numberDensities = b.getNumberDensities() for nucObj in isotxsLib.getNuclides(b.getMicroSuffix()): nucMicroXS = nucObj.micros nucNDens = numberDensities.get(nucObj.name, 0.0) nuFissionTotal = sum(nucMicroXS.neutronsPerFission * nucMicroXS.fission) numerator += nucMicroXS.chi * nucNDens * nuFissionTotal denominator += nucNDens * nuFissionTotal if denominator != 0.0: return numerator / denominator else: return numpy.zeros(numGroups)
def _getLibTypeSuffix(libType): if libType == "micros": libTypeSuffix = "" elif libType == "gammaXS": libTypeSuffix = "Gamma" else: libTypeSuffix = None runLog.warning( "ARMI currently supports only micro XS libraries of types " '"micros" (neutron) and "gammaXS" (gamma).' ) return libTypeSuffix
[docs]def computeNeutronEnergyDepositionConstants(numberDensities, lib, microSuffix): """ Compute the macroscopic neutron energy deposition group constants. These group constants can be multiplied by the flux to obtain energy deposition rates. Parameters ---------- numberDensities : dict nucName keys, number density values (atoms/bn-cm) of all nuclides in the composite for which the macroscopic group constants are computed. See composite `getNuclideNumberDensities` method. lib : library object Microscopic cross section library. microSuffix : str Microscopic library suffix (e.g. 'AB') for this composite. See composite `getMicroSuffix` method. Returns ------- energyDepositionConsts : numpy array Neutron energy deposition group constants. (J/cm) Notes ----- PMATRX documentation says units will be eV/s when multiplied by flux but it's eV/s/cm^3. (eV/s/cm^3 = eV-bn * 1/cm^2/s * 1/bn-cm.) Converted here to obtain J/cm (eV-bn * 1/bn-cm * J / eV) """ return ( computeMacroscopicGroupConstants( "neutronHeating", numberDensities, lib, microSuffix ) * units.JOULES_PER_eV )
[docs]def computeGammaEnergyDepositionConstants(numberDensities, lib, microSuffix): """ Compute the macroscopic gamma energy deposition group constants. These group constants can be multiplied by the flux to obtain energy deposition rates. Parameters ---------- numberDensities : dict nucName keys, number density values (atoms/bn-cm) of all nuclides in the composite for which the macroscopic group constants are computed. See composite `getNuclideNumberDensities` method. lib : library object Microscopic cross section library. microSuffix : str Microscopic library suffix (e.g. 'AB') for this composite. See composite `getMicroSuffix` method. Returns ------- energyDepositionConsts : numpy array gamma energy deposition group constants. (J/cm) Notes ----- PMATRX documentation says units will be eV/s when multiplied by flux but it's eV/s/cm^3. (eV/s/cm^3 = eV-bn * 1/cm^2/s * 1/bn-cm.) Convert here to obtain J/cm (eV-bn * 1/bn-cm * J / eV) """ return ( computeMacroscopicGroupConstants( "gammaHeating", numberDensities, lib, microSuffix ) * units.JOULES_PER_eV )
[docs]def computeFissionEnergyGenerationConstants(numberDensities, lib, microSuffix): r""" Get the fission energy generation group constant of a block. .. math:: E_{generation_fission} = \kappa_f \Sigma_f Power comes from fission and capture reactions. Parameters ---------- numberDensities : dict nucName keys, number density values (atoms/bn-cm) of all nuclides in the composite for which the macroscopic group constants are computed. See composite `getNuclideNumberDensities` method. lib : library object Microscopic cross section library. microSuffix : str Microscopic library suffix (e.g. 'AB') for this composite. See composite `getMicroSuffix` method. Returns ------- fissionEnergyFactor: numpy.array Fission energy generation group constants (in Joules/cm) """ fissionEnergyFactor = computeMacroscopicGroupConstants( FISSION_XS, numberDensities, lib, microSuffix, libType="micros", multConstant=E_FISSION, ) return fissionEnergyFactor
[docs]def computeCaptureEnergyGenerationConstants(numberDensities, lib, microSuffix): r""" Get the energy generation group constant of a block. .. math:: E_{generation capture} = \kappa_c \Sigma_c Typically, one only cares about the flux* this XS (to find total power), but the XS itself is required in some sensitivity studies. Power comes from fission and capture reactions. Parameters ---------- numberDensities : dict nucName keys, number density values (atoms/bn-cm) of all nuclides in the composite for which the macroscopic group constants are computed. See composite `getNumberDensities` method. lib : library object Microscopic cross section library. microSuffix : str Microscopic library suffix (e.g. 'AB') for this composite. See composite `getMicroSuffix` method. Returns ------- captureEnergyFactor: numpy.array Capture energy generation group constants (in Joules/cm) """ captureEnergyFactor = None for xs in CAPTURE_XS: if captureEnergyFactor is None: captureEnergyFactor = numpy.zeros( numpy.shape( computeMacroscopicGroupConstants( xs, numberDensities, lib, microSuffix, libType="micros" ) ) ) captureEnergyFactor += computeMacroscopicGroupConstants( xs, numberDensities, lib, microSuffix, libType="micros", multConstant=E_CAPTURE, ) return captureEnergyFactor
[docs]def computeMacroscopicGroupConstants( constantName, numberDensities, lib, microSuffix, libType=None, multConstant=None, multLib=None, ): """ Compute any macroscopic group constants given number densities and a microscopic library. Parameters ---------- constantName : str Name of the reaction for which to obtain the group constants. This name should match a cross section name or an attribute in the collection. numberDensities : dict nucName keys, number density values (atoms/bn-cm) of all nuclides in the composite for which the macroscopic group constants are computed. See composite `getNuclideNumberDensities` method. lib : library object Microscopic cross section library. microSuffix : str Microscopic library suffix (e.g. 'AB') for this composite. See composite `getMicroSuffix` method. libType : str, optional The block attribute containing the desired microscopic XS for this block: either "micros" for neutron XS or "gammaXS" for gamma XS. multConstant : str, optional Name of constant by which the group constants will be multiplied. This name should match a cross section name or an attribute in the collection. multLib : library object, optional Microscopic cross section nuclide library to obtain the multiplier from. If None, same library as base cross section is used. Returns ------- macroGroupConstant : numpy array Macroscopic group constants for the requested reaction. """ skippedNuclides = [] skippedMultNuclides = [] macroGroupConstants = None # sort the numberDensities because a summation is being performed that may result in slight # differences based on the order. for nuclideName, numberDensity in sorted(numberDensities.items()): if not numberDensity: continue try: libNuclide = lib.getNuclide(nuclideName, microSuffix) multLibNuclide = libNuclide except KeyError: skippedNuclides.append(nuclideName) # Nuclide does not exist in the library continue if multLib: try: multLibNuclide = multLib.getNuclide(nuclideName, microSuffix) except KeyError: skippedMultNuclides.append( nuclideName ) # Nuclide does not exist in the library continue microGroupConstants = _getMicroGroupConstants( libNuclide, constantName, nuclideName, libType ) multiplierVal = _getXsMultiplier(multLibNuclide, multConstant, libType) if macroGroupConstants is None: macroGroupConstants = numpy.zeros(microGroupConstants.shape) if ( microGroupConstants.shape != macroGroupConstants.shape and not microGroupConstants.any() ): microGroupConstants = numpy.zeros(macroGroupConstants.shape) macroGroupConstants += ( numpy.asarray(numberDensity) * microGroupConstants * multiplierVal ) if skippedNuclides: msg = "The following nuclides are not in microscopic library {}: {}".format( lib, skippedNuclides ) runLog.error(msg, single=True) raise ValueError(msg) if skippedMultNuclides: runLog.debug( "The following nuclides are not in multiplier library {}: {}".format( multLib, skippedMultNuclides ), single=True, ) return macroGroupConstants
def _getXsMultiplier(libNuclide, multiplier, libType): if multiplier: try: microCollection = getattr(libNuclide, libType) multiplierVal = getattr(microCollection, multiplier) except: # noqa: bare-except multiplierVal = libNuclide.isotxsMetadata[multiplier] else: multiplierVal = 1.0 return numpy.asarray(multiplierVal) def _getMicroGroupConstants(libNuclide, constantName, nuclideName, libType): if libType: microCollection = getattr(libNuclide, libType) else: microCollection = libNuclide microGroupConstants = numpy.asarray(getattr(microCollection, constantName)) if not microGroupConstants.any(): runLog.debug( "Nuclide {} does not have {} microscopic group constants.".format( nuclideName, constantName ), single=True, ) return microGroupConstants