Source code for terrapower.physics.neutronics.dragon.dragonWriter

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

"""
Write DRAGON inputs based on data contained in ARMI objects.

This uses templates and attempts to do most of the logic in code
to build the appropriate data structures. Then, the template engine
is responsible for transforming that data into the format required
for DRAGON to process it as input.

Classes here are intended to be specialized with more design-specific
subclasess in design-specific ARMI apps (or other clients).
"""

from typing import NamedTuple

from jinja2 import Template

from armi.physics.neutronics.energyGroups import GROUP_STRUCTURE
from armi.utils import units
from armi.nucDirectory import nuclideBases as nb
from armi.nucDirectory import thermalScattering as tsl
from armi import runLog
from armi.reactor.flags import Flags
from armi.physics.neutronics import energyGroups

N_CHARS_ALLOWED_IN_LIB_NAME = 8


# mapping between thermal scattering laws and DRAGON lib names.
# unfortunately this is library specific so that will need to be treated somehow.
DRAGON_TSL = {tsl.byNbAndCompound[nb.byName["C"], tsl.GRAPHITE_10P]: "C12_GR"}


[docs]class DragonWriter: """ Write a DRAGON input file using a template. This base class should strive to avoid any design-specific assumptions. """ def __init__(self, armiObjs, options): """ Initialize a writer. Parameters ---------- armiObjs : list The ARMI object(s) to process into template data. These represent the parts of a reactor you want to model. options : DragonOptions Data structure that contains execution and modeling controls """ self.armiObjs = armiObjs self.options = options def __str__(self): return f"<DragonWriter for {str(self.armiObjs)[:15]}...>"
[docs] def write(self): """ Write a DRAGON input file. """ runLog.info(f"Writing input with {self}") templateData = self._buildTemplateData() template = self._readTemplate() with open(self.options.inputFile, "w") as dragonInput: dragonInput.write(template.render(**templateData))
[docs] def _readTemplate(self): """Read the template file.""" with open(self.options.templatePath) as templateFormat: return Template(templateFormat.read())
[docs] def _buildTemplateData(self): """ Return data to be sent to the template to produce the DRAGON input. """ templateData = { "xsId": self.options.xsID, "nucData": self.options.libDataFileShort, "nucDataComment": self.options.libDataFile, # full path "groupStructure": self._makeGroupStructure(), } return templateData
[docs] def _makeGroupStructure(self): """ Make energy group structure bounds. DRAGON only needs group boundary values between 0 and the max. It assumes lowest boundary is 0 eV and the upper most boundary is the highest energy fine group boundary in the specified library. """ return GROUP_STRUCTURE[self.options.groupStructure][1:]
[docs]class DragonWriterHomogenized(DragonWriter): """ Write DRAGON inputs with homogenized compositions. This subclass assumes that the DRAGON case will represent one or more armi objects. The current implementation is capable of writing MIX cards for multiple compositions at once but does not yet have the ability to write geometry representation for anything beyond 0-D. """
[docs] def _buildTemplateData(self): templateData = DragonWriter._buildTemplateData(self) templateData.update( { "mixtures": self._makeMixtures(), "buckling": self.options.xsSettings.criticalBuckling, } ) return templateData
[docs] def _makeMixtures(self): """Make a DragonMixture from each object slated for inclusion in the input.""" return [ DragonMixture(obj, self.options, i) for i, obj in enumerate(self.armiObjs) ]
[docs]class MixtureNuclide(NamedTuple): """Data structure for a nuclide in a DRAGON mixture.""" armiName: str dragName: str xsid: str ndens: float selfShield: str
[docs]class DragonMixture: """ Data structure for a single mixture in Dragon. Each mixture can be associated with: * A temperature * A number density vector * A mapping between library names and internal nuclide names * A self-shielding vector """ def __init__(self, armiObj, options, index): self.armiObj = armiObj self.options = options self.index = index
[docs] def getTempInK(self): """ Return the mixture temperature in Kelvin. Notes ----- Only 1 temperature can be specified per mixture in DRAGON. For 0-D cases, the temperature of the fuel component is used for the entire mixture. For heterogeneous models, component temperature should be used. Component temperature may not work well yet for non BOL cases since .. warning:: The ARMI cross section group manager does not currently set the fuel component temperature to the average component temperatures when making a representative block. Thus, for the time being, fuel temperature of an arbitrary block in each representative block's parents will be obtained. """ avgNum = 0.0 avgDenom = 0.0 if any(self.armiObj.doChildrenHaveFlags(Flags.FUEL)): typeSpec = Flags.FUEL else: typeSpec = None for fuel in self.armiObj.iterComponents(typeSpec): vol = fuel.getArea() avgNum += fuel.temperatureInC * vol avgDenom += vol return units.getTk(Tc=avgNum / avgDenom)
[docs] def getMixVector(self): """ Generate mixture composition table. """ nucs = self.options.nuclides nucData = [] numberDensities = self.armiObj.getNuclideNumberDensities(nucs) thermalScatteringInfo = getNuclideThermalScatteringData(self.armiObj) if not any(numberDensities): # This is an empty armiObj. Can happen with zero-volume dummy components. # This writer's job is to accurately reflect the state of the reactor, # we simply return an empty set of number densities. return [] for nucName, nDens in zip(nucs, numberDensities): nuclideBase = nb.byName[nucName] if isinstance( nuclideBase, (nb.LumpNuclideBase, nb.DummyNuclideBase), ): # This skips lumped fission products. continue nucData.append( MixtureNuclide( armiName=nuclideBase.label, xsid=self.options.xsID, dragName=getDragLibNucID(nuclideBase, thermalScatteringInfo), ndens=nDens, selfShield=self.getSelfShieldingFlag(nuclideBase, nDens), ) ) return nucData
# pylint: disable=unused-argument
[docs] def getSelfShieldingFlag(self, nucBase, nDens) -> str: """ Get self shielding flag for a given nuclide. Figuring out how to structure resonant region index (inrs) requires some engineering judgment. Need index to make sure each mixture gets different fine-group flux. Flags self-sheilding if density is greater than a threshold or if it is a heavy metal """ if nucBase.isHeavyMetal() or self.armiObj.density() > 0.0001: return f"{self.index + 1}" return ""
[docs]def getDragLibNucID(nucBase, thermalScatteringInfo): """ Return the DRAGLIB isotope name for this nuclide. Parameters ---------- nucBase : NuclideBase The nuclide to get the DRAGLIB ID for. Notes ----- These IDs are compatible with DRAGLIB nuclear data format which is available: https://www.polymtl.ca/merlin/libraries.htm """ metastable = nucBase.state if nucBase in thermalScatteringInfo: # use DRAGON-specific thermal scattering names. These are unfortunately library-specific dragLibId = DRAGON_TSL[thermalScatteringInfo[nucBase]] else: # DRAGON is case sensitive on nuc names so lower().capitalize() matters. dragLibId = f"{nucBase.element.symbol.lower().capitalize()}{nucBase.a}" if metastable > 0: # Am242m, etc dragLibId += "m" return dragLibId
[docs]def getNuclideThermalScatteringData(armiObj): """ Make a mapping between nuclideBases in an armiObj and relevant thermal scattering laws. In some cases, a nuclide will be present both with a TSL and without (e.g. hydrogen in water and hydrogen in concrete in the same armiObj). While this could conceptually be handled somehow, we simply error out at this time. Notes ----- This code is copy/pasted originally from the test case in the framework. The code reviewer would not allow this to be put in the framework so we are forced to copy paste... Sorry. Returns ------- tslByNuclideBase : dict A dictionary with NuclideBase keys and ThermalScattering values Raises ------ RuntimeError When a armiObj has nuclides subject to more than one TSL, or subject to a TLS in one case and no TSL in another. Examples -------- >>> tslInfo = getNuclideThermalScatteringData(armiObj) >>> if nucBase in tslInfo: >>> aceLabel = tslInfo[nucBase].aceLabel """ tslByNuclideBase = {} freeNuclideBases = set() for c in armiObj.iterComponents(): nucs = {nb.byName[nn] for nn in c.getNuclides()} freeNucsHere = set() freeNucsHere.update(nucs) for tsl in c.material.thermalScatteringLaws: for subjectNb in tsl.getSubjectNuclideBases(): if subjectNb in nucs: if ( subjectNb in tslByNuclideBase and tslByNuclideBase[subjectNb] is not tsl ): raise RuntimeError( f"{subjectNb} in {armiObj} is subject to more than 1 different TSL: " f"{tsl} and {tslByNuclideBase[subjectNb]}" ) tslByNuclideBase[subjectNb] = tsl freeNucsHere.remove(subjectNb) freeNuclideBases.update(freeNucsHere) freeAndBound = freeNuclideBases.intersection(set(tslByNuclideBase.keys())) if freeAndBound: raise RuntimeError( f"{freeAndBound} is/are present in both bound and free forms in {armiObj}" ) return tslByNuclideBase