Source code for armi.reactor.assemblyLists

# Copyright 2019 TerraPower, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Module containing :py:class:`AssemblyList` and related classes.

Assembly Lists are core-like objects that store collections of Assemblies. They were
originally developed to serve as things like spent-fuel pools and the like, where
spatial location of Assemblies need not be quite as precise.

Presently, the :py:class:`armi.reactor.reactors.Core` constructs a spent fuel pool
`self.sfp`. We are in the process of removing these as instance attributes of the
``Core``, and moving them into sibling systems on the root
:py:class:`armi.reactor.reactors.Reactor` object.
"""
import abc
import itertools

from armi import runLog
from armi.utils import units
from armi.reactor import grids
from armi.reactor import composites


[docs]class AutoFiller(abc.ABC): """ Class for governing automatic insertion of Assemblies when a specific Composite location isn't requested. This is kept separate from the ``AssemblyList`` class itself to promote composition over inheritance; reasonable implementations of auto-fill strategies will have their own state, which subclasses of ``AssemblyList`` should not have to manage. """
[docs] def getNextLocation(self, a) -> grids.LocationBase: """Return the next automatic location."""
[docs] def assemblyAdded(self, a): """ Register that an assembly has been added. This allows an ``AutoFiller`` to be notified that an assembly has been added manually. """
[docs]class RowMajorAutoFiller(AutoFiller): """ :py:class:`AutoFiller` implementation for filling a "rectangular" grid of Assemblies. This fills the :py:class:`armi.reactor.grids.Grid` of the associated :py:class:`AssemblyList` by filling subsequent rows with ``nMajor`` assemblies before moving to the next row. """ def __init__(self, aList, nMajor): self._nMajor = nMajor self._aList = aList
[docs] def getNextLocation(self, _a): filledLocations = {a.spatialLocator for a in self._aList} grid = self._aList.spatialGrid for idx in itertools.count(): j = idx // self._nMajor i = idx % self._nMajor loc = grid[i, j, 0] if loc not in filledLocations: return loc return None
[docs] def assemblyAdded(self, a): """ Do nothing. A more optimal implementation would cache things that would be affected by this. """
[docs]class AssemblyList(composites.Composite): """ A quasi-arbitrary collection of Assemblies. The AssemblyList is similar to a Core, in that it is designed to store Assembly objects. Unlike the Core, they have far fewer convenience functions, and permit looser control over where assemblies actually live. """ def __init__(self, name, parent=None): composites.Composite.__init__(self, name) self.parent = parent # make a Cartesian assembly rack by default. Anything that really cares about # the layout should specify one manually or in Blueprints self.spatialGrid = grids.CartesianGrid.fromRectangle(50.0, 50.0) self._filler = RowMajorAutoFiller(self, 10) @property def r(self): # This needs to be here until we remove the dependency of Reactor upon # AssemblyLists from armi.reactor import reactors return self.getAncestor(fn=lambda x: isinstance(x, reactors.Reactor)) def __repr__(self): return "<AssemblyList object: {0}>".format(self.name)
[docs] def add(self, assem, loc=None): """ Add an Assembly to the list. Parameters ---------- assem : Assembly The Assembly to add to the list loc : LocationBase, optional If provided, the assembly is inserted at that location. If it is not provided, the locator on the Assembly object will be used. If the Assembly's locator belongs to ``self.spatialGrid``, the Assembly's existing locator will not be used. This is unlike the Core, which would try to use the same indices, but move the locator to the Core's grid. With a locator, the associated ``AutoFiller`` will be used. """ if loc is not None and loc.grid is not self.spatialGrid: raise ValueError( "An assembly cannot be added to {} using a spatial locator " "from another grid".format(self) ) locProvided = loc is not None or ( assem.spatialLocator is not None and assem.spatialLocator.grid is self.spatialGrid ) if locProvided: loc = loc or assem.spatialLocator else: loc = self._filler.getNextLocation(assem) super().add(assem) assem.spatialLocator = loc self._filler.assemblyAdded(assem)
[docs] def getAssembly(self, name): """Get a specific Assembly by name.""" for a in self.getChildren(): if a.getName() == name: return a return None
[docs] def count(self): if not self.getChildren(): return runLog.important("Count:") totCount = 0 thisTimeCount = 0 a = self.getChildren()[0] lastTime = a.getAge() / units.DAYS_PER_YEAR + a.p.chargeTime for a in self.getChildren(): thisTime = a.getAge() / units.DAYS_PER_YEAR + a.p.chargeTime if thisTime != lastTime: runLog.important( "Number of assemblies moved at t={0:6.2f}: {1:04d}. Cumulative: {2:04d}".format( lastTime, thisTimeCount, totCount ) ) lastTime = thisTime thisTimeCount = 0 totCount += 1 thisTimeCount += 1
[docs]class SpentFuelPool(AssemblyList): """A place to put assemblies when they've been discharged. Can tell you inventory stats, etc."""
[docs] def add(self, assem, loc=None): """ Add an Assembly to the list. Parameters ---------- assem : Assembly The Assembly to add to the list loc : LocationBase, optional If provided, the assembly is inserted at that location. If it is not provided, the locator on the Assembly object will be used. If the Assembly's locator belongs to ``self.spatialGrid``, the Assembly's existing locator will not be used. This is unlike the Core, which would try to use the same indices, but move the locator to the Core's grid. With a locator, the associated ``AutoFiller`` will be used. """ # If the assembly added has a negative ID, that is a placeholder, fix it. if assem.p.assemNum < 0: # update the assembly count in the Reactor newNum = self.r.incrementAssemNum() assem.renumber(newNum) super().add(assem, loc)
[docs] def report(self): title = "{0} Report".format(self.name) runLog.important("-" * len(title)) runLog.important(title) runLog.important("-" * len(title)) totFis = 0.0 for a in self.getChildren(): runLog.important( "{assembly:15s} discharged at t={dTime:10f} after {residence:10f} yrs. It entered at cycle: {cycle}. " "It has {fiss:10f} kg (x {mult}) fissile and peak BU={bu:.2f} %.".format( assembly=a, dTime=a.p.dischargeTime, residence=(a.p.dischargeTime - a.p.chargeTime), cycle=a.p.chargeCycle, fiss=a.getFissileMass() * a.p.multiplicity, bu=a.getMaxParam("percentBu"), mult=a.p.multiplicity, ) ) totFis += a.getFissileMass() * a.p.multiplicity / 1000 # convert to kg runLog.important( "Total full-core fissile inventory of {0} is {1:.4E} MT".format( self, totFis / 1000.0 ) )
[docs] def normalizeNames(self, startIndex=0): """ Renumber and rename all the Assemblies and Blocks. Parameters ---------- startIndex : int, optional The default is to start counting at zero. But if you are renumbering assemblies across the entire Reactor, you may want to start at a different number. Returns ------- int The new max Assembly number. """ ind = startIndex for a in self.getChildren(): oldName = a.getName() newName = a.makeNameFromAssemNum(ind) if oldName == newName: ind += 1 continue a.p.assemNum = ind a.setName(newName) for b in a: axialIndex = int(b.name.split("-")[-1]) b.name = b.makeName(ind, axialIndex) ind += 1 return int