class GroupedComponent(yamlize.Object): """ A pointer to a component with a multiplicity to be used in a ComponentGroup. Multiplicity can be a fraction (e.g. to set volume fractions) """ name = yamlize.Attribute(type=str) mult = yamlize.Attribute(type=float)
class Triplet(yamlize.Object): """A x, y, z triplet for coordinates or lattice pitch.""" x = yamlize.Attribute(type=float) y = yamlize.Attribute(type=float, default=0.0) z = yamlize.Attribute(type=float, default=0.0) def __init__(self, x=0.0, y=0.0, z=0.0): self.x = x self.y = y self.z = z
class NuclideFlag(yamlize.Object): """ This class defines a nuclide options for use within the ARMI simulation, defining whether or not it should be included in the burn chain and cross sections. """ nuclideName = yamlize.Attribute(type=str) @nuclideName.validator def nuclideName(self, value): if value not in ALLOWED_KEYS: raise ValueError( "`{}` is not a valid nuclide name, must be one of: {}".format( value, ALLOWED_KEYS ) ) burn = yamlize.Attribute(type=bool) xs = yamlize.Attribute(type=bool) def __init__(self, nuclideName, burn, xs): # note: yamlize does not call an __init__ method, instead it uses __new__ and setattr self.nuclideName = nuclideName self.burn = burn self.xs = xs def __repr__(self): return "<NuclideFlag name:{} burn:{} xs:{}>".format( self.nuclideName, self.burn, self.xs ) def prepForCase(self, activeSet, inertSet, undefinedBurnChainActiveNuclides): """Take in the string nuclide or element name, try to expand it out to its bases correctly.""" actualNuclides = nucDir.getNuclidesFromInputName(self.nuclideName) for actualNuclide in actualNuclides: if self.burn: if not actualNuclide.trans and not actualNuclide.decays: undefinedBurnChainActiveNuclides.add(actualNuclide.name) activeSet.add(actualNuclide.name) if self.xs: inertSet.add(actualNuclide.name)
class AssemblyKeyedList(yamlize.KeyedList): """ Effectively and OrderedDict of assembly items, keyed on the assembly name. This uses yamlize KeyedList for YAML serialization. """ item_type = AssemblyBlueprint key_attr = AssemblyBlueprint.name heights = yamlize.Attribute(type=yamlize.FloatList, default=None) axialMeshPoints = yamlize.Attribute( key="axial mesh points", type=yamlize.IntList, default=None ) # note: yamlize does not call an __init__ method, instead it uses __new__ and setattr @property def bySpecifier(self): """Used by the reactor to _loadAssembliesIntoCore later, specifiers are two character strings""" return {aDesign.specifier: aDesign for aDesign in self}
class ComponentGroup(yamlize.KeyedList): """ A single component group containing multiple GroupedComponents Example ------- triso: kernel: mult: 0.7 buffer: mult: 0.3 """ group_name = yamlize.Attribute(type=str) key_attr = GroupedComponent.name item_type = GroupedComponent
class MaterialModifications(yamlize.Map): """ A yamlize map for reading and holding material modifications. A user may specify material modifications directly as keys/values on this class, in which case these material modifications will be blanket applied to the entire block. If the user wishes to specify material modifications specific to a component within the block, they should use the `by component` attribute, specifying the keys/values underneath the name of a specific component in the block. """ key_type = yamlize.Typed(str) value_type = yamlize.Sequence byComponent = yamlize.Attribute( key="by component", type=ByComponentModifications, default=ByComponentModifications(), )
class SystemBlueprint(yamlize.Object): """ The reactor-level structure input blueprint. .. note:: We use string keys to link grids to objects that use them. This differs from how blocks/assembies are specified, which use YAML anchors. YAML anchors have proven to be problematic and difficult to work with """ name = yamlize.Attribute(key="name", type=str) typ = yamlize.Attribute(key="type", type=str, default="core") gridName = yamlize.Attribute(key="grid name", type=str) origin = yamlize.Attribute(key="origin", type=Triplet, default=None) def __init__(self, name=None, gridName=None, origin=None): """ A Reactor Level Structure like a core or SFP. Notes ----- yamlize does not call an __init__ method, instead it uses __new__ and setattr this is only needed for when you want to make this object from a non-YAML source. """ self.name = name self.gridName = gridName self.origin = origin @staticmethod def _resolveSystemType(typ: str): # avoid circular import, though ideally reactors shouldnt depend on blueprints! from armi.reactor import reactors from armi.reactor import assemblyLists # TODO: This is far from the the perfect place for this; most likely plugins # will need to be able to extend it. Also, the concept of "system" will need to # be well-defined, and most systems will probably need their own, rather # dissimilar Blueprints. We may need to break with yamlize unfortunately. _map = {"core": reactors.Core, "sfp": assemblyLists.SpentFuelPool} try: cls = _map[typ] except KeyError: raise ValueError( "Could not determine an appropriate class for handling a " "system of type `{}`. Supported types are {}.".format(typ, _map.keys()) ) return cls def construct(self, cs, bp, reactor, geom=None): """Build a core/IVS/EVST/whatever and fill it with children.""" from armi.reactor import reactors # avoid circular import runLog.info("Constructing the `{}`".format(self.name)) # TODO: We should consider removing automatic geom file migration. if geom is not None and self.name == "core": gridDesign = geom.toGridBlueprints("core")[0] else: if not bp.gridDesigns: raise ValueError( "The input must define grids to construct a reactor, but " "does not. Update input." ) gridDesign = bp.gridDesigns.get(self.gridName, None) system = self._resolveSystemType(self.typ)(self.name) # TODO: This could be somewhere better. If system blueprints could be # subclassed, this could live in the CoreBlueprint. setOptionsFromCS() also isnt # great to begin with, so ideally it could be removed entirely. if isinstance(system, reactors.Core): system.setOptionsFromCs(cs) # Some systems may not require a prescribed grid design. Only try to use one if # it was provided if gridDesign is not None: spatialGrid = gridDesign.construct() system.spatialGrid = spatialGrid system.spatialGrid.armiObject = system reactor.add(system) # need parent before loading assemblies spatialLocator = grids.CoordinateLocation( self.origin.x, self.origin.y, self.origin.z, None ) system.spatialLocator = spatialLocator if armi.MPI_RANK != 0: # on non-master nodes we don't bother building up the assemblies # because they will be populated with DistributeState. return None # TODO: This is also pretty specific to Core-like things. We envision systems # with non-Core-like structure. Again, probably only doable with subclassing of # Blueprints self._loadAssemblies(cs, system, gridDesign, gridDesign.gridContents, bp) # TODO: This post-construction work is specific to Cores for now. We need to # generalize this. Things to consider: # - Should the Core be able to do geom modifications itself, since it already # has the grid constructed from the grid design? # - Should the summary be so specifically Material data? Should this be done for # non-Cores? Like geom modifications, could this just be done in processLoading? # Should it be invoked higher up, by whatever code is requesting the Reactor be # built from Blueprints? if isinstance(system, reactors.Core): summarizeMaterialData(system) self._modifyGeometry(system, gridDesign) system.processLoading(cs) return system # pylint: disable=no-self-use def _loadAssemblies(self, cs, container, gridDesign, gridContents, bp): runLog.header( "=========== Adding Assemblies to {} ===========".format(container) ) badLocations = set() for locationInfo, aTypeID in gridContents.items(): newAssembly = bp.constructAssem(cs, specifier=aTypeID) i, j = locationInfo loc = container.spatialGrid[i, j, 0] try: container.add(newAssembly, loc) except exceptions.SymmetryError: badLocations.add(loc) if badLocations: raise ValueError( "Geometry core map xml had assemblies outside the " "first third core, but had third core symmetry. \n" "Please update symmetry to be `full core` or " "remove assemblies outside the first third. \n" "The locations outside the first third are {}".format(badLocations) ) def _modifyGeometry(self, container, gridDesign): """Perform post-load geometry conversions like full core, edge assems.""" # all cases should have no edge assemblies. They are added ephemerally when needed from armi.reactor.converters import geometryConverters # circular imports runLog.header("=========== Applying Geometry Modifications ===========") converter = geometryConverters.EdgeAssemblyChanger() converter.removeEdgeAssemblies(container) # now update the spatial grid dimensions based on the populated children # (unless specified on input) if not gridDesign.latticeDimensions: runLog.info( "Updating spatial grid pitch data for {} geometry".format( container.geomType ) ) if container.geomType == geometry.HEX: container.spatialGrid.changePitch(container[0][0].getPitch()) elif container.geomType == geometry.CARTESIAN: xw, yw = container[0][0].getPitch() container.spatialGrid.changePitch(xw, yw)
class BlockBlueprint(yamlize.KeyedList): """Input definition for Block.""" item_type = componentBlueprint.ComponentBlueprint key_attr = componentBlueprint.ComponentBlueprint.name name = yamlize.Attribute(key="name", type=str) gridName = yamlize.Attribute(key="grid name", type=str, default=None) flags = yamlize.Attribute(type=str, default=None) _geomOptions = _configureGeomOptions() def _getBlockClass(self, outerComponent): """ Get the ARMI ``Block`` class for the specified outerComponent. Parameters ---------- outerComponent : Component Largest component in block. """ for compCls, blockCls in self._geomOptions.items(): if isinstance(outerComponent, compCls): return blockCls raise ValueError( "Block input for {} has outer component {} which is " " not a supported Block geometry subclass. Update geometry." "".format(self.name, outerComponent)) def construct(self, cs, blueprint, axialIndex, axialMeshPoints, height, xsType, materialInput): """ Construct an ARMI ``Block`` to be placed in an ``Assembly``. Parameters ---------- cs : CaseSettings CaseSettings object for the appropriate simulation. blueprint : Blueprints Blueprints object containing various detailed information, such as nuclides to model axialIndex : int The Axial index this block exists within the parent assembly axialMeshPoints : int number of mesh points for use in the neutronics kernel height : float initial height of the block xsType : str String representing the xsType of this block. materialInput : dict dict containing material modification names and values """ runLog.debug("Constructing block {}".format(self.name)) components = collections.OrderedDict() # build grid before components so you can load # the components into the grid. gridDesign = self._getGridDesign(blueprint) if gridDesign: spatialGrid = gridDesign.construct() else: spatialGrid = None for componentDesign in self: c = componentDesign.construct(blueprint, materialInput) components[c.name] = c if spatialGrid: componentLocators = gridDesign.getMultiLocator( spatialGrid, componentDesign.latticeIDs) if componentLocators: # this component is defined in the block grid # We can infer the multiplicity from the grid. # Otherwise it's a component that is in a block # with grids but that's not in the grid itself. c.spatialLocator = componentLocators mult = c.getDimension("mult") if mult and mult != 1.0 and mult != len(c.spatialLocator): raise ValueError( f"Conflicting ``mult`` input ({mult}) and number of " f"lattice positions ({len(c.spatialLocator)}) for {c}. " "Recommend leaving off ``mult`` input when using grids." ) elif not mult or mult == 1.0: # learn mult from grid definition c.setDimension("mult", len(c.spatialLocator)) for c in components.values(): c._resolveLinkedDims(components) boundingComp = sorted(components.values())[-1] # give a temporary name (will be updated by b.makeName as real blocks populate systems) b = self._getBlockClass(boundingComp)( name=f"block-bol-{axialIndex:03d}") for paramDef in b.p.paramDefs.inCategory( parameters.Category.assignInBlueprints): val = getattr(self, paramDef.name) if val is not None: b.p[paramDef.name] = val flags = None if self.flags is not None: flags = Flags.fromString(self.flags) b.setType(self.name, flags) for c in components.values(): b.add(c) b.p.nPins = b.getNumPins() b.p.axMesh = _setBlueprintNumberOfAxialMeshes( axialMeshPoints, cs["axialMeshRefinementFactor"]) b.p.height = height b.p.heightBOL = height # for fuel performance b.p.xsType = xsType b.setBuLimitInfo(cs) b = self._mergeComponents(b) b.verifyBlockDims() b.spatialGrid = spatialGrid return b def _getGridDesign(self, blueprint): """ Get the appropriate grid design This happens when a lattice input is provided on the block. Otherwise all components are ambiguously defined in the block. """ if self.gridName: if self.gridName not in blueprint.gridDesigns: raise KeyError( f"Lattice {self.gridName} defined on {self} is not " "defined in the blueprints `lattices` section.") return blueprint.gridDesigns[self.gridName] return None @staticmethod def _mergeComponents(b): solventNamesToMergeInto = set(c.p.mergeWith for c in b if c.p.mergeWith) if solventNamesToMergeInto: runLog.warning( "Component(s) {} in block {} has merged components inside it. The merge was valid at hot " "temperature, but the merged component only has the basic thermal expansion factors " "of the component(s) merged into. Expansion properties or dimensions of non hot " "temperature may not be representative of how the original components would have acted had " "they not been merged. It is recommended that merging happen right before " "a physics calculation using a block converter to avoid this." "".format(solventNamesToMergeInto, b.name), single=True, ) for solventName in solventNamesToMergeInto: soluteNames = [] for c in b: if c.p.mergeWith == solventName: soluteNames.append(c.name) converter = blockConverters.MultipleComponentMerger( b, soluteNames, solventName) b = converter.convert() return b
if c.p.mergeWith == solventName: soluteNames.append(c.name) converter = blockConverters.MultipleComponentMerger( b, soluteNames, solventName) b = converter.convert() return b for paramDef in parameters.forType(blocks.Block).inCategory( parameters.Category.assignInBlueprints): setattr( BlockBlueprint, paramDef.name, yamlize.Attribute(name=paramDef.name, default=None), ) def _setBlueprintNumberOfAxialMeshes(meshPoints, factor): """ Set the blueprint number of axial mesh based on the axial mesh refinement factor. """ if factor <= 0: raise ValueError("A positive axial mesh refinement factor " f"must be provided. A value of {factor} is invalid.") if factor != 1: runLog.important( "An axial mesh refinement factor of {} is applied " "to blueprint based on setting specification.".format(factor),
class GridBlueprint(yamlize.Object): """ A grid input blueprint. These directly build Grid objects and contain information about how to populate the Grid with child ArmiObjects for the Reactor Model. The grids get origins either from a parent block (for pin lattices) or from a System (for Cores, SFPs, and other components). For backward compatibility, the geometry and grid can be alternatively read from a latticeFile (historically the geometry.xml file). Attributes ---------- name : str The grid name geom : str The geometry of the grid (e.g. 'cartesian') latticeFile : str Path to input file containing just the lattice contents definition latticeMap : str An asciimap representation of the lattice contents latticeDimensions : Triplet An x/y/z dict with grid dimensions in cm symmetry : str A string defining the symmetry mode of the grid gridContents : dict A {(i,j): str} dictionary mapping spatialGrid indices in 2-D to string specifiers of what's supposed to be in the grid. """ name = yamlize.Attribute(key="name", type=str) geom = yamlize.Attribute(key="geom", type=str, default=geometry.HEX) latticeFile = yamlize.Attribute(key="lattice file", type=str, default=None) latticeMap = yamlize.Attribute(key="lattice map", type=str, default=None) latticeDimensions = yamlize.Attribute(key="lattice pitch", type=Triplet, default=None) symmetry = yamlize.Attribute(key="symmetry", type=str, default=geometry.THIRD_CORE + geometry.PERIODIC) # gridContents is the final form of grid contents information; # it is set regardless of how the input is read. This is how all # grid contents information is written out. gridContents = yamlize.Attribute(key="grid contents", type=dict, default=None) def __init__( self, name=None, geom=geometry.HEX, latticeMap=None, latticeFile=None, symmetry=geometry.THIRD_CORE + geometry.PERIODIC, gridContents=None, ): """ A Grid blueprint. Notes ----- yamlize does not call an __init__ method, instead it uses __new__ and setattr this is only needed for when you want to make this object from a non-YAML source. .. warning:: This is a Yamlize object, so ``__init__`` never really gets called. Only ``__new__`` does. """ self.name = name self.geom = geom self.latticeMap = latticeMap self.latticeFile = latticeFile self.symmetry = symmetry self.gridContents = gridContents self.eqPathInput = {} def construct(self): """Build a Grid from a grid definition.""" self._readGridContents() grid = self._constructSpatialGrid() return grid def _constructSpatialGrid(self): """ Build spatial grid. If you do not enter latticeDimensions, a unit grid will be produced which must be adjusted to the proper dimensions (often by inspection of children) at a later time. """ geom = self.geom maxIndex = self._getMaxIndex() runLog.extra("Creating the spatial grid") if geom in [geometry.RZT, geometry.RZ]: # for now, these can only be read in from the old geometry XML files. spatialGrid = self._makeRZGridFromLatticeFile() if geom == geometry.HEX: pitch = self.latticeDimensions.x if self.latticeDimensions else 1.0 # add 2 for potential dummy assems spatialGrid = grids.hexGridFromPitch( pitch, numRings=maxIndex + 2, ) elif geom == geometry.CARTESIAN: # if full core or not cut-off, bump the first assembly from the center of the mesh # into the positive values. xw, yw = ((self.latticeDimensions.x, self.latticeDimensions.y) if self.latticeDimensions else (1.0, 1.0)) isOffset = (self.symmetry and geometry.THROUGH_CENTER_ASSEMBLY not in self.symmetry) spatialGrid = grids.cartesianGridFromRectangle( xw, yw, numRings=maxIndex, isOffset=isOffset, ) runLog.debug("Built grid: {}".format(spatialGrid)) # set geometric metadata on spatialGrid. This information is needed in various # parts of the code and is best encapsulated on the grid itself rather than on # the container state. spatialGrid.geomType = self.geom spatialGrid.symmetry = self.symmetry return spatialGrid def _getMaxIndex(self): """ Find the max index in the grid contents. Used to limit the size of the spatialGrid. Used to be called maxNumRings. """ return max(itertools.chain(*zip(*self.gridContents.keys()))) def _makeRZGridFromLatticeFile(self): """Read an old-style XML file to build a RZT spatial grid.""" geom = geometry.SystemLayoutInput() geom.readGeomFromFile(self.latticeFile) spatialGrid = grids.thetaRZGridFromGeom(geom) return spatialGrid def _readGridContents(self): """ Read the specifiers as a function of grid position. The contents can either be provided as: * A dict mapping indices to specifiers (default output of this) * An asciimap * A YAML file in the gen-2 geometry file format * An XML file in the gen-1 geometry file format The output will always be stored in ``self.gridContents``. """ if self.gridContents: # grid contents read directly from input so nothing to do here. return elif self.latticeMap: self._readGridContentsLattice() elif self.latticeFile: self._readGridContentsFile() def _readGridContentsLattice(self): """Read an ascii map of grid contents. This update the gridContents attribute, which is a dict mapping grid i,j,k indices to textual specifiers (e.g. ``IC``)) """ latticeCls = asciimaps.asciiMapFromGeomAndSym(self.geom, self.symmetry) lattice = latticeCls() self.gridContents = lattice.readMap(self.latticeMap) def _readGridContentsFile(self): """ Read grid contents from a file. Notes ----- This reads both the old XML format as well as the new YAML format. The concept of a grid blueprint is slowly trying to take over from the geometry file/geometry object. """ self.gridContents = {} geom = geometry.SystemLayoutInput() geom.readGeomFromFile(self.latticeFile) for indices, spec in geom.assemTypeByIndices.items(): self.gridContents[indices] = spec self.geom = str(geom.geomType) self.symmetry = str(geom.symmetry) # eqPathInput allows fuel management to be input alongside the core grid. # This would be better as an independent grid but is here for now to help # migrate inputs from previous versions. self.eqPathInput = geom.eqPathInput def getLocators(self, spatialGrid: grids.Grid, latticeIDs: list): """ Return spatialLocators in grid corresponding to lattice IDs. This requires a fully-populated ``gridContents`` attribute. """ if latticeIDs is None: return [] if self.gridContents is None: return [] # tried using yamlize to coerce ints to strings but failed # after much struggle, so we just auto-convert here to deal # with int-like specifications. # (yamlize.StrList fails to coerce when ints are provided) latticeIDs = [str(i) for i in latticeIDs] locators = [] for (i, j), spec in self.gridContents.items(): locator = spatialGrid[i, j, 0] if spec in latticeIDs: locators.append(locator) return locators def getMultiLocator(self, spatialGrid, latticeIDs): """Create a MultiIndexLocation based on lattice IDs.""" spatialLocator = grids.MultiIndexLocation(grid=spatialGrid) spatialLocator.extend(self.getLocators(spatialGrid, latticeIDs)) return spatialLocator
class Blueprints(yamlize.Object, metaclass=_BlueprintsPluginCollector): """Base Blueprintsobject representing all the subsections in the input file.""" nuclideFlags = yamlize.Attribute(key="nuclide flags", type=isotopicOptions.NuclideFlags, default=None) customIsotopics = yamlize.Attribute(key="custom isotopics", type=isotopicOptions.CustomIsotopics, default=None) blockDesigns = yamlize.Attribute(key="blocks", type=BlockKeyedList, default=None) assemDesigns = yamlize.Attribute(key="assemblies", type=AssemblyKeyedList, default=None) systemDesigns = yamlize.Attribute(key="systems", type=Systems, default=None) gridDesigns = yamlize.Attribute(key="grids", type=Grids, default=None) # These are used to set up new attributes that come from plugins. Defining its # initial state here to make pylint happy _resolveFunctions = [] def __new__(cls): # yamlizable does not call __init__, so attributes that are not defined above # need to be initialized here self = yamlize.Object.__new__(cls) self.assemblies = {} self._prepped = False self._assembliesBySpecifier = {} self.allNuclidesInProblem = ( ordered_set.OrderedSet() ) # Better for performance since these are used for lookups self.activeNuclides = ordered_set.OrderedSet() self.inertNuclides = ordered_set.OrderedSet() self.elementsToExpand = [] return self def __init__(self): # again, yamlize does not call __init__, instead we use Blueprints.load which # creates and instance of a Blueprints object and initializes it with values # using setattr. Since the method is never called, it serves the purpose of # preventing pylint from issuing warnings about attributes not existing. self._assembliesBySpecifier = {} self._prepped = False self.systemDesigns = Systems() self.assemDesigns = AssemblyKeyedList() self.blockDesigns = BlockKeyedList() self.assemblies = {} self.grids = Grids() self.elementsToExpand = [] def __repr__(self): return "<{} Assemblies:{} Blocks:{}>".format(self.__class__.__name__, len(self.assemDesigns), len(self.blockDesigns)) def constructAssem(self, cs, name=None, specifier=None): """ Construct a new assembly instance from the assembly designs in this Blueprints object. Parameters ---------- cs : CaseSettings object Used to apply various modeling options when constructing an assembly. name : str (optional, and should be exclusive with specifier) Name of the assembly to construct. This should match the key that was used to define the assembly in the Blueprints YAML file. specifier : str (optional, and should be exclusive with name) Identifier of the assembly to construct. This should match the identifier that was used to define the assembly in the Blueprints YAML file. Raises ------ ValueError If neither name nor specifier are passed Notes ----- There is some possibility for "compiling" the logic with closures to make constructing an assembly / block / component faster. At this point is is pretty much irrelevant because we are currently just deepcopying already constructed assemblies. Currently, this method is backward compatible with other code in ARMI and generates the `.assemblies` attribute (the BOL assemblies). Eventually, this should be removed. """ self._prepConstruction(cs) # TODO: this should be migrated assembly designs instead of assemblies if name is not None: assem = self.assemblies[name] elif specifier is not None: assem = self._assembliesBySpecifier[specifier] else: raise ValueError( "Must supply assembly name or specifier to construct") a = copy.deepcopy(assem) # since a deepcopy has the same assembly numbers and block id's, we need to make it unique a.makeUnique() return a def _prepConstruction(self, cs): """ This method initializes a bunch of information within a Blueprints object such as assigning assembly and block type numbers, resolving the nuclides in the problem, and pre-populating assemblies. Ideally, it would not be necessary at all, but the ``cs`` currently contains a bunch of information necessary to create the applicable model. If it were possible, it would be terrific to override the Yamlizable.from_yaml method to run this code after the instance has been created, but we need additional information in order to build the assemblies that is not within the YAML file. This method should not be called directly, but it is used in testing. """ if not self._prepped: self._assignTypeNums() for func in self._resolveFunctions: func(self, cs) self._resolveNuclides(cs) self._assembliesBySpecifier.clear() self.assemblies.clear() for aDesign in self.assemDesigns: a = aDesign.construct(cs, self) self._assembliesBySpecifier[aDesign.specifier] = a self.assemblies[aDesign.name] = a self._checkAssemblyAreaConsistency(cs) runLog.header( "=========== Verifying Assembly Configurations ===========") # pylint: disable=no-member armi.getPluginManagerOrFail().hook.afterConstructionOfAssemblies( assemblies=self.assemblies.values(), cs=cs) self._prepped = True def _assignTypeNums(self): if self.blockDesigns is None: # this happens when directly defining assemblies. self.blockDesigns = BlockKeyedList() for aDesign in self.assemDesigns: for bDesign in aDesign.blocks: if bDesign not in self.blockDesigns: self.blockDesigns.add(bDesign) def _resolveNuclides(self, cs): """ Process elements and determine how to expand them to natural isotopics. Also builds meta-data about which nuclides are in the problem. This system works by building a dictionary in the ``elementsToExpand`` attribute with ``Element`` keys and list of ``NuclideBase`` values. The actual expansion of elementals to isotopics occurs during :py:meth:`Component construction <armi.reactor.blueprints.componentBlueprint. ComponentBlueprint._constructMaterial>`. """ from armi import utils actives = set() inerts = set() undefBurnChainActiveNuclides = set() if self.nuclideFlags is None: self.nuclideFlags = isotopicOptions.genDefaultNucFlags() self.elementsToExpand = [] for nucFlag in self.nuclideFlags: # this returns any nuclides that are flagged specifically for expansion by input expandedElements = nucFlag.fileAsActiveOrInert( actives, inerts, undefBurnChainActiveNuclides) self.elementsToExpand.extend(expandedElements) inerts -= actives self.customIsotopics = self.customIsotopics or isotopicOptions.CustomIsotopics( ) ( elementalsToKeep, expansions, ) = isotopicOptions.autoSelectElementsToKeepFromSettings(cs) nucsFromInput = actives | inerts # join # Flag all elementals for expansion unless they've been flagged otherwise by # user input or automatic lattice/datalib rules. for elemental in nuclideBases.instances: if not isinstance(elemental, nuclideBases.NaturalNuclideBase): # `elemental` may be a NaturalNuclideBase or a NuclideBase # skip all NuclideBases continue if elemental in elementalsToKeep: continue if elemental.name in actives: currentSet = actives actives.remove(elemental.name) elif elemental.name in inerts: currentSet = inerts inerts.remove(elemental.name) else: # This was not specified in the nuclide flags at all. # If a material with this in its composition is brought in # it's nice from a user perspective to allow it. # But current behavior is that all nuclides in problem # must be declared up front. continue self.elementsToExpand.append(elemental.element) if (elemental.name in self.nuclideFlags and self.nuclideFlags[elemental.name].expandTo): # user-input has precedence newNuclides = [ nuclideBases.byName[nn] for nn in self.nuclideFlags[ elemental.element.symbol].expandTo ] elif (elemental in expansions and elemental.element.symbol in self.nuclideFlags): # code-specific expansion required newNuclides = expansions[elemental] # overlay code details onto nuclideFlags for other parts of the code # that will use them. # CRAP: would be better if nuclideFlags did this upon reading s.t. # order didn't matter. On the other hand, this is the only place in # the code where NuclideFlags get built and have user settings around # (hence "resolve"). # This must be updated because the operative expansion code just uses the flags # # Also, if this element is not in nuclideFlags at all, we just don't add it self.nuclideFlags[elemental.element.symbol].expandTo = [ nb.name for nb in newNuclides ] else: # expand to all possible natural isotopics newNuclides = elemental.element.getNaturalIsotopics() for nb in newNuclides: currentSet.add(nb.name) if self.elementsToExpand: runLog.info( "Will expand {} elementals to have natural isotopics".format( ", ".join(element.symbol for element in self.elementsToExpand))) self.activeNuclides = ordered_set.OrderedSet(sorted(actives)) self.inertNuclides = ordered_set.OrderedSet(sorted(inerts)) self.allNuclidesInProblem = ordered_set.OrderedSet( sorted(actives.union(inerts))) # Inform user which nuclides are truncating the burn chain. if undefBurnChainActiveNuclides: runLog.info( tabulate.tabulate( [[ "Nuclides truncating the burn-chain:", utils.createFormattedStrWithDelimiter( list(undefBurnChainActiveNuclides)), ]], tablefmt="plain", ), single=True, ) def _checkAssemblyAreaConsistency(self, cs): references = None for a in self.assemblies.values(): if references is None: references = (a, a.getArea()) continue assemblyArea = a.getArea() if isinstance(a, assemblies.RZAssembly): # R-Z assemblies by definition have different areas, so skip the check continue if abs(references[1] - assemblyArea) > 1e-9: runLog.error("REFERENCE COMPARISON ASSEMBLY:") references[0][0].printContents() runLog.error("CURRENT COMPARISON ASSEMBLY:") a[0].printContents() raise InputError( "Assembly {} has a different area {} than assembly {} {}. Check inputs for accuracy" .format(a, assemblyArea, references[0], references[1])) blockArea = a[0].getArea() for b in a[1:]: if (abs(b.getArea() - blockArea) / blockArea > cs["acceptableBlockAreaError"]): runLog.error("REFERENCE COMPARISON BLOCK:") a[0].printContents(includeNuclides=False) runLog.error("CURRENT COMPARISON BLOCK:") b.printContents(includeNuclides=False) for c in b.getChildren(): runLog.error("{0} area {1} effective area {2}" "".format(c, c.getArea(), c.getVolume() / b.getHeight())) raise InputError( "Block {} has a different area {} than block {} {}. Check inputs for accuracy" .format(b, b.getArea(), a[0], blockArea)) @classmethod def migrate(cls, inp: typing.TextIO): """Given a stream representation of a blueprints file, migrate it. Parameters ---------- inp : typing.TextIO Input stream to migrate. """ for migI in migration.ACTIVE_MIGRATIONS: if issubclass(migI, migration.base.BlueprintsMigration): mig = migI(stream=inp) inp = mig.apply() return inp
class ComponentBlueprint(yamlize.Object): """ This class defines the inputs necessary to build ARMI component objects. It uses ``yamlize`` to enable serialization to and from YAML. """ name = yamlize.Attribute(type=str) @name.validator def name(self, name): # pylint: disable=no-self-use; reason=yamlize requirement if name in {"cladding"}: raise ValueError("Cannot set ComponentBlueprint.name to {}".format(name)) shape = yamlize.Attribute(type=str) @shape.validator def shape(self, shape): # pylint: disable=no-self-use; reason=yamlize requirement normalizedShape = shape.strip().lower() if normalizedShape not in components.ComponentType.TYPES: raise ValueError("Cannot set ComponentBlueprint.shape to {}".format(shape)) material = yamlize.Attribute(type=str) Tinput = yamlize.Attribute(type=float) Thot = yamlize.Attribute(type=float) isotopics = yamlize.Attribute(type=str, default=None) latticeIDs = yamlize.Attribute(type=list, default=None) origin = yamlize.Attribute(type=list, default=None) orientation = yamlize.Attribute(type=str, default=None) mergeWith = yamlize.Attribute(type=str, default=None) area = yamlize.Attribute(type=float, default=None) def construct(self, blueprint, matMods): """Construct a component""" runLog.debug("Constructing component {}".format(self.name)) kwargs = self._conformKwargs(blueprint, matMods) component = components.factory(self.shape.strip().lower(), [], kwargs) _insertDepletableNuclideKeys(component, blueprint) return component def _conformKwargs(self, blueprint, matMods): """This method gets the relevant kwargs to construct the component""" kwargs = {"mergeWith": self.mergeWith or "", "isotopics": self.isotopics or ""} for attr in self.attributes: # yamlize magic val = attr.get_value(self) if attr.name == "shape" or val == attr.default: continue elif attr.name == "material": # value is a material instance value = self._constructMaterial(blueprint, matMods) elif attr.name == "latticeIDs": # Don't pass latticeIDs on to the component constructor. # They're applied during block construction. continue else: value = attr.get_value(self) # Keep digging until the actual value is found. This is a bit of a hack to get around an # issue in yamlize/ComponentDimension where Dimensions can end up chained. while isinstance(value, ComponentDimension): value = value.value kwargs[attr.name] = value return kwargs def _constructMaterial(self, blueprint, matMods): nucsInProblem = blueprint.allNuclidesInProblem # make material with defaults mat = materials.resolveMaterialClassByName(self.material)() if self.isotopics is not None: # Apply custom isotopics before processing input mods so # the input mods have the final word blueprint.customIsotopics.apply(mat, self.isotopics) # add mass fraction custom isotopics info, since some material modifications need to see them # e.g. in the base Material.applyInputParams matMods.update( { "customIsotopics": { k: v.massFracs for k, v in blueprint.customIsotopics.items() } } ) if len(matMods) > 1: # don't apply if only customIsotopics is in there try: # update material with updated input params from blueprints file. mat.applyInputParams(**matMods) except TypeError: # This component does not accept material modification inputs of the names passed in # Keep going since the modification could work for another component pass expandElementals(mat, blueprint) missing = set(mat.p.massFrac.keys()).difference(nucsInProblem) if missing: raise exceptions.ConsistencyError( "The nuclides {} are present in material {} by compositions, but are not " "specified in the `nuclide flags` section of the input file. " "They need to be added, or custom isotopics need to be applied.".format( missing, mat ) ) return mat
mat.p.massFrac, elementExpansionPairs ) def _insertDepletableNuclideKeys(c, blueprint): if not any(nuc in blueprint.activeNuclides for nuc in c.getNuclides()): return c.p.flags |= Flags.DEPLETABLE nuclideBases.initReachableActiveNuclidesThroughBurnChain( c.p.numberDensities, blueprint.activeNuclides ) # This import-time magic requires all possible components # be imported before this module imports. The intent # was to make registration basically automatic. This has proven # to be quite problematic and will be replaced with an # explicit plugin-level component registration system. for dimName in set( [ kw for cType in components.ComponentType.TYPES.values() for kw in cType.DIMENSION_NAMES ] ): setattr( ComponentBlueprint, dimName, yamlize.Attribute(name=dimName, type=ComponentDimension, default=None), )
class ComponentBlueprint(yamlize.Object): """ This class defines the inputs necessary to build ARMI component objects. It uses ``yamlize`` to enable serialization to and from YAML. """ name = yamlize.Attribute(type=str) @name.validator def name(self, name): # pylint: disable=no-self-use; reason=yamlize requirement if name in {"cladding"}: raise ValueError( "Cannot set ComponentBlueprint.name to {}".format(name)) shape = yamlize.Attribute(type=str) @shape.validator def shape(self, shape): # pylint: disable=no-self-use; reason=yamlize requirement normalizedShape = shape.strip().lower() if normalizedShape not in components.ComponentType.TYPES: raise ValueError( "Cannot set ComponentBlueprint.shape to {}".format(shape)) material = yamlize.Attribute(type=str) Tinput = yamlize.Attribute(type=float) Thot = yamlize.Attribute(type=float) isotopics = yamlize.Attribute(type=str, default=None) centers = yamlize.Attribute(type=str, default=None) orientation = yamlize.Attribute(type=str, default=None) mergeWith = yamlize.Attribute(type=str, default=None) def construct(self, blueprint, matMods): """Construct a component""" runLog.debug("Constructing component {}".format(self.name)) kwargs, appliedMatMods = self._conformKwargs(blueprint, matMods) component = components.factory(self.shape.strip().lower(), [], kwargs) _insertDepletableNuclideKeys(component, blueprint) return component, appliedMatMods def _conformKwargs(self, blueprint, matMods): """This method gets the relevant kwargs to construct the component""" kwargs = { "mergeWith": self.mergeWith or "", "isotopics": self.isotopics or "" } for attr in self.attributes: # yamlize magic val = attr.get_value(self) if attr.name == "shape" or val == attr.default: continue elif attr.name == "material": # value is a material instance value, appliedMatMods = self._constructMaterial( blueprint, matMods) else: value = attr.get_value(self) # Keep digging until the actual value is found. This is a bit of a hack to get around an # issue in yamlize/ComponentDimension where Dimensions can end up chained. while isinstance(value, ComponentDimension): value = value.value kwargs[attr.name] = value return kwargs, appliedMatMods def _constructMaterial(self, blueprint, matMods): nucsInProblem = blueprint.allNuclidesInProblem mat = materials.resolveMaterialClassByName( self.material)() # make material with defaults if self.isotopics is not None: blueprint.customIsotopics.apply(mat, self.isotopics) appliedMatMods = False if any(matMods): try: mat.applyInputParams( **matMods ) # update material with updated input params from YAML file. appliedMatMods = True except TypeError: # This component does not accept material modification inputs of the names passed in # Keep going since the modification could work for another component pass # expand elementals densityTools.expandElementalMassFracsToNuclides( mat.p.massFrac, blueprint.elementsToExpand) missing = set(mat.p.massFrac.keys()).difference(nucsInProblem) if missing: raise exceptions.ConsistencyError( "The nuclides {} are present in material {} by compositions, but are not " "specified in the input file. They need to be added.".format( missing, mat)) return mat, appliedMatMods
class CustomIsotopic(yamlize.Map): """ User specified, custom isotopics input defined by a name (such as MOX), and key/pairs of nuclide names and numeric values consistent with the ``input format``. """ key_type = yamlize.Typed(str) value_type = yamlize.Typed(float) name = yamlize.Attribute(type=str) inputFormat = yamlize.Attribute(key="input format", type=str) @inputFormat.validator def inputFormat(self, value): if value not in self._allowedFormats: raise ValueError( "Cannot set `inputFormat` to `{}`, must be one of: {}".format( value, self._allowedFormats)) _density = yamlize.Attribute(key="density", type=float, default=None) _allowedFormats = { "number fractions", "number densities", "mass fractions" } def __new__(cls, *args): self = yamlize.Map.__new__(cls, *args) # the density as computed by source number densities self._computedDensity = None return self def __init__(self, name, inputFormat, density): # note: yamlize does not call an __init__ method, instead it uses __new__ and setattr self._name = None self.name = name self._inputFormat = None self.inputFormat = inputFormat self.density = density self.massFracs = {} def __setitem__(self, key, value): if key not in ALLOWED_KEYS: raise ValueError( "Key `{}` is not valid, must be one of: {}".format( key, ALLOWED_KEYS)) yamlize.Map.__setitem__(self, key, value) @property def density(self): return self._computedDensity or self._density @density.setter def density(self, value): if self._computedDensity is not None: raise AttributeError( "Density was computed from number densities, and should not be " "set directly.") self._density = value if value is not None and value < 0: raise ValueError( "Cannot set `density` to `{}`, must greater than 0".format( value)) @classmethod def from_yaml(cls, loader, node, rtd): """ Override the ``Yamlizable.from_yaml`` to inject custom data validation logic, and complete initialization of the object. """ self = yamlize.Map.from_yaml.__func__(cls, loader, node, rtd) try: self._initializeMassFracs() self._expandElementMassFracs() except Exception as ex: # use a YamlizingError to get line/column of erroneous input raise yamlize.YamlizingError(str(ex), node) return self @classmethod def from_yaml_key_val(cls, loader, key_node, val_node, key_attr, rtd): """ Override the ``Yamlizable.from_yaml`` to inject custom data validation logic, and complete initialization of the object. """ self = yamlize.Map.from_yaml_key_val.__func__(cls, loader, key_node, val_node, key_attr, rtd) try: self._initializeMassFracs() self._expandElementMassFracs() except Exception as ex: # use a YamlizingError to get line/column of erroneous input raise yamlize.YamlizingError(str(ex), val_node) return self def _initializeMassFracs(self): self.massFracs = dict() # defaults to 0.0, __init__ is not called if any(v < 0.0 for v in self.values()): raise ValueError("Custom isotopic input for {} is negative".format( self.name)) valSum = sum(self.values()) if not abs(valSum - 1.0) < 1e-5 and "fractions" in self.inputFormat: raise ValueError( "Fractional custom isotopic input values must sum to 1.0 in: {}" .format(self.name)) if self.inputFormat == "number fractions": sumNjAj = 0.0 for nuc, nj in self.items(): if nj: sumNjAj += nj * nucDir.getAtomicWeight(nuc) for nuc, value in self.items(): massFrac = value * nucDir.getAtomicWeight(nuc) / sumNjAj self.massFracs[nuc] = massFrac elif self.inputFormat == "number densities": if self._density is not None: raise exceptions.InputError( "Custom isotopic `{}` is over-specified. It was provided as number " "densities, and but density ({}) was also provided. Is the input format " "correct?".format(self.name, self.density)) M = { nuc: Ni / units.MOLES_PER_CC_TO_ATOMS_PER_BARN_CM * nucDir.getAtomicWeight(nuc) for nuc, Ni in self.items() } densityTotal = sum(M.values()) if densityTotal < 0: raise ValueError("Computed density is negative") for nuc, Mi in M.items(): self.massFracs[nuc] = Mi / densityTotal self._computedDensity = densityTotal elif self.inputFormat == "mass fractions": self.massFracs = dict(self) # as input else: raise ValueError( "Unrecognized custom isotopics input format {}.".format( self.inputFormat)) def _expandElementMassFracs(self): """ Expand the custom isotopics input entries that are elementals to isotopics. This is necessary when the element name is not a elemental nuclide. Most everywhere else expects Nuclide objects (or nuclide names). This input allows a user to enter "U" which would expand to the naturally occurring uranium isotopics. This is different than the isotopic expansion done for meeting user-specified modeling options (such as an MC**2, or MCNP expecting elements or isotopes), because it translates the user input into something that can be used later on. """ elementsToExpand = [] for nucName in self.massFracs: if nucName not in nuclideBases.byName: element = elements.bySymbol.get(nucName) if element is not None: runLog.info( "Expanding custom isotopic `{}` element `{}` to natural isotopics" .format(self.name, nucName)) # include all natural isotopes with None flag elementsToExpand.append((element, None)) else: raise exceptions.InputError( "Unrecognized nuclide/isotope/element in input: {}". format(nucName)) densityTools.expandElementalMassFracsToNuclides( self.massFracs, elementsToExpand) def apply(self, material): """ Apply specific isotopic compositions to a component. Generically, materials have composition-dependent bulk properties such as mass density. Note that this operation does not update these material properties. Use with care. Parameters ---------- material : Material An ARMI Material instance. """ material.p.massFrac = dict(self.massFracs) if self.density is not None: if not isinstance(material, materials.Custom): runLog.warning( "You specified a custom mass density on `{}` with custom isotopics `{}`. " "This has no effect; you can only set this on `Custom` " "materials. Continuing to use {} mass density.".format( material, self.name, material)) return # specifically, non-Custom materials only use refDensity and dLL, .p.density has no effect material.p.density = self.density
class AssemblyBlueprint(yamlize.Object): """ A data container for holding information needed to construct an ARMI assembly. This class utilizes ``yamlize`` to enable serialization to and from the blueprints YAML file. """ name = yamlize.Attribute(type=str) flags = yamlize.Attribute(type=str, default=None) specifier = yamlize.Attribute(type=str) blocks = yamlize.Attribute(type=blockBlueprint.BlockList) height = yamlize.Attribute(type=yamlize.FloatList) axialMeshPoints = yamlize.Attribute(key="axial mesh points", type=yamlize.IntList) radialMeshPoints = yamlize.Attribute( key="radial mesh points", type=int, default=None ) azimuthalMeshPoints = yamlize.Attribute( key="azimuthal mesh points", type=int, default=None ) materialModifications = yamlize.Attribute( key="material modifications", type=MaterialModifications, default=None ) xsTypes = yamlize.Attribute(key="xs types", type=yamlize.StrList) # note: yamlizable does not call an __init__ method, instead it uses __new__ and setattr _assemTypes = _configureAssemblyTypes() @classmethod def getAssemClass(cls, blocks): """ Get the ARMI ``Assembly`` class for the specified blocks Parameters ---------- blocks : list of Blocks Blocks for which to determine appropriate containing Assembly type """ blockClasses = {b.__class__ for b in blocks} for bType, aType in cls._assemTypes.items(): if bType in blockClasses: return aType raise ValueError( 'Unsupported block geometries in {}: "{}"'.format(cls.name, blocks) ) def construct(self, cs, blueprint): """ Construct an instance of this specific assembly blueprint. Parameters ---------- cs : CaseSettings CaseSettings object which containing relevant modeling options. blueprint : Blueprint Root blueprint object containing relevant modeling options. """ runLog.info("Constructing assembly `{}`".format(self.name)) a = self._constructAssembly(cs, blueprint) self._checkParamConsistency(a) a.calculateZCoords() return a def _constructAssembly(self, cs, blueprint): """Construct the current assembly.""" blocks = [] for axialIndex, bDesign in enumerate(self.blocks): b = self._createBlock(cs, blueprint, bDesign, axialIndex) blocks.append(b) assemblyClass = self.getAssemClass(blocks) a = assemblyClass(self.name) flags = None if self.flags is not None: flags = Flags.fromString(self.flags) a.p.flags = flags # set a basic grid with the right number of blocks with bounds to be adjusted. a.spatialGrid = grids.axialUnitGrid(len(blocks)) a.spatialGrid.armiObject = a # TODO: Remove mesh points from blueprints entirely. Submeshing should be # handled by specific physics interfaces radMeshPoints = self.radialMeshPoints or 1 a.p.RadMesh = radMeshPoints aziMeshPoints = self.azimuthalMeshPoints or 1 a.p.AziMesh = aziMeshPoints # loop a second time because we needed all the blocks before choosing the # assembly class. for axialIndex, block in enumerate(blocks): b.p.assemNum = a.p.assemNum b.name = b.makeName(a.p.assemNum, axialIndex) a.add(block) # Assign values for the parameters if they are defined on the blueprints for paramDef in a.p.paramDefs.inCategory( parameters.Category.assignInBlueprints ): val = getattr(self, paramDef.name) if val is not None: a.p[paramDef.name] = val return a def _createBlock(self, cs, blueprint, bDesign, axialIndex): """Create a block based on the block design and the axial index.""" materialInputs = self.materialModifications or {} meshPoints = self.axialMeshPoints[axialIndex] height = self.height[axialIndex] xsType = self.xsTypes[axialIndex] materialInput = { modName: modList[axialIndex] for modName, modList in materialInputs.items() if modList[axialIndex] != "" } b = bDesign.construct( cs, blueprint, axialIndex, meshPoints, height, xsType, materialInput ) # TODO: remove when the plugin system is fully set up? b.completeInitialLoading() return b def _checkParamConsistency(self, a): """Check that the number of block params specified is equal to the number of blocks specified.""" materialInputs = self.materialModifications or {} paramsToCheck = { "mesh points": self.axialMeshPoints, "heights": self.height, "xs types": self.xsTypes, } for modName, modList in materialInputs.items(): paramName = "material modifications for {}".format(modName) paramsToCheck[paramName] = modList for paramName, blockVals in paramsToCheck.items(): if len(self.blocks) != len(blockVals): raise ValueError( "Assembly {} had {} blocks, but {} {}. These numbers should be equal. " "Check input for errors.".format( a, len(self.blocks), len(blockVals), paramName ) )
class BlockBlueprint(yamlize.KeyedList): """Input definition for Block.""" item_type = componentBlueprint.ComponentBlueprint key_attr = componentBlueprint.ComponentBlueprint.name name = yamlize.Attribute(type=str) _geomOptions = _configureGeomOptions() def _getBlockClass(self, outerComponent): """ Get the ARMI ``Block`` class for the specified geomType. Parameters ---------- outerComponent : Component Largest component in block. """ for compCls, blockCls in self._geomOptions.items(): if isinstance(outerComponent, compCls): return blockCls raise ValueError( "Block input for {} has outer component {} which is " " not a supported Block geometry subclass. Update geometry." "".format(self.name, outerComponent) ) def construct( self, cs, blueprint, axialIndex, axialMeshPoints, height, xsType, materialInput ): """ Construct an ARMI ``Block`` to be placed in an ``Assembly``. Parameters ---------- cs : CaseSettings CaseSettings object for the appropriate simulation. blueprint : Blueprints Blueprints object containing various detailed information, such as nuclides to model axialIndex : int The Axial index this block exists within the parent assembly axialMeshPoints : int number of mesh points for use in the neutronics kernel height : float initial height of the block xsType : str String representing the xsType of this block. materialInput : dict dict containing material modification names and values """ runLog.debug("Constructing block {}".format(self.name)) appliedMatMods = False components = collections.OrderedDict() for cDesign in self: c, compAppliedMatMods = cDesign.construct(blueprint, materialInput) components[c.name] = c appliedMatMods |= compAppliedMatMods if any(materialInput) and not appliedMatMods: raise ValueError( "Failure to apply material modifications {} in block {}".format( materialInput, self.name ) ) for c in components.values(): c._resolveLinkedDims(components) boundingComp = sorted(components.values())[-1] b = self._getBlockClass(boundingComp)("Bxxx{0}".format(AXIAL_CHARS[axialIndex])) for paramDef in b.p.paramDefs.inCategory( parameters.Category.assignInBlueprints ): val = getattr(self, paramDef.name) if val is not None: b.p[paramDef.name] = val b.setType(self.name) for c in components.values(): b.addComponent(c) b.p.nPins = b.getNumPins() b.p.axMesh = _setBlueprintNumberOfAxialMeshes( axialMeshPoints, cs["axialMeshRefinementFactor"] ) b.p.height = height b.p.heightBOL = height # for fuel performance b.p.xsType = xsType b.setBuLimitInfo(cs) b.buildNumberDensityParams(nucNames=blueprint.allNuclidesInProblem) b = self._mergeComponents(b) b.verifyBlockDims() return b def _mergeComponents(self, b): solventNamesToMergeInto = set(c.p.mergeWith for c in b if c.p.mergeWith) if solventNamesToMergeInto: runLog.warning( "Component(s) {} in block {} has merged components inside it. The merge was valid at hot " "temperature, but the merged component only has the basic thermal expansion factors " "of the component(s) merged into. Expansion properties or dimensions of non hot " "temperature may not be representative of how the original components would have acted had " "they not been merged. It is recommended that merging happen right before " "a physics calculation using a block converter to avoid this." "".format(solventNamesToMergeInto, b.name), single=True, ) for solventName in solventNamesToMergeInto: soluteNames = [] for c in b: if c.p.mergeWith == solventName: soluteNames.append(c.name) converter = blockConverters.MultipleComponentMerger( b, soluteNames, solventName ) b = converter.convert() return b
class Blueprints(yamlize.Object): """Base Blueprintsobject representing all the subsections in the input file.""" nuclideFlags = yamlize.Attribute(key="nuclide flags", type=NuclideFlags, default=None) customIsotopics = yamlize.Attribute(key="custom isotopics", type=CustomIsotopics, default=None) blockDesigns = yamlize.Attribute(key="blocks", type=BlockKeyedList, default=None) assemDesigns = yamlize.Attribute(key="assemblies", type=AssemblyKeyedList, default=None) systemDesigns = yamlize.Attribute(key="systems", type=Systems, default=None) # These are used to set up new attributes that come from plugins. Defining its # initial state here to make pylint happy _resolveFunctions = [] def __new__(cls): # yamlizable does not call __init__, so attributes that are not defined above # need to be initialized here self = yamlize.Object.__new__(cls) self.assemblies = {} self._prepped = False self._assembliesBySpecifier = {} self.allNuclidesInProblem = ( ordered_set.OrderedSet() ) # Better for performance since these are used for lookups self.activeNuclides = ordered_set.OrderedSet() self.inertNuclides = ordered_set.OrderedSet() self.elementsToExpand = [] return self def __init__(self): # again, yamlize does not call __init__, instead we use Blueprints.load which creates and # instance of a Blueprints object and initializes it with values using setattr. Since the # method is never called, it serves the purpose of preventing pylint from issuing warnings # about attributes not existing. self.systemDesigns = Systems() self.assemDesigns = AssemblyKeyedList() self.blockDesigns = BlockKeyedList() self.assemblies = {} def __repr__(self): return "<{} Assemblies:{} Blocks:{}>".format(self.__class__.__name__, len(self.assemDesigns), len(self.blockDesigns)) def constructAssem(self, geomType, cs, name=None, specifier=None): """ Construct a new assembly instance from the assembly designs in this Blueprints object. Parameters ---------- geomType : str string indicating the geometry type. This is used to select the correct Assembly and Block subclasses. ``'hex'`` should be used to create hex assemblies. This input is derived based on the Geometry object, though it would be nice to instead infer it from block components, and then possibly fail if there is mismatch. Though, you can fit a round peg in a square hole so long as D <= s. cs : CaseSettings object Used to apply various modeling options when constructing an assembly. name : str (optional, and should be exclusive with specifier) Name of the assembly to construct. This should match the key that was used to define the assembly in the Blueprints YAML file. specifier : str (optional, and should be exclusive with name) Identifier of the assembly to construct. This should match the identifier that was used to define the assembly in the Blueprints YAML file. Raises ------ ValueError If neither name nor specifier are passed Notes ----- There is some possibility for "compiling" the logic with closures to make constructing an assembly / block / component faster. At this point is is pretty much irrelevant because we are currently just deepcopying already constructed assemblies. Currently, this method is backward compatible with other code in ARMI and generates the `.assemblies` attribute (the BOL assemblies). Eventually, this should be removed. """ self._prepConstruction(geomType, cs) # TODO: this should be migrated assembly designs instead of assemblies if name is not None: assem = self.assemblies[name] elif specifier is not None: assem = self._assembliesBySpecifier[specifier] else: raise ValueError( "Must supply assembly name or specifier to construct") a = copy.deepcopy(assem) # since a deepcopy has the same assembly numbers and block id's, we need to make it unique a.makeUnique() return a def _prepConstruction(self, geomType, cs): """ This method initializes a bunch of information within a Blueprints object such as assigning assembly and block type numbers, resolving the nuclides in the problem, and pre-populating assemblies. Ideally, it would not be necessary at all, but the ``cs`` currently contains a bunch of information necessary to create the applicable model. If it were possible, it would be terrific to override the Yamlizable.from_yaml method to run this code after the instance has been created, but we need additional information in order to build the assemblies that is not within the YAML file. This method should not be called directly, but it is used in testing. """ if not self._prepped: self._assignTypeNums() for func in self._resolveFunctions: func(self, cs) self._resolveNuclides(cs) self._assembliesBySpecifier.clear() self.assemblies.clear() for aDesign in self.assemDesigns: a = aDesign.construct(cs, self) self._assembliesBySpecifier[aDesign.specifier] = a self.assemblies[aDesign.name] = a self._checkAssemblyAreaConsistency(cs) runLog.header( "=========== Verifying Assembly Configurations ===========") # pylint: disable=no-member armi.getPluginManagerOrFail().hook.afterConstructionOfAssemblies( assemblies=self.assemblies.values(), cs=cs) self._prepped = True def _assignTypeNums(self): if self.blockDesigns is None: # this happens when directly defining assemblies. self.blockDesigns = BlockKeyedList() for aDesign in self.assemDesigns: for bDesign in aDesign.blocks: if bDesign not in self.blockDesigns: self.blockDesigns.add(bDesign) def _resolveNuclides(self, cs): """Expands the density of any elemental nuclides to its natural isotopics.""" from armi import utils # expand burn-chain to only contain nuclides, no elements actives = set() inerts = set() undefBurnChainActiveNuclides = set() if self.nuclideFlags is None: self.nuclideFlags = genDefaultNucFlags() for nucFlag in self.nuclideFlags: nucFlag.prepForCase(actives, inerts, undefBurnChainActiveNuclides) inerts -= actives self.customIsotopics = self.customIsotopics or CustomIsotopics() self.elementsToExpand = [] elementalsToSkip = self._selectNuclidesToExpandForModeling(cs) # if elementalsToSkip=[CR], we expand everything else. e.g. CR -> CR (unchanged) nucsFromInput = actives | inerts # join for elemental in nuclideBases.instances: if not isinstance(elemental, nuclideBases.NaturalNuclideBase): continue if elemental.name not in nucsFromInput: continue # we've now confirmed this elemental is in the problem if elemental in elementalsToSkip: continue nucsInProblem = actives if elemental.name in actives else inerts nucsInProblem.remove(elemental.name) self.elementsToExpand.append(elemental.element) for nb in elemental.element.getNaturalIsotopics(): nucsInProblem.add(nb.name) if self.elementsToExpand: runLog.info( "Expanding {} elementals to have natural isotopics".format( ", ".join(element.symbol for element in self.elementsToExpand))) self.activeNuclides = ordered_set.OrderedSet(sorted(actives)) self.inertNuclides = ordered_set.OrderedSet(sorted(inerts)) self.allNuclidesInProblem = ordered_set.OrderedSet( sorted(actives.union(inerts))) # Inform user that the burn-chain may not be complete if undefBurnChainActiveNuclides: runLog.info( tabulate.tabulate( [[ "Nuclides truncating the burn-chain:", utils.createFormattedStrWithDelimiter( list(undefBurnChainActiveNuclides)), ]], tablefmt="plain", ), single=True, ) @staticmethod def _selectNuclidesToExpandForModeling(cs): elementalsToSkip = set() endf70Elementals = [ nuclideBases.byName[name] for name in ["C", "V", "ZN"] ] endf71Elementals = [nuclideBases.byName[name] for name in ["C"]] if "MCNP" in cs["neutronicsKernel"]: if int(cs["mcnpLibrary"]) == 50: elementalsToSkip.update( nuclideBases.instances) # skip expansion # ENDF/B VII.0 elif 70 <= int(cs["mcnpLibrary"]) <= 79: elementalsToSkip.update(endf70Elementals) # ENDF/B VII.1 elif 80 <= int(cs["mcnpLibrary"]) <= 89: elementalsToSkip.update(endf71Elementals) else: raise InputError( "Failed to determine nuclides for modeling. " "The `mcnpLibrary` setting value ({}) is not supported.". format(cs["mcnpLibrary"])) elif cs["xsKernel"] in ["SERPENT", "MC2v3", "MC2v3-PARTISN"]: elementalsToSkip.update(endf70Elementals) elif cs["xsKernel"] == "MC2v2": elementalsToSkip.update(nuclideBases.instances) # skip expansion return elementalsToSkip def _checkAssemblyAreaConsistency(self, cs): references = None for a in self.assemblies.values(): if references is None: references = (a, a.getArea()) continue assemblyArea = a.getArea() if abs(references[1] - assemblyArea) > 1e-9 and not hasattr( a.location, "ThRZmesh"): # if the location has a mesh then the assemblies can have irregular areas runLog.error("REFERENCE COMPARISON ASSEMBLY:") references[0][0].printContents() runLog.error("CURRENT COMPARISON ASSEMBLY:") a[0].printContents() raise InputError( "Assembly {} has a different area {} than assembly {} {}. Check inputs for accuracy" .format(a, assemblyArea, references[0], references[1])) blockArea = a[0].getArea() for b in a[1:]: if (abs(b.getArea() - blockArea) / blockArea > cs["acceptableBlockAreaError"]): runLog.error("REFERENCE COMPARISON BLOCK:") a[0].printContents(includeNuclides=False) runLog.error("CURRENT COMPARISON BLOCK:") b.printContents(includeNuclides=False) for c in b.getChildren(): runLog.error("{0} area {1} effective area {2}" "".format(c, c.getArea(), c.getVolume() / b.getHeight())) raise InputError( "Block {} has a different area {} than block {} {}. Check inputs for accuracy" .format(b, b.getArea(), a[0], blockArea))
class SystemBlueprint(yamlize.Object): """ The reactor-level structure input blueprint. .. note:: We use string keys to link grids to objects that use them. This differs from how blocks/assembies are specified, which use YAML anchors. YAML anchors have proven to be problematic and difficult to work with """ name = yamlize.Attribute(key="name", type=str) typ = yamlize.Attribute(key="type", type=str, default="core") gridName = yamlize.Attribute(key="grid name", type=str) origin = yamlize.Attribute(key="origin", type=Triplet, default=None) def __init__(self, name=None, gridName=None, origin=None): """ A Reactor Level Structure like a core or SFP. Notes ----- yamlize does not call an __init__ method, instead it uses __new__ and setattr this is only needed for when you want to make this object from a non-YAML source. """ self.name = name self.gridName = gridName self.origin = origin self.axialExpChngr = None @staticmethod def _resolveSystemType(typ: str): # Loop over all plugins that could be attached and determine if any # tell us how to build a specific systems attribute. Sub-optimial # as this check is called for each system (e.g., core, spent fuel pool). # It is assumed that the number of systems is currently low enough to justify # this structure. manager = getPluginManagerOrFail() # Only need this to handle the case we don't find the system we expect seen = set() for options in manager.hook.defineSystemBuilders(): for key, builder in options.items(): # Take the first match we find. This would allow other plugins to # define a new core builder before finding those defined by the # ReactorPlugin if key == typ: return builder seen.add(key) raise ValueError( "Could not determine an appropriate class for handling a " "system of type `{}`. Supported types are {}.".format( typ, sorted(seen))) def construct(self, cs, bp, reactor, geom=None, loadAssems=True): """Build a core/IVS/EVST/whatever and fill it with children. Parameters ---------- cs : :py:class:`Settings <armi.settings.Settings>` object. armi settings to apply bp : :py:class:`Reactor <armi.reactor.blueprints.Blueprints>` object. armi blueprints to apply reactor : :py:class:`Reactor <armi.reactor.reactors.Reactor>` object. reactor to fill geom : optional loadAssems : bool, optional whether to fill reactor with assemblies, as defined in blueprints, or not. Is False in :py:class:`UniformMeshGeometryConverter <armi.reactor.converters.uniformMesh.UniformMeshGeometryConverter>` within the initNewReactor() class method. Raises ------ ValueError input error, no grid design provided ValueError for 1/3 core maps, assemblies are defined outside the expected 1/3 core region """ from armi.reactor import reactors # avoid circular import runLog.info("Constructing the `{}`".format(self.name)) self.axialExpChngr = AxialExpansionChanger( cs["detailedAxialExpansion"]) # TODO: We should consider removing automatic geom file migration. if geom is not None and self.name == "core": gridDesign = geom.toGridBlueprints("core")[0] else: if not bp.gridDesigns: raise ValueError( "The input must define grids to construct a reactor, but " "does not. Update input.") gridDesign = bp.gridDesigns.get(self.gridName, None) system = self._resolveSystemType(self.typ)(self.name) # TODO: This could be somewhere better. If system blueprints could be # subclassed, this could live in the CoreBlueprint. setOptionsFromCS() also isnt # great to begin with, so ideally it could be removed entirely. if isinstance(system, reactors.Core): system.setOptionsFromCs(cs) # Some systems may not require a prescribed grid design. Only try to use one if # it was provided if gridDesign is not None: spatialGrid = gridDesign.construct() system.spatialGrid = spatialGrid system.spatialGrid.armiObject = system reactor.add(system) # need parent before loading assemblies spatialLocator = grids.CoordinateLocation(self.origin.x, self.origin.y, self.origin.z, None) system.spatialLocator = spatialLocator if context.MPI_RANK != 0: # on non-master nodes we don't bother building up the assemblies # because they will be populated with DistributeState. return None # TODO: This is also pretty specific to Core-like things. We envision systems # with non-Core-like structure. Again, probably only doable with subclassing of # Blueprints if loadAssems: self._loadAssemblies(cs, system, gridDesign.gridContents, bp) # TODO: This post-construction work is specific to Cores for now. We need to # generalize this. Things to consider: # - Should the Core be able to do geom modifications itself, since it already # has the grid constructed from the grid design? # - Should the summary be so specifically Material data? Should this be done for # non-Cores? Like geom modifications, could this just be done in processLoading? # Should it be invoked higher up, by whatever code is requesting the Reactor be # built from Blueprints? if isinstance(system, reactors.Core): summarizeMaterialData(system) self._modifyGeometry(system, gridDesign) system.processLoading(cs) return system # pylint: disable=no-self-use def _loadAssemblies(self, cs, container, gridContents, bp): runLog.header("=========== Adding Assemblies to {} ===========".format( container)) badLocations = set() for locationInfo, aTypeID in gridContents.items(): newAssembly = bp.constructAssem(cs, specifier=aTypeID) if not cs["inputHeightsConsideredHot"]: if not newAssembly.hasFlags(Flags.CONTROL): self.axialExpChngr.setAssembly(newAssembly) self.axialExpChngr.expansionData.computeThermalExpansionFactors( ) self.axialExpChngr.axiallyExpandAssembly(thermal=True) i, j = locationInfo loc = container.spatialGrid[i, j, 0] try: container.add(newAssembly, loc) except LookupError: badLocations.add(loc) if badLocations: raise ValueError( "Geometry core map xml had assemblies outside the " "first third core, but had third core symmetry. \n" "Please update symmetry to be `full core` or " "remove assemblies outside the first third. \n" "The locations outside the first third are {}".format( badLocations)) def _modifyGeometry(self, container, gridDesign): """Perform post-load geometry conversions like full core, edge assems.""" # all cases should have no edge assemblies. They are added ephemerally when needed from armi.reactor.converters import geometryConverters # circular imports runLog.header( "=========== Applying Geometry Modifications ===========") converter = geometryConverters.EdgeAssemblyChanger() converter.removeEdgeAssemblies(container) # now update the spatial grid dimensions based on the populated children # (unless specified on input) if not gridDesign.latticeDimensions: runLog.info( "Updating spatial grid pitch data for {} geometry".format( container.geomType)) if container.geomType == geometry.GeomType.HEX: container.spatialGrid.changePitch(container[0][0].getPitch()) elif container.geomType == geometry.GeomType.CARTESIAN: xw, yw = container[0][0].getPitch() container.spatialGrid.changePitch(xw, yw)
class NuclideFlag(yamlize.Object): """ Defines whether or not each nuclide is included in the burn chain and cross sections. Also controls which nuclides get expanded from elementals to isotopics and which natural isotopics to exclude (if any). Oftentimes, cross section library creators include some natural isotopes but not all. For example, it is common to include O16 but not O17 or O18. Each code has slightly different interpretations of this so we give the user full control here. We also try to provide useful defaults. There are lots of complications that can arise in these choices. It makes reasonable sense to use elemental compositions for things that are typically used without isotopic modifications (Fe, O, Zr, Cr, Na). If we choose to expand some or all of these to isotopics at initialization based on cross section library requirements, a single case will work fine with a given lattice physics option. However, restarting from that case with different cross section needs is challenging. Attributes ---------- nuclideName : str The name of the nuclide burn : bool True if this nuclide should be added to the burn chain. If True, all reachable nuclides via transmutation and decay must be included as well. xs : bool True if this nuclide should be included in the cross section libraries. Effectively, if this nuclide is in the problem at all, this should be true. expandTo : list of str, optional isotope nuclideNames to expand to. For example, if nuclideName is ``O`` then this could be ``["O16", "O17"]`` to expand it into those two isotopes (but not ``O18``). The nuclides will be scaled up uniformly to account for any missing natural nuclides. """ nuclideName = yamlize.Attribute(type=str) @nuclideName.validator def nuclideName(self, value): if value not in ALLOWED_KEYS: raise ValueError( "`{}` is not a valid nuclide name, must be one of: {}".format( value, ALLOWED_KEYS)) burn = yamlize.Attribute(type=bool) xs = yamlize.Attribute(type=bool) expandTo = yamlize.Attribute(type=yamlize.StrList, default=None) def __init__(self, nuclideName, burn, xs, expandTo): # note: yamlize does not call an __init__ method, instead it uses __new__ and setattr self.nuclideName = nuclideName self.burn = burn self.xs = xs self.expandTo = expandTo def __repr__(self): return "<NuclideFlag name:{} burn:{} xs:{}>".format( self.nuclideName, self.burn, self.xs) def fileAsActiveOrInert(self, activeSet, inertSet, undefinedBurnChainActiveNuclides): """ Given a nuclide or element name, file it as either active or inert. If isotopic expansions are requested, include the isotopics rather than the NaturalNuclideBase, as the NaturalNuclideBase will never occur in such a problem. """ nb = nuclideBases.byName[self.nuclideName] if self.expandTo: nucBases = [nuclideBases.byName[nn] for nn in self.expandTo] expanded = [nb.element] # error to expand non-elements else: nucBases = [nb] expanded = [] for nuc in nucBases: if self.burn: if not nuc.trans and not nuc.decays: # DUMPs and LFPs usually undefinedBurnChainActiveNuclides.add(nuc.name) activeSet.add(nuc.name) if self.xs: inertSet.add(nuc.name) return expanded
class SystemBlueprint(yamlize.Object): """ The reactor-level structure input blueprint. .. note:: We use strings to link grids to things that use them rather than YAML anchors in this part of the input. This seems inconsistent with how blocks are referred to in assembly blueprints but this is part of a transition away from YAML anchors. """ name = yamlize.Attribute(key="name", type=str) gridName = yamlize.Attribute(key="grid name", type=str) origin = yamlize.Attribute(key="origin", type=Triplet, default=None) def __init__(self, name=None, gridName=None, origin=None): """ A Reactor Level Structure like a core or SFP. Notes ----- yamlize does not call an __init__ method, instead it uses __new__ and setattr this is only needed for when you want to make this object from a non-YAML source. """ self.name = name self.gridName = gridName self.origin = origin def construct(self, cs, bp, reactor, geom=None): """Build a core/IVS/EVST/whatever and fill it with children.""" from armi.reactor import reactors # avoid circular import runLog.info("Constructing the `{}`".format(self.name)) if geom is not None: gridDesign = geom.toGridBlueprint("core") else: gridDesign = bp.gridDesigns[self.gridName] spatialGrid = gridDesign.construct() container = reactors.Core(self.name) container.setOptionsFromCs(cs) container.spatialGrid = spatialGrid container.spatialGrid.armiObject = container reactor.add(container) # need parent before loading assemblies spatialLocator = grids.CoordinateLocation(self.origin.x, self.origin.y, self.origin.z, None) container.spatialLocator = spatialLocator if armi.MPI_RANK != 0: # on non-master nodes we don't bother building up the assemblies # because they will be populated with DistributeState. # This is intended to optimize speed and minimize ram. return None self._loadAssemblies(cs, container, gridDesign, gridDesign.gridContents, bp) summarizeMaterialData(container) self._modifyGeometry(container, gridDesign) container.processLoading(cs) return container # pylint: disable=no-self-use def _loadAssemblies(self, cs, container, gridDesign, gridContents, bp): runLog.header("=========== Adding Assemblies to {} ===========".format( container)) badLocations = set() for locationInfo, aTypeID in gridContents.items(): newAssembly = bp.constructAssem(gridDesign.geom, cs, specifier=aTypeID) i, j = locationInfo loc = container.spatialGrid[i, j, 0] if (container.symmetry == geometry.THIRD_CORE + geometry.PERIODIC and not container.spatialGrid.isInFirstThird( loc, includeTopEdge=True)): badLocations.add(loc) container.add(newAssembly, loc) if badLocations: raise ValueError( "Geometry core map xml had assemblies outside the " "first third core, but had third core symmetry. \n" "Please update symmetry to be `full core` or " "remove assemblies outside the first third. \n" "The locations outside the first third are {}".format( badLocations)) def _modifyGeometry(self, container, gridDesign): """Perform post-load geometry conversions like full core, edge assems.""" # all cases should have no edge assemblies. They are added ephemerally when needed from armi.reactor.converters import geometryConverters # circular imports runLog.header( "=========== Applying Geometry Modifications ===========") converter = geometryConverters.EdgeAssemblyChanger() converter.removeEdgeAssemblies(container) # now update the spatial grid dimensions based on the populated children # (unless specified on input) if not gridDesign.latticeDimensions: runLog.info( "Updating spatial grid pitch data for {} geometry".format( container.geomType)) if container.geomType == geometry.HEX: container.spatialGrid.changePitch(container[0][0].getPitch()) elif container.geomType == geometry.CARTESIAN: xw, yw = container[0][0].getPitch() container.spatialGrid.changePitch(xw, yw)
class GridBlueprint(yamlize.Object): """ A grid input blueprint. These directly build Grid objects and contain information about how to populate the Grid with child ArmiObjects for the Reactor Model. The grids get origins either from a parent block (for pin lattices) or from a System (for Cores, SFPs, and other components). Attributes ---------- name : str The grid name geom : str The geometry of the grid (e.g. 'cartesian') latticeMap : str An asciimap representation of the lattice contents latticeDimensions : Triplet An x/y/z Triplet with grid dimensions in cm. This is used to specify a uniform grid, such as Cartesian or Hex. Mutually exclusive with gridBounds. gridBounds : dict A dictionary containing explicit grid boundaries. Specific keys used will depend on the type of grid being defined. Mutually exclusive with latticeDimensions. symmetry : str A string defining the symmetry mode of the grid gridContents : dict A {(i,j): str} dictionary mapping spatialGrid indices in 2-D to string specifiers of what's supposed to be in the grid. """ name = yamlize.Attribute(key="name", type=str) geom = yamlize.Attribute(key="geom", type=str, default=geometry.HEX) latticeMap = yamlize.Attribute(key="lattice map", type=str, default=None) latticeDimensions = yamlize.Attribute(key="lattice pitch", type=Triplet, default=None) gridBounds = yamlize.Attribute(key="grid bounds", type=dict, default=None) symmetry = yamlize.Attribute(key="symmetry", type=str, default=geometry.THIRD_CORE + geometry.PERIODIC) # gridContents is the final form of grid contents information; # it is set regardless of how the input is read. This is how all # grid contents information is written out. gridContents = yamlize.Attribute(key="grid contents", type=dict, default=None) @gridContents.validator def gridContents(self, value): # pylint: disable=method-hidden if value is None: return True if not all(isinstance(key, tuple) for key in value.keys()): raise InputError( "Keys need to be presented as [i, j]. Check the blueprints.") def __init__( self, name=None, geom=geometry.HEX, latticeMap=None, symmetry=geometry.THIRD_CORE + geometry.PERIODIC, gridContents=None, gridBounds=None, ): """ A Grid blueprint. Notes ----- yamlize does not call an __init__ method, instead it uses __new__ and setattr this is only needed for when you want to make this object from a non-YAML source. .. warning:: This is a Yamlize object, so ``__init__`` never really gets called. Only ``__new__`` does. """ self.name = name self.geom = geom self.latticeMap = latticeMap self.symmetry = symmetry self.gridContents = gridContents self.gridBounds = gridBounds def construct(self): """Build a Grid from a grid definition.""" self._readGridContents() grid = self._constructSpatialGrid() return grid def _constructSpatialGrid(self): """ Build spatial grid. If you do not enter latticeDimensions, a unit grid will be produced which must be adjusted to the proper dimensions (often by inspection of children) at a later time. """ geom = self.geom maxIndex = self._getMaxIndex() runLog.extra("Creating the spatial grid") if geom in (geometry.RZT, geometry.RZ): if self.gridBounds is None: # This check is regrattably late. It would be nice if we could validate # that bounds are provided if R-Theta mesh is being used. raise InputError( "Grid bounds must be provided for `{}` to specify a grid with " "r-theta components.".format(self.name)) for key in ("theta", "r"): if key not in self.gridBounds: raise InputError( "{} grid bounds were not provided for `{}`.".format( key, self.name)) # convert to list, otherwise it is a CommentedSeq theta = numpy.array(self.gridBounds["theta"]) radii = numpy.array(self.gridBounds["r"]) for l, name in ((theta, "theta"), (radii, "radii")): if not _isMonotonicUnique(l): raise InputError( "Grid bounds for {}:{} is not sorted or contains " "duplicates. Check blueprints.".format( self.name, name)) spatialGrid = grids.ThetaRZGrid(bounds=(theta, radii, (0.0, 0.0))) if geom == geometry.HEX: pitch = self.latticeDimensions.x if self.latticeDimensions else 1.0 # add 2 for potential dummy assems spatialGrid = grids.HexGrid.fromPitch(pitch, numRings=maxIndex + 2) elif geom == geometry.CARTESIAN: # if full core or not cut-off, bump the first assembly from the center of # the mesh into the positive values. xw, yw = ((self.latticeDimensions.x, self.latticeDimensions.y) if self.latticeDimensions else (1.0, 1.0)) isOffset = (self.symmetry and geometry.THROUGH_CENTER_ASSEMBLY not in self.symmetry) spatialGrid = grids.CartesianGrid.fromRectangle(xw, yw, numRings=maxIndex + 1, isOffset=isOffset) runLog.debug("Built grid: {}".format(spatialGrid)) # set geometric metadata on spatialGrid. This information is needed in various # parts of the code and is best encapsulated on the grid itself rather than on # the container state. spatialGrid.geomType = self.geom spatialGrid.symmetry = self.symmetry return spatialGrid def _getMaxIndex(self): """ Find the max index in the grid contents. Used to limit the size of the spatialGrid. Used to be called maxNumRings. """ if self.gridContents: return max(itertools.chain(*zip(*self.gridContents.keys()))) else: return 6 def expandToFull(self): """ Unfold the blueprints to represent full symmetry. .. note:: This relatively rudimentary, and copies entries from the currently-represented domain to their corresponding locations in full symmetry. This may not produce the desired behavior for some scenarios, such as when expanding fuel shuffling paths or the like. Future work may make this more sophisticated. """ if geometry.FULL_CORE in self.symmetry: # No need! return grid = self.construct() newContents = copy.copy(self.gridContents) for idx, contents in self.gridContents.items(): equivs = grid.getSymmetricEquivalents(idx) for idx2 in equivs: newContents[idx2] = contents self.gridContents = newContents split = (geometry.THROUGH_CENTER_ASSEMBLY if geometry.THROUGH_CENTER_ASSEMBLY in self.symmetry else "") self.symmetry = geometry.FULL_CORE + split def _readGridContents(self): """ Read the specifiers as a function of grid position. The contents can either be provided as: * A dict mapping indices to specifiers (default output of this) * An asciimap The output will always be stored in ``self.gridContents``. """ if self.gridContents: return elif self.latticeMap: self._readGridContentsLattice() if self.gridContents is None: # Make sure we have at least something; clients shouldn't have to worry # about whether gridContents exist at all. self.gridContents = dict() def _readGridContentsLattice(self): """Read an ascii map of grid contents. This update the gridContents attribute, which is a dict mapping grid i,j,k indices to textual specifiers (e.g. ``IC``)) """ latticeCls = asciimaps.asciiMapFromGeomAndSym(self.geom, self.symmetry) lattice = latticeCls() latticeMap = lattice.readMap(self.latticeMap) self.gridContents = dict() for (i, j), spec in latticeMap.items(): if spec == "-": # skip placeholders continue self.gridContents[i, j] = spec def getLocators(self, spatialGrid: grids.Grid, latticeIDs: list): """ Return spatialLocators in grid corresponding to lattice IDs. This requires a fully-populated ``gridContents`` attribute. """ if latticeIDs is None: return [] if self.gridContents is None: return [] # tried using yamlize to coerce ints to strings but failed # after much struggle, so we just auto-convert here to deal # with int-like specifications. # (yamlize.StrList fails to coerce when ints are provided) latticeIDs = [str(i) for i in latticeIDs] locators = [] for (i, j), spec in self.gridContents.items(): locator = spatialGrid[i, j, 0] if spec in latticeIDs: locators.append(locator) return locators def getMultiLocator(self, spatialGrid, latticeIDs): """Create a MultiIndexLocation based on lattice IDs.""" spatialLocator = grids.MultiIndexLocation(grid=spatialGrid) spatialLocator.extend(self.getLocators(spatialGrid, latticeIDs)) return spatialLocator
class BlockBlueprint(yamlize.KeyedList): """Input definition for Block.""" item_type = componentBlueprint.ComponentBlueprint key_attr = componentBlueprint.ComponentBlueprint.name name = yamlize.Attribute(key="name", type=str) gridName = yamlize.Attribute(key="grid name", type=str, default=None) flags = yamlize.Attribute(type=str, default=None) _geomOptions = _configureGeomOptions() def _getBlockClass(self, outerComponent): """ Get the ARMI ``Block`` class for the specified outerComponent. Parameters ---------- outerComponent : Component Largest component in block. """ for compCls, blockCls in self._geomOptions.items(): if isinstance(outerComponent, compCls): return blockCls raise ValueError( "Block input for {} has outer component {} which is " " not a supported Block geometry subclass. Update geometry." "".format(self.name, outerComponent)) def construct(self, cs, blueprint, axialIndex, axialMeshPoints, height, xsType, materialInput): """ Construct an ARMI ``Block`` to be placed in an ``Assembly``. Parameters ---------- cs : CaseSettings CaseSettings object for the appropriate simulation. blueprint : Blueprints Blueprints object containing various detailed information, such as nuclides to model axialIndex : int The Axial index this block exists within the parent assembly axialMeshPoints : int number of mesh points for use in the neutronics kernel height : float initial height of the block xsType : str String representing the xsType of this block. materialInput : dict Double-layered dict. Top layer groups the by-block material modifications under the `byBlock` key and the by-component material modifications under the component's name. The inner dict under each key contains material modification names and values. """ runLog.debug("Constructing block {}".format(self.name)) components = collections.OrderedDict() # build grid before components so you can load # the components into the grid. gridDesign = self._getGridDesign(blueprint) if gridDesign: spatialGrid = gridDesign.construct() else: spatialGrid = None self._checkByComponentMaterialInput(materialInput) for componentDesign in self: filteredMaterialInput = self._filterMaterialInput( materialInput, componentDesign) c = componentDesign.construct(blueprint, filteredMaterialInput) if cs["inputHeightsConsideredHot"]: if "group" in c.name: for component in c: component.applyHotHeightDensityReduction() componentBlueprint.insertDepletableNuclideKeys( component, blueprint) else: c.applyHotHeightDensityReduction() componentBlueprint.insertDepletableNuclideKeys( c, blueprint) components[c.name] = c if spatialGrid: componentLocators = gridDesign.getMultiLocator( spatialGrid, componentDesign.latticeIDs) if componentLocators: # this component is defined in the block grid # We can infer the multiplicity from the grid. # Otherwise it's a component that is in a block # with grids but that's not in the grid itself. c.spatialLocator = componentLocators mult = c.getDimension("mult") if mult and mult != 1.0 and mult != len(c.spatialLocator): raise ValueError( f"Conflicting ``mult`` input ({mult}) and number of " f"lattice positions ({len(c.spatialLocator)}) for {c}. " "Recommend leaving off ``mult`` input when using grids." ) elif not mult or mult == 1.0: # learn mult from grid definition c.setDimension("mult", len(c.spatialLocator)) # Resolve linked dims after all components in the block are created for c in components.values(): c.resolveLinkedDims(components) boundingComp = sorted(components.values())[-1] # give a temporary name (will be updated by b.makeName as real blocks populate systems) b = self._getBlockClass(boundingComp)( name=f"block-bol-{axialIndex:03d}") for paramDef in b.p.paramDefs.inCategory( parameters.Category.assignInBlueprints): val = getattr(self, paramDef.name) if val is not None: b.p[paramDef.name] = val flags = None if self.flags is not None: flags = Flags.fromString(self.flags) b.setType(self.name, flags) for c in components.values(): b.add(c) b.p.nPins = b.getNumPins() b.p.axMesh = _setBlueprintNumberOfAxialMeshes( axialMeshPoints, cs["axialMeshRefinementFactor"]) b.p.height = height b.p.heightBOL = height # for fuel performance b.p.xsType = xsType b.setBuLimitInfo(cs) b = self._mergeComponents(b) b.verifyBlockDims() b.spatialGrid = spatialGrid if b.spatialGrid is None and cs[globalSettings.CONF_BLOCK_AUTO_GRID]: try: b.autoCreateSpatialGrids() except (ValueError, NotImplementedError) as e: runLog.warning(str(e), single=True) return b def _checkByComponentMaterialInput(self, materialInput): for component in materialInput: if component != "byBlock": if component not in [ componentDesign.name for componentDesign in self ]: if materialInput[component]: # ensure it is not empty raise ValueError( f"The component '{component}' used to specify a by-component" f" material modification is not in block '{self.name}'." ) @staticmethod def _filterMaterialInput(materialInput, componentDesign): """ Get the by-block material modifications and those specifically for this component. If a material modification is specified both by-block and by-component for a given component, the by-component value will be used. """ filteredMaterialInput = {} # first add the by-block modifications without question if "byBlock" in materialInput: for modName, modVal in materialInput["byBlock"].items(): filteredMaterialInput[modName] = modVal # then get the by-component modifications as appropriate for component, mod in materialInput.items(): if component == "byBlock": pass # we already added these else: # these are by-component mods, first test if the component matches # before adding. if component matches, add the modifications, # overwriting any by-block modifications of the same type if component == componentDesign.name: for modName, modVal in mod.items(): filteredMaterialInput[modName] = modVal return filteredMaterialInput def _getGridDesign(self, blueprint): """ Get the appropriate grid design This happens when a lattice input is provided on the block. Otherwise all components are ambiguously defined in the block. """ if self.gridName: if self.gridName not in blueprint.gridDesigns: raise KeyError( f"Lattice {self.gridName} defined on {self} is not " "defined in the blueprints `lattices` section.") return blueprint.gridDesigns[self.gridName] return None @staticmethod def _mergeComponents(b): solventNamesToMergeInto = set(c.p.mergeWith for c in b.iterComponents() if c.p.mergeWith) if solventNamesToMergeInto: runLog.warning( "Component(s) {} in block {} has merged components inside it. The merge was valid at hot " "temperature, but the merged component only has the basic thermal expansion factors " "of the component(s) merged into. Expansion properties or dimensions of non hot " "temperature may not be representative of how the original components would have acted had " "they not been merged. It is recommended that merging happen right before " "a physics calculation using a block converter to avoid this." "".format(solventNamesToMergeInto, b.name), single=True, ) for solventName in solventNamesToMergeInto: soluteNames = [] for c in b: if c.p.mergeWith == solventName: soluteNames.append(c.name) converter = blockConverters.MultipleComponentMerger( b, soluteNames, solventName) b = converter.convert() return b