コード例 #1
0
ファイル: componentBlueprint.py プロジェクト: ntouran/armi
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)
コード例 #2
0
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
コード例 #3
0
ファイル: isotopicOptions.py プロジェクト: youngmit/armi
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)
コード例 #4
0
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}
コード例 #5
0
ファイル: componentBlueprint.py プロジェクト: ntouran/armi
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
コード例 #6
0
ファイル: assemblyBlueprint.py プロジェクト: ntouran/armi
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(),
    )
コード例 #7
0
ファイル: reactorBlueprint.py プロジェクト: pxm321/armi
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)
コード例 #8
0
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
コード例 #9
0
                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),
コード例 #10
0
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
コード例 #11
0
ファイル: __init__.py プロジェクト: pxm321/armi
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
コード例 #12
0
ファイル: componentBlueprint.py プロジェクト: wilcoxjd/armi
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
コード例 #13
0
ファイル: componentBlueprint.py プロジェクト: wilcoxjd/armi
        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),
    )
コード例 #14
0
ファイル: componentBlueprint.py プロジェクト: youngmit/armi
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
コード例 #15
0
ファイル: isotopicOptions.py プロジェクト: crisobg1/Framework
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
コード例 #16
0
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
                    )
                )
コード例 #17
0
ファイル: blockBlueprint.py プロジェクト: youngmit/armi
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
コード例 #18
0
ファイル: __init__.py プロジェクト: youngmit/armi
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))
コード例 #19
0
ファイル: reactorBlueprint.py プロジェクト: ntouran/armi
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)
コード例 #20
0
ファイル: isotopicOptions.py プロジェクト: crisobg1/Framework
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
コード例 #21
0
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)
コード例 #22
0
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
コード例 #23
0
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