diff --git a/MANIFEST.in b/MANIFEST.in index 25454e9b0..372ebd74e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include armi/bookkeeping/tests/armiRun-A0039-aHist-ref.txt +include armi/bookkeeping/tests/armiRun-A0032-aHist-ref.txt include armi/nuclearDataIO/cccc/tests/fixtures/labels.ascii include armi/nuclearDataIO/cccc/tests/fixtures/labels.binary include armi/nuclearDataIO/cccc/tests/fixtures/mc2v3.dlayxs diff --git a/armi/bookkeeping/db/__init__.py b/armi/bookkeeping/db/__init__.py index 213485398..6a1d69cdd 100644 --- a/armi/bookkeeping/db/__init__.py +++ b/armi/bookkeeping/db/__init__.py @@ -67,7 +67,7 @@ # re-export package components for easier import from .permissions import Permissions -from .database3 import Database3, updateGlobalAssemblyNum +from .database3 import Database3 from .databaseInterface import DatabaseInterface from .compareDB3 import compareDatabases from .factory import databaseFactory @@ -144,12 +144,6 @@ def loadOperator(pathToDb, loadCycle, loadNode, allowMissing=False): settings.setMasterCs(cs) - # Update the global assembly number because, if the user is loading a reactor from - # blueprints and does not have access to an operator, it is unlikely that there is - # another reactor that has alter the global assem num. Fresh cases typically want - # this updated. - updateGlobalAssemblyNum(r) - o = thisCase.initializeOperator(r=r) runLog.important( "The operator will not be in the same state that it was at that cycle and " diff --git a/armi/bookkeeping/db/database3.py b/armi/bookkeeping/db/database3.py index 30dc69cae..5f8cc3bfc 100644 --- a/armi/bookkeeping/db/database3.py +++ b/armi/bookkeeping/db/database3.py @@ -70,7 +70,6 @@ from armi.bookkeeping.db.typedefs import History, Histories from armi.nucDirectory import nuclideBases from armi.physics.neutronics.settings import CONF_LOADING_FILE -from armi.reactor import assemblies from armi.reactor import grids from armi.reactor import parameters from armi.reactor import systemLayoutInput @@ -101,18 +100,6 @@ def getH5GroupName(cycle: int, timeNode: int, statePointName: str = None) -> str return "c{:0>2}n{:0>2}{}".format(cycle, timeNode, statePointName or "") -def updateGlobalAssemblyNum(r) -> None: - """ - Updated the global assembly number counter in ARMI, using the assemblies - read from a database. - """ - assemNum = r.core.p.maxAssemNum - if assemNum is not None: - assemblies.setAssemNumCounter(int(assemNum + 1)) - else: - raise ValueError("Could not load maxAssemNum from the database") - - class Database3: """ Version 3 of the ARMI Database, handling serialization and loading of Reactor states. @@ -691,8 +678,7 @@ def load( Whether to emit a warning, rather than crash if reading a database with undefined parameters. Default False. updateGlobalAssemNum : bool, optional - Whether to update the global assembly number to the value stored in - r.core.p.maxAssemNum. Default True. + DeprecationWarning: This is unused. updateMasterCs : bool, optional Whether to apply the cs (whether provided as an argument or read from the database) as the primary for the case. Default True. Can be useful @@ -741,9 +727,11 @@ def load( ) root = comps[0][0] - # ensure the max assembly number is correct, unless the user says no if updateGlobalAssemNum: - updateGlobalAssemblyNum(root) + runLog.warning( + "The method input `updateGlobalAssemNum` is no longer used.", + single=True, + ) # return a Reactor object if cs[CONF_SORT_REACTOR]: diff --git a/armi/bookkeeping/db/tests/test_comparedb3.py b/armi/bookkeeping/db/tests/test_comparedb3.py index b195b566a..4c86c6706 100644 --- a/armi/bookkeeping/db/tests/test_comparedb3.py +++ b/armi/bookkeeping/db/tests/test_comparedb3.py @@ -125,7 +125,7 @@ def test_compareDatabaseDuplicate(self): self.assertEqual(diffs.nDiffs(), 0) def test_compareDatabaseSim(self): - """end-to-end test of compareDatabases() on very simlar databases.""" + """End-to-end test of compareDatabases() on very simlar databases.""" # build two super-simple H5 files for testing o, r = test_reactors.loadTestReactor( TEST_ROOT, customSettings={"reloadDBName": "reloadingDB.h5"} @@ -177,7 +177,7 @@ def test_compareDatabaseSim(self): dbs[1]._fullPath, timestepCompare=[(0, 0), (0, 1)], ) - self.assertEqual(len(diffs.diffs), 465) + self.assertEqual(len(diffs.diffs), 468) # Cycle length is only diff (x3) self.assertEqual(diffs.nDiffs(), 3) diff --git a/armi/bookkeeping/db/tests/test_database3.py b/armi/bookkeeping/db/tests/test_database3.py index b72252551..3cc72635e 100644 --- a/armi/bookkeeping/db/tests/test_database3.py +++ b/armi/bookkeeping/db/tests/test_database3.py @@ -81,7 +81,14 @@ def test_writeToDB(self): self.assertEqual(sorted(self.db.h5db["c00n00"].keys()), sorted(keys)) # validate availabilityFactor did not make it into the H5 file - rKeys = ["cycle", "cycleLength", "flags", "serialNum", "timeNode"] + rKeys = [ + "maxAssemNum", + "cycle", + "cycleLength", + "flags", + "serialNum", + "timeNode", + ] self.assertEqual( sorted(self.db.h5db["c00n00"]["Reactor"].keys()), sorted(rKeys) ) @@ -109,8 +116,7 @@ def makeShuffleHistory(self): # Serial numbers *are not stable* (i.e., they can be different between test runs # due to parallelism and test run order). However, they are the simplest way to # check correctness of location-based history tracking. So we stash the serial - # numbers at the location of interest so that we can use them later to check our - # work. + # numbers at the location of interest so we can use them later to check our work. self.centralAssemSerialNums = [] self.centralTopBlockSerialNums = [] @@ -365,39 +371,6 @@ def test_loadSortSetting(self): self.assertEqual(len(r0), len(r1)) self.assertEqual(len(r0.core), len(r1.core)) - def test_load_updateGlobalAssemNum(self): - from armi.reactor import assemblies - from armi.reactor.assemblies import resetAssemNumCounter - - self.makeHistory() - - resetAssemNumCounter() - self.assertEqual(assemblies._assemNum, 0) - - r = self.db.load(0, 0, allowMissing=True, updateGlobalAssemNum=False) - # len(r.sfp) is zero but these nums are still reserved - numSFPBlueprints = 4 - expectedNum = len(r.core) + numSFPBlueprints - self.assertEqual(assemblies._assemNum, expectedNum) - - # now do the same call again and show that the global _assemNum keeps going up. - # in db.load, rector objects are built in layout._initComps() so the global assem num - # will continue to grow (in this case, double). - self.db.load(0, 0, allowMissing=True, updateGlobalAssemNum=False) - self.assertEqual(assemblies._assemNum, expectedNum * 2) - - # now load but set updateGlobalAssemNum=True and show that the global assem num - # is updated and equal to self.r.p.maxAssemNum + 1 which is equal to the number of - # assemblies in blueprints/core. - r = self.db.load(0, 0, allowMissing=True, updateGlobalAssemNum=True) - expected = len(self.r.core) + len(self.r.blueprints.assemblies.values()) - self.assertEqual(15, expected) - - # repeat the test above to show that subsequent db loads (with updateGlobalAssemNum=True) - # do not continue to increase the global assem num. - self.db.load(0, 0, allowMissing=True, updateGlobalAssemNum=True) - self.assertEqual(15, expected) - def test_history(self): self.makeShuffleHistory() diff --git a/armi/bookkeeping/tests/armiRun-A0039-aHist-ref.txt b/armi/bookkeeping/tests/armiRun-A0032-aHist-ref.txt similarity index 98% rename from armi/bookkeeping/tests/armiRun-A0039-aHist-ref.txt rename to armi/bookkeeping/tests/armiRun-A0032-aHist-ref.txt index d08e9d04a..c8911acb8 100644 --- a/armi/bookkeeping/tests/armiRun-A0039-aHist-ref.txt +++ b/armi/bookkeeping/tests/armiRun-A0032-aHist-ref.txt @@ -47,7 +47,7 @@ EOL bottom top center Assembly info -A0039 outer core fuel +A0032 outer core fuel "reflector" C A "fuel" C A "fuel" C A diff --git a/armi/mpiActions.py b/armi/mpiActions.py index e9c12e404..383adf9aa 100644 --- a/armi/mpiActions.py +++ b/armi/mpiActions.py @@ -68,7 +68,6 @@ from armi import settings from armi import utils from armi.reactor import reactors -from armi.reactor import assemblies from armi.reactor.parameters import parameterDefinitions from armi.utils import iterables @@ -610,8 +609,6 @@ def _distributeReactor(self, cs): runLog.debug( "The reactor has {} assemblies".format(len(self.r.core.getAssemblies())) ) - numAssemblies = self.broadcast(assemblies.getAssemNum()) - assemblies.setAssemNumCounter(numAssemblies) # attach here so any interface actions use a properly-setup reactor. self.o.reattach(self.r, cs) # sets r and cs diff --git a/armi/physics/fuelCycle/fuelHandlerInterface.py b/armi/physics/fuelCycle/fuelHandlerInterface.py index 4eccd3e64..dbaf5db37 100644 --- a/armi/physics/fuelCycle/fuelHandlerInterface.py +++ b/armi/physics/fuelCycle/fuelHandlerInterface.py @@ -105,6 +105,7 @@ def manageFuel(self, cycle): self.r.core.locateAllAssemblies() shuffleFactors, _ = fh.getFactorList(cycle) fh.outage(shuffleFactors) # move the assemblies around + if self.cs[CONF_PLOT_SHUFFLE_ARROWS]: arrows = fh.makeShuffleArrows() plotting.plotFaceMap( diff --git a/armi/physics/fuelCycle/fuelHandlers.py b/armi/physics/fuelCycle/fuelHandlers.py index 67758029f..1d51ae223 100644 --- a/armi/physics/fuelCycle/fuelHandlers.py +++ b/armi/physics/fuelCycle/fuelHandlers.py @@ -90,7 +90,7 @@ def r(self): return self.o.r def outage(self, factor=1.0): - r""" + """ Simulates a reactor reload outage. Moves and tracks fuel. This sets the moveList structure. diff --git a/armi/physics/fuelCycle/tests/test_fuelHandlers.py b/armi/physics/fuelCycle/tests/test_fuelHandlers.py index cccf1b4aa..c4ebaa7d9 100644 --- a/armi/physics/fuelCycle/tests/test_fuelHandlers.py +++ b/armi/physics/fuelCycle/tests/test_fuelHandlers.py @@ -398,7 +398,7 @@ def test_repeatShuffles(self): for a in self.r.sfp.getChildren(): self.assertEqual(a.getLocation(), "SFP") - # do some shuffles. + # do some shuffles fh = self.r.o.getInterface("fuelHandler") self.runShuffling(fh) # changes caseTitle @@ -455,7 +455,7 @@ def test_readMoves(self): sfpMove = moves[2][-2] self.assertEqual(sfpMove[0], "SFP") self.assertEqual(sfpMove[1], "005-003") - self.assertEqual(sfpMove[4], "A0085") # name of assem in SFP + self.assertEqual(sfpMove[4], "A0077") # name of assem in SFP def test_processMoveList(self): fh = fuelHandlers.FuelHandler(self.o) @@ -468,7 +468,7 @@ def test_processMoveList(self): loadNames, _, ) = fh.processMoveList(moves[2]) - self.assertIn("A0085", loadNames) + self.assertIn("A0077", loadNames) self.assertIn(None, loadNames) self.assertNotIn("SFP", loadChains) self.assertNotIn("LoadQueue", loadChains) diff --git a/armi/reactor/assemblies.py b/armi/reactor/assemblies.py index da89dfd5e..a0dbc5a41 100644 --- a/armi/reactor/assemblies.py +++ b/armi/reactor/assemblies.py @@ -20,6 +20,7 @@ import copy import math import pickle +from random import randint import numpy from scipy import interpolate @@ -33,31 +34,6 @@ from armi.reactor.flags import Flags from armi.reactor.parameters import ParamLocation -# to count the blocks that we create and generate a block number -_assemNum = 0 - - -def incrementAssemNum(): - global _assemNum # tracked on a module level - val = _assemNum # return value before incrementing. - _assemNum += 1 - return val - - -def getAssemNum(): - global _assemNum - return _assemNum - - -def resetAssemNumCounter(): - setAssemNumCounter(0) - - -def setAssemNumCounter(val): - runLog.extra("Resetting global assembly number to {0}".format(val)) - global _assemNum - _assemNum = val - class Assembly(composites.Composite): """ @@ -80,8 +56,7 @@ class Assembly(composites.Composite): SPENT_FUEL_POOL = "SFP" # For assemblies coming in from the database, waiting to be loaded to their old # position. This is a necessary distinction, since we need to make sure that a bunch - # of fuel management stuff doesn't treat its re-placement into the core as a new - # move + # of fuel management stuff doesn't treat its re-placement into the core as a new move DATABASE = "database" NOT_IN_CORE = [LOAD_QUEUE, SPENT_FUEL_POOL] @@ -93,11 +68,13 @@ def __init__(self, typ, assemNum=None): Name of assembly design (e.g. the name from the blueprints input file). assemNum : int, optional - The unique ID number of this assembly. If none is passed, the class-level - value will be taken and then incremented. + The unique ID number of this assembly. If None is provided, we generate a + random int. This makes it clear that it is a placeholder. When an assembly with + a negative ID is placed into a Reactor, it will be given a new, positive ID. """ + # If no assembly number is provided, generate a random number as a placeholder. if assemNum is None: - assemNum = incrementAssemNum() + assemNum = randint(-9e12, -1) name = self.makeNameFromAssemNum(assemNum) composites.Composite.__init__(self, name) self.p.assemNum = assemNum @@ -137,18 +114,24 @@ def __lt__(self, other): except ValueError: return False - def makeUnique(self): + def renameBlocksAccordingToAssemblyNum(self): """ - Function to make an assembly unique by getting a new assembly number. + Updates the names of all blocks to comply with the assembly number. - This also adjusts the assembly's blocks IDs. This is necessary when using - ``deepcopy`` to get a unique ``assemNum`` since a deepcopy implies it would - otherwise have been the same object. + Useful after an assembly number/name has been loaded from a snapshot and you + want to update all block names to be consistent. + + It may be better to store block numbers on each block as params. A database that + can hold strings would be even better. + + Notes + ----- + You must run armi.reactor.reactors.Reactor.regenAssemblyLists after calling + this. """ - self.p.assemNum = incrementAssemNum() - self.name = self.makeNameFromAssemNum(self.p.assemNum) + assemNum = self.getNum() for bi, b in enumerate(self): - b.setName(b.makeName(self.p.assemNum, bi)) + b.setName(b.makeName(assemNum, bi)) @staticmethod def makeNameFromAssemNum(assemNum): @@ -157,8 +140,35 @@ def makeNameFromAssemNum(assemNum): AssemNums are like serial numbers for assemblies. """ - name = "A{0:04d}".format(int(assemNum)) - return name + return "A{0:04d}".format(int(assemNum)) + + def renumber(self, newNum): + """ + Change the assembly number of this assembly. + + And handle the downstream impacts of changing the name of this Assembly and all + of the Blocks within this Assembly. + + Parameters + ---------- + newNum : int + The new Assembly number. + """ + self.p.assemNum = int(newNum) + self.name = self.makeNameFromAssemNum(self.p.assemNum) + self.renameBlocksAccordingToAssemblyNum() + + def makeUnique(self): + """ + Function to make an assembly unique by getting a new assembly number. + + This also adjusts the assembly's blocks IDs. This is necessary when using + ``deepcopy`` to get a unique ``assemNum`` since a deepcopy implies it would + otherwise have been the same object. + """ + # Default to a random negative assembly number (unique enough) + self.p.assemNum = randint(-9e12, -1) + self.renumber(self.p.assemNum) def add(self, obj): """ @@ -169,8 +179,7 @@ def add(self, obj): """ composites.Composite.add(self, obj) obj.spatialLocator = self.spatialGrid[0, 0, len(self) - 1] - # assemblies have bounds-based 1-D spatial grids. Adjust it to have the right - # value. + # assemblies have bounds-based 1-D spatial grids. Adjust it to the right value. if len(self.spatialGrid._bounds[2]) < len(self): self.spatialGrid._bounds[2][len(self)] = ( self.spatialGrid._bounds[2][len(self) - 1] + obj.getHeight() @@ -1132,25 +1141,6 @@ def reestablishBlockOrder(self): # update the name too. NOTE: You must update the history tracker. b.setName(b.makeName(self.p.assemNum, zi)) - def renameBlocksAccordingToAssemblyNum(self): - """ - Updates the names of all blocks to comply with the assembly number. - - Useful after an assembly number/name has been loaded from a snapshot and you - want to update all block names to be consistent. - - It may be better to store block numbers on each block as params. A database that - can hold strings would be even better. - - Notes - ----- - You must run armi.reactor.reactors.Reactor.regenAssemblyLists after calling - this. - """ - assemNum = self.getNum() - for bi, b in enumerate(self): - b.setName(b.makeName(assemNum, bi)) - def countBlocksWithFlags(self, blockTypeSpec=None): """ Returns the number of blocks of a specified type. diff --git a/armi/reactor/assemblyLists.py b/armi/reactor/assemblyLists.py index 71ba63ed3..97aa7259e 100644 --- a/armi/reactor/assemblyLists.py +++ b/armi/reactor/assemblyLists.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -r""" +""" Module containing :py:class:`AssemblyList` and related classes. Assembly Lists are core-like objects that store collections of Assemblies. They were @@ -21,7 +21,8 @@ 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. +``Core``, and moving them into sibling systems on the root +:py:class:`armi.reactor.reactors.Reactor` object. """ import abc import itertools @@ -129,15 +130,12 @@ def add(self, assem, loc=None): The Assembly to add to the list loc : LocationBase, optional - If provided, the assembly is inserted at that location, similarly to how a - Core would function. 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. If no locator is passed, or if the - Assembly's locator is not in the AssemblyList's grid, then the Assembly will - be automatically located in the grid using the associated ``AutoFiller`` - object. + 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( @@ -170,6 +168,7 @@ def getAssembly(self, name): def count(self): if not self.getChildren(): return + runLog.important("Count:") totCount = 0 thisTimeCount = 0 @@ -194,6 +193,31 @@ def count(self): class SpentFuelPool(AssemblyList): """A place to put assemblies when they've been discharged. Can tell you inventory stats, etc.""" + 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) + def report(self): title = "{0} Report".format(self.name) runLog.important("-" * len(title)) @@ -219,3 +243,37 @@ def report(self): self, totFis / 1000.0 ) ) + + 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 diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py index 1b2322766..40f90b919 100644 --- a/armi/reactor/blocks.py +++ b/armi/reactor/blocks.py @@ -266,7 +266,6 @@ def getSmearDensity(self, cold=True): ------- smearDensity : float The smear density as a fraction - """ fuels = self.getComponents(Flags.FUEL) if not fuels: diff --git a/armi/reactor/blueprints/__init__.py b/armi/reactor/blueprints/__init__.py index 0197126ac..923e6a94b 100644 --- a/armi/reactor/blueprints/__init__.py +++ b/armi/reactor/blueprints/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -r""" +""" Blueprints describe the geometric and composition details of the objects in the reactor (e.g. fuel assemblies, control rods, etc.). @@ -305,23 +305,11 @@ def _prepConstruction(self, cs): self._assembliesBySpecifier.clear() self.assemblies.clear() - # retrieve current count of assemblies to restore after - # creating blueprints assemblies. This is particularly useful for - # doing snapshot based runs and multiple database loads, and ensures - # that each database load/snapshot to not cumulative increase the assembly - # count during creation of blueprints assemblies. During initial - # constructions the first N numbers are reserved for blueprints, so this - # ensures consistency. - currentCount = assemblies.getAssemNum() - # reset the assembly counter so that blueprints assemblies are always - # numbered 0 to len(self.assemDesigns) - assemblies.resetAssemNumCounter() for aDesign in self.assemDesigns: a = aDesign.construct(cs, self) self._assembliesBySpecifier[aDesign.specifier] = a self.assemblies[aDesign.name] = a - if currentCount != 0: - assemblies.setAssemNumCounter(currentCount) + runLog.header("=========== Verifying Assembly Configurations ===========") self._checkAssemblyAreaConsistency(cs) diff --git a/armi/reactor/converters/geometryConverters.py b/armi/reactor/converters/geometryConverters.py index 75ae2b003..6c05a5269 100644 --- a/armi/reactor/converters/geometryConverters.py +++ b/armi/reactor/converters/geometryConverters.py @@ -96,10 +96,6 @@ def reset(self): runLog.info( f"Resetting the state of the converted reactor core model in {self}" ) - currentAssemCounter = assemblies.getAssemNum() - assemblies.setAssemNumCounter( - currentAssemCounter - len(self._newAssembliesAdded) - ) self._newAssembliesAdded = [] @@ -253,7 +249,7 @@ def convert(self, r): ) def addRing(self, assemType="big shield"): - r""" + """ Add a ring of fuel assemblies around the outside of an existing core. Works by first finding the assembly furthest from the center, then filling in @@ -1428,8 +1424,8 @@ def addEdgeAssemblies(self, core): ) def removeEdgeAssemblies(self, core): - r""" - remove the edge assemblies in preparation for the nodal diffusion approximation. + """ + Remove the edge assemblies in preparation for the nodal diffusion approximation. This makes use of the assemblies knowledge of if it is in a region that it needs to be removed. @@ -1464,7 +1460,8 @@ def removeEdgeAssemblies(self, core): pDefs = parameters.ALL_DEFINITIONS.unchanged_since(NEVER) pDefs.setAssignmentFlag(SINCE_LAST_GEOMETRY_TRANSFORMATION) else: - runLog.extra("No edge assemblies to remove") + runLog.debug("No edge assemblies to remove.") + self.reset() @staticmethod diff --git a/armi/reactor/converters/tests/test_uniformMesh.py b/armi/reactor/converters/tests/test_uniformMesh.py index fb1ad27a6..a5a0869bd 100644 --- a/armi/reactor/converters/tests/test_uniformMesh.py +++ b/armi/reactor/converters/tests/test_uniformMesh.py @@ -366,6 +366,8 @@ def applyNonUniformHeightDistribution(reactor): a[-1].setHeight(a[-1].getHeight() - delta) a.calculateZCoords() + reactor.normalizeNames() + class TestUniformMesh(unittest.TestCase): """ @@ -411,7 +413,7 @@ def test_convertNumberDensities(self): ) # conversion didn't change source reactor mass def test_applyStateToOriginal(self): - applyNonUniformHeightDistribution(self.r) # note: this perturbs the ref. mass + applyNonUniformHeightDistribution(self.r) # note: this perturbs the ref mass self.converter.convert(self.r) for ib, b in enumerate(self.converter.convReactor.core.getBlocks()): @@ -685,7 +687,6 @@ def test_reactorConversion(self): self.assertFalse(b.p.rateAbs) self.converter.convert(self.r) - self.assertEqual( len(controlAssems), len(self.converter._nonUniformAssemStorage), diff --git a/armi/reactor/converters/uniformMesh.py b/armi/reactor/converters/uniformMesh.py index a89349ace..c404b4b80 100644 --- a/armi/reactor/converters/uniformMesh.py +++ b/armi/reactor/converters/uniformMesh.py @@ -160,6 +160,7 @@ def _computeAverageAxialMesh(self): aMesh = src.core.findAllAxialMeshPoints([a])[1:] if len(aMesh) == refNumPoints: allMeshes.append(aMesh) + averageMesh = average1DWithinTolerance(numpy.array(allMeshes)) self._commonMesh = numpy.array(averageMesh) @@ -433,12 +434,12 @@ def convert(self, r=None): includePinCoordinates=self.includePinCoordinates, ) homogAssem.spatialLocator = assem.spatialLocator + homogAssem.p.assemNum = assem.p.assemNum - # Remove this assembly from the core and add it to the - # temporary storage list so that it can be replaced with the homogenized assembly. - # Note that we do not call the `removeAssembly` method because - # this will delete the core assembly from existence rather than - # only stripping its spatialLocator away. + # Remove this assembly from the core and add it to the temporary storage + # so that it can be replaced with the homogenized assembly. Note that we + # do not call `removeAssembly()` because this will delete the core + # assembly from existence rather than only stripping its spatialLocator. if assem.spatialLocator in self.convReactor.core.childrenByLocator: self.convReactor.core.childrenByLocator.pop(assem.spatialLocator) self.convReactor.core.remove(assem) @@ -449,7 +450,6 @@ def convert(self, r=None): assem.setName(assem.getName() + self._TEMP_STORAGE_NAME_SUFFIX) self._nonUniformAssemStorage.add(assem) self.convReactor.core.add(homogAssem) - else: runLog.extra(f"Building copy of {r} with a uniform axial mesh.") self.convReactor = self.initNewReactor(r, self._cs) diff --git a/armi/reactor/reactorParameters.py b/armi/reactor/reactorParameters.py index 2fe1bc5bc..c30979741 100644 --- a/armi/reactor/reactorParameters.py +++ b/armi/reactor/reactorParameters.py @@ -86,6 +86,13 @@ def defineReactorParameters(): "timeNode", units=units.UNITLESS, description="Integer timeNode", default=0 ) + pb.defParam( + "maxAssemNum", + units=units.UNITLESS, + description="Max number of assemblies created so far in the Reactor (integer)", + default=0, + ) + with pDefs.createBuilder( location=ParamLocation.AVERAGE, default=0.0, categories=["economics"] ) as pb: diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index 291fd8b3f..09f4ece19 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -40,7 +40,6 @@ from armi import getPluginManagerOrFail, materials, nuclearDataIO from armi import runLog from armi.nuclearDataIO import xsLibraries -from armi.reactor import assemblies from armi.reactor import assemblyLists from armi.reactor import composites from armi.reactor import geometry @@ -75,6 +74,7 @@ def __init__(self, name, blueprints): self.o = None self.spatialGrid = None self.spatialLocator = None + self.p.maxAssemNum = 0 self.p.cycle = 0 self.p.flags |= Flags.REACTOR self.core = None @@ -111,6 +111,47 @@ def add(self, container): ) self.core = cores[0] + def incrementAssemNum(self): + """ + Increase the max assembly number by one and returns the current value. + + Notes + ----- + The "max assembly number" is not currently used in the Reactor. So the idea + is that we return the current number, then iterate it for the next assembly. + + Obviously, this method will be unused for non-assembly-based reactors. + + Returns + ------- + int + The new max Assembly number. + """ + val = int(self.p.maxAssemNum) + self.p.maxAssemNum += 1 + return val + + def normalizeNames(self): + """ + Renumber and rename all the Assemblies and Blocks. + + This method normalizes the names in the Core then the SFP. + + Returns + ------- + int + The new max Assembly number. + """ + self.p.maxAssemNum = 0 + + ind = self.core.normalizeNames(self.p.maxAssemNum) + self.p.maxAssemNum = ind + + ind = self.sfp.normalizeNames(self.p.maxAssemNum) + self.p.maxAssemNum = ind + + return ind + def loadFromCs(cs) -> Reactor: """ @@ -154,6 +195,7 @@ def factory(cs, bp, geom: Optional[SystemLayoutInput] = None) -> Reactor: ) coreDesign = bp.systemDesigns["core"] coreDesign.construct(cs, bp, r, geom=geom) + for structure in bp.systemDesigns: if structure.name.lower() != "core": structure.construct(cs, bp, r) @@ -531,6 +573,52 @@ def removeAllAssemblies(self, discharge=True): self.parent.sfp.removeAll() self.blocksByName = {} self.assembliesByName = {} + self.parent.p.maxAssemNum = 0 + + 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: + 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 + + self.normalizeInternalBookeeping() + + return ind + + def normalizeInternalBookeeping(self): + """Update some bookkeeping dictionaries of assembly and block names in this Core.""" + self.assembliesByName = {} + self.blocksByName = {} + for assem in self: + self.assembliesByName[assem.getName()] = assem + for b in assem: + self.blocksByName[b.getName()] = b def add(self, a, spatialLocator=None): """ @@ -552,6 +640,10 @@ def add(self, a, spatialLocator=None): -------- removeAssembly : removes an assembly """ + # Negative assembly IDs are placeholders, and we need to renumber the assembly + if a.p.assemNum < 0: + a.renumber(self.r.incrementAssemNum()) + # resetting .assigned forces database to be rewritten for shuffled core paramDefs = set(parameters.ALL_DEFINITIONS) paramDefs.difference_update(set(parameters.forType(Core))) @@ -595,9 +687,7 @@ def add(self, a, spatialLocator=None): runLog.error( "The assembly {1} in the reactor already has the name {0}.\nCannot add {2}. " "Current assemNum is {3}" - "".format( - aName, self.assembliesByName[aName], a, assemblies.getAssemNum() - ) + "".format(aName, self.assembliesByName[aName], a, self.r.p.maxAssemNum) ) raise RuntimeError("Core already contains an assembly with the same name.") self.assembliesByName[aName] = a @@ -1269,7 +1359,6 @@ def getNuclideCategories(self): structureNuclides : set set of nuclide names - """ if not self._nuclideCategories: coolantNuclides = set() @@ -1485,8 +1574,8 @@ def getAssembly( """ if assemblyName: return self.getAssemblyByName(assemblyName) - for a in self.getAssemblies(*args, **kwargs): + for a in self.getAssemblies(*args, **kwargs): if a.getLocation() == locationString: return a if a.getNum() == assemNum: @@ -1507,8 +1596,6 @@ def getAssemblyWithAssemNum(self, assemNum): ------- foundAssembly : Assembly object or None The assembly found, or None - - """ return self.getAssembly(assemNum=assemNum) @@ -1532,7 +1619,6 @@ def getAssemblyPitch(self): ------- pitch : float The assembly pitch. - """ return self.spatialGrid.pitch @@ -1604,7 +1690,6 @@ def findNeighbors( See Also -------- grids.Grid.getSymmetricEquivalents - """ neighborIndices = self.spatialGrid.getNeighboringCellIndices( *a.spatialLocator.getCompleteIndices() @@ -1647,6 +1732,7 @@ def _getReflectiveDuplicateAssembly(self, neighborLoc): duplicateAssem = self.childrenByLocator.get(neighborLocation2) if duplicateAssem is not None: duplicates.append(duplicateAssem) + # should always be 0 or 1 nDuplicates = len(duplicates) if nDuplicates == 1: @@ -1946,7 +2032,7 @@ def addMoreNodes(self, meshList): return meshList, True def findAllAziMeshPoints(self, extraAssems=None, applySubMesh=True): - r""" + """ Returns a list of all azimuthal (theta)-mesh positions in the core. Parameters @@ -1957,7 +2043,6 @@ def findAllAziMeshPoints(self, extraAssems=None, applySubMesh=True): applySubMesh : bool generates submesh points to further discretize the theta reactor mesh - """ i, _, _ = self.findAllMeshPoints(extraAssems, applySubMesh) return i @@ -1966,7 +2051,6 @@ def findAllRadMeshPoints(self, extraAssems=None, applySubMesh=True): """ Return a list of all radial-mesh positions in the core. - Parameters ---------- extraAssems : list @@ -1976,7 +2060,6 @@ def findAllRadMeshPoints(self, extraAssems=None, applySubMesh=True): applySubMesh : bool (not implemented) generates submesh points to further discretize the radial reactor mesh - """ _, j, _ = self.findAllMeshPoints(extraAssems, applySubMesh) return j @@ -2004,7 +2087,7 @@ def getMaxNumPins(self): return max(b.getNumPins() for b in self.getBlocks()) def getMinimumPercentFluxInFuel(self, target=0.005): - r""" + """ Goes through the entire reactor to determine what percentage of flux occures at each ring. Starting with the outer ring, this function helps determine the effective size of the core where additional assemblies will not help the breeding in the TWR. diff --git a/armi/reactor/tests/test_assemblies.py b/armi/reactor/tests/test_assemblies.py index f8a93e460..21d916e2b 100644 --- a/armi/reactor/tests/test_assemblies.py +++ b/armi/reactor/tests/test_assemblies.py @@ -41,8 +41,6 @@ from armi.utils import directoryChangers from armi.utils import textProcessors from armi.reactor.tests import test_reactors -from armi.reactor.assemblies import getAssemNum -from armi.reactor.assemblies import resetAssemNumCounter from armi.physics.neutronics.settings import ( CONF_LOADING_FILE, CONF_XS_KERNEL, @@ -288,12 +286,6 @@ def test_notesParameter(self): self.assembly.p.notes = tooLongNote self.assertEqual(self.assembly.p.notes, tooLongNote[0:1000]) - def test_resetAssemNumCounter(self): - resetAssemNumCounter() - cur = 0 - ref = getAssemNum() - self.assertEqual(cur, ref) - def test_iter(self): cur = [] for block in self.assembly: diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index efcf9f211..eccc2428a 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -172,12 +172,10 @@ def loadTestReactor( fName = os.path.join(inputFilePath, inputFileName) customSettings = customSettings or {} isPickeledReactor = fName == ARMI_RUN_PATH and customSettings == {} - assemblies.resetAssemNumCounter() if isPickeledReactor and TEST_REACTOR: # return test reactor only if no custom settings are needed. o, r, assemNum = cPickle.loads(TEST_REACTOR) - assemblies.setAssemNumCounter(assemNum) settings.setMasterCs(o.cs) o.reattach(r, o.cs) return o, r @@ -210,7 +208,7 @@ def loadTestReactor( if isPickeledReactor: # cache it for fast load for other future tests # protocol=2 allows for classes with __slots__ but not __getstate__ to be pickled - TEST_REACTOR = cPickle.dumps((o, o.r, assemblies.getAssemNum()), protocol=2) + TEST_REACTOR = cPickle.dumps((o, o.r, o.r.p.maxAssemNum), protocol=2) return o, o.r @@ -321,7 +319,7 @@ def test_getBlocksByIndices(self): indices = [(1, 1, 1), (3, 2, 2)] actualBlocks = self.r.core.getBlocksByIndices(indices) actualNames = [b.getName() for b in actualBlocks] - expectedNames = ["B0022-001", "B0043-002"] + expectedNames = ["B0014-001", "B0035-002"] self.assertListEqual(expectedNames, actualNames) def test_getAllXsSuffixes(self): @@ -339,6 +337,28 @@ def test_countBlocksOfType(self): ) self.assertEqual(numControlBlocks, 3) + def test_normalizeNames(self): + # these are the correct, normalized names + numAssems = 73 + a = self.r.core.getFirstAssembly() + correctNames = [a.makeNameFromAssemNum(n) for n in range(numAssems)] + + # validate the reactor is what we think now + self.assertEqual(len(self.r.core), numAssems) + currentNames = [a.getName() for a in self.r.core] + self.assertNotEqual(correctNames, currentNames) + + # validate that we can normalize the names correctly once + self.r.normalizeNames() + currentNames = [a.getName() for a in self.r.core] + self.assertEqual(correctNames, currentNames) + + # validate that repeated applications of this method are stable + for _ in range(3): + self.r.normalizeNames() + currentNames = [a.getName() for a in self.r.core] + self.assertEqual(correctNames, currentNames) + def test_setB10VolOnCreation(self): """Test the setting of b.p.initialB10ComponentVol.""" for controlBlock in self.r.core.getBlocks(Flags.CONTROL): @@ -634,10 +654,11 @@ def test_getMinimumPercentFluxInFuel(self): def test_getAssembly(self): a1 = self.r.core.getAssemblyWithAssemNum(assemNum=10) - a2 = self.r.core.getAssembly(locationString="005-023") + a2 = self.r.core.getAssembly(locationString="003-001") a3 = self.r.core.getAssembly(assemblyName="A0010") - self.assertEqual(a1, a2) + self.assertEqual(a1, a3) + self.assertEqual(a1, a2) def test_restoreReactor(self): aListLength = len(self.r.core.getAssemblies()) diff --git a/armi/tests/armiRun-SHUFFLES.txt b/armi/tests/armiRun-SHUFFLES.txt index 7655f1260..e17307d19 100644 --- a/armi/tests/armiRun-SHUFFLES.txt +++ b/armi/tests/armiRun-SHUFFLES.txt @@ -13,7 +13,7 @@ Before cycle 2: 005-004 moved to 004-001 with assembly type igniter fuel with enrich list: 0.00000000 0.11000000 0.11000000 0.11000000 0.00000000 006-004 moved to 005-004 with assembly type igniter fuel with enrich list: 0.00000000 0.11000000 0.11000000 0.11000000 0.00000000 LoadQueue moved to 006-004 with assembly type igniter fuel with enrich list: 0.00000000 0.11000000 0.11000000 0.11000000 0.00000000 -SFP moved to 005-003 with assembly type feed fuel ANAME=A0085 with enrich list: 0.00000000 0.11000000 0.11000000 0.11000000 0.00000000 +SFP moved to 005-003 with assembly type feed fuel ANAME=A0077 with enrich list: 0.00000000 0.11000000 0.11000000 0.11000000 0.00000000 005-003 moved to SFP with assembly type igniter fuel with enrich list: 0.00000000 0.11000000 0.11000000 0.11000000 0.00000000 Before cycle 3: @@ -23,6 +23,6 @@ Before cycle 3: 005-002 moved to 004-003 with assembly type igniter fuel with enrich list: 0.00000000 0.11000000 0.11000000 0.11000000 0.00000000 006-007 moved to 005-002 with assembly type igniter fuel with enrich list: 0.00000000 0.11000000 0.11000000 0.11000000 0.00000000 LoadQueue moved to 006-007 with assembly type igniter fuel with enrich list: 0.00000000 0.11000000 0.11000000 0.11000000 0.00000000 -SFP moved to 005-004 with assembly type feed fuel ANAME=A0086 with enrich list: 0.00000000 0.11000000 0.11000000 0.11000000 0.00000000 +SFP moved to 005-004 with assembly type feed fuel ANAME=A0078 with enrich list: 0.00000000 0.11000000 0.11000000 0.11000000 0.00000000 005-004 moved to SFP with assembly type igniter fuel with enrich list: 0.00000000 0.11000000 0.11000000 0.11000000 0.00000000 diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst index 44065b86c..a3026ffea 100644 --- a/doc/release/0.2.rst +++ b/doc/release/0.2.rst @@ -11,6 +11,7 @@ What's new in ARMI #. The Spent Fuel Pool (``sfp``) was moved from the ``Core`` out to the ``Reactor``. (`PR#1336 `_) #. Broad cleanup of ``Parameters``: filled in all empty units and descriptions, removed unused params. (`PR#1345 `_) #. Removed redundant ``Material.name`` variable. (`PR#1335 `_) +#. Moved the ``Reactor`` assembly number from the global scope to a ``Parameter``. (`PR#1383 `_) #. Added SHA1 hashes of XS control files to the welcome text. (`PR#1334 `_) #. Add python 3.11 to ARMI's CI testing GH actions! (`PR#1341 `_) #. Put back ``avgFuelTemp`` block parameter. (`PR#1362 `_) diff --git a/ruff.toml b/ruff.toml index 402d61c61..d852f3cf2 100644 --- a/ruff.toml +++ b/ruff.toml @@ -11,6 +11,8 @@ required-version = "0.0.272" # TID - tidy imports select = ["E", "F", "D", "N801", "SIM", "TID"] +# TODO: We want to support PLW0603 - don't use the global keyword + # Ruff rules we ignore (for now) because they are not 100% automatable # # D100 - Missing docstring in public module