diff --git a/.coveragerc b/.coveragerc index 56bf64677..fd25fbcd3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,7 +7,7 @@ omit = venv/ source = armi # change default .coverage file to something that doesn't have a dot -# because our Windows file server can't handle dots. :s +# because the Windows file server can't handle dots. data_file = coverage_results.cov [coverage:run] diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index ff649d108..aa18e74da 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -9,6 +9,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} steps: - uses: actions/checkout@v2 @@ -25,5 +26,16 @@ jobs: - name: Run Coverage Part 1 run: tox -e cov1 || true - name: Run Coverage Part 2 - run: tox -e cov2,report + run: tox -e cov2 + - name: Convert Coverage Results + env: + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + run: | + pip install coveragepy-lcov + coveragepy-lcov --data_file_path coverage_results.cov --output_file_path lcov.txt + - name: Publish to coveralls.io + uses: coverallsapp/github-action@v1.1.2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + path-to-lcov: lcov.txt diff --git a/armi/bookkeeping/db/databaseInterface.py b/armi/bookkeeping/db/databaseInterface.py index bebe0c9c0..752bad27e 100644 --- a/armi/bookkeeping/db/databaseInterface.py +++ b/armi/bookkeeping/db/databaseInterface.py @@ -144,7 +144,19 @@ def interactEveryNode(self, cycle, node): Write to database. DBs should receive the state information of the run at each node. + + Notes + ----- + - if tight coupling is enabled, the DB will be written in operator.py::Operator::_timeNodeLoop + via writeDBEveryNode """ + if self.o.cs["numCoupledIterations"]: + # h5 cant handle overwriting so we skip here and write once the tight coupling loop has completed + return + self.writeDBEveryNode(cycle, node) + + def writeDBEveryNode(self, cycle, node): + """write the database at the end of the time node""" # skip writing for last burn step since it will be written at interact EOC if node < self.o.burnSteps[cycle]: self.r.core.p.minutesSinceStart = ( diff --git a/armi/bookkeeping/db/tests/test_databaseInterface.py b/armi/bookkeeping/db/tests/test_databaseInterface.py index 877eadbaf..f0844088a 100644 --- a/armi/bookkeeping/db/tests/test_databaseInterface.py +++ b/armi/bookkeeping/db/tests/test_databaseInterface.py @@ -101,6 +101,12 @@ def tearDown(self): self.stateRetainer.__exit__() self.td.__exit__(None, None, None) + def test_interactEveryNodeReturn(self): + """test that the DB is NOT written to if cs["numCoupledIterations"] != 0""" + self.o.cs["numCoupledIterations"] = 2 + self.dbi.interactEveryNode(0, 0) + self.assertFalse(self.dbi.database.hasTimeStep(0, 0)) + def test_interactBOL(self): self.assertIsNotNone(self.dbi._db) self.dbi.interactBOL() @@ -115,6 +121,15 @@ def test_distributable(self): self.dbi.interactDistributeState() self.assertEqual(self.dbi.distributable(), 4) + def test_timeNodeLoop_numCoupledIterations(self): + """test that database is written out after the coupling loop has completed""" + # clear out interfaces (no need to run physics) but leave database + self.o.interfaces = [self.dbi] + self.o.cs["numCoupledIterations"] = 1 + self.assertFalse(self.dbi._db.hasTimeStep(0, 0)) + self.o._timeNodeLoop(0, 0) + self.assertTrue(self.dbi._db.hasTimeStep(0, 0)) + class TestDatabaseWriter(unittest.TestCase): def setUp(self): diff --git a/armi/operators/operator.py b/armi/operators/operator.py index 6ec88cc80..864e047da 100644 --- a/armi/operators/operator.py +++ b/armi/operators/operator.py @@ -380,6 +380,9 @@ def _timeNodeLoop(self, cycle, timeNode): for coupledIteration in range(self.cs["numCoupledIterations"]): self.r.core.p.coupledIteration = coupledIteration + 1 self.interactAllCoupled(coupledIteration) + # database has not yet been written, so we need to write it. + dbi = self.getInterface("database") + dbi.writeDBEveryNode(cycle, timeNode) def _interactAll(self, interactionName, activeInterfaces, *args): """ diff --git a/armi/operators/settingsValidation.py b/armi/operators/settingsValidation.py index d20eecd99..92f307091 100644 --- a/armi/operators/settingsValidation.py +++ b/armi/operators/settingsValidation.py @@ -477,6 +477,16 @@ def _willBeCopiedFrom(fName): self.NO_ACTION, ) + self.addQuery( + lambda: not self.cs["looseCoupling"] + and self.cs["numCoupledIterations"] > 0, + "You have {0} coupled iterations selected, but have not activated loose coupling.".format( + self.cs["numCoupledIterations"] + ), + "Set looseCoupling to True?", + lambda: self._assignCS("looseCoupling", True), + ) + self.addQuery( lambda: self.cs["numCoupledIterations"] > 0, "You have {0} coupling iterations selected.".format( diff --git a/armi/operators/snapshots.py b/armi/operators/snapshots.py index c4efc66d3..2ddaceb9d 100644 --- a/armi/operators/snapshots.py +++ b/armi/operators/snapshots.py @@ -63,6 +63,11 @@ def _mainOperate(self): "Beginning snapshot ({0:02d}, {1:02d})".format(ssCycle, ssNode) ) dbi.loadState(ssCycle, ssNode) + + # need to update reactor power after the database load + # this is normally handled in operator._cycleLoop + self.r.p.core.power = self.cs["power"] + halt = self.interactAllBOC(self.r.p.cycle) if halt: break diff --git a/armi/operators/tests/test_operators.py b/armi/operators/tests/test_operators.py index 6195a14e0..040b8b2a2 100644 --- a/armi/operators/tests/test_operators.py +++ b/armi/operators/tests/test_operators.py @@ -14,7 +14,7 @@ """Tests for operators""" -# pylint: disable=abstract-method,no-self-use,unused-argument +# pylint: disable=abstract-method,protected-access,unused-argument import unittest from armi import settings @@ -44,78 +44,75 @@ class InterfaceC(Interface): # TODO: Add a test that shows time evolution of Reactor (REQ_EVOLVING_STATE) class OperatorTests(unittest.TestCase): + def setUp(self): + self.o, self.r = test_reactors.loadTestReactor() + def test_addInterfaceSubclassCollision(self): - self.cs = settings.Settings() - o, r = test_reactors.loadTestReactor() + cs = settings.Settings() - interfaceA = InterfaceA(r, self.cs) + interfaceA = InterfaceA(self.r, cs) - interfaceB = InterfaceB(r, self.cs) - o.addInterface(interfaceA) + interfaceB = InterfaceB(self.r, cs) + self.o.addInterface(interfaceA) # 1) Adds B and gets rid of A - o.addInterface(interfaceB) - self.assertEqual(o.getInterface("Second"), interfaceB) - self.assertEqual(o.getInterface("First"), None) + self.o.addInterface(interfaceB) + self.assertEqual(self.o.getInterface("Second"), interfaceB) + self.assertEqual(self.o.getInterface("First"), None) # 2) Now we have B which is a subclass of A, # we want to not add A (but also not have an error) - o.addInterface(interfaceA) - self.assertEqual(o.getInterface("Second"), interfaceB) - self.assertEqual(o.getInterface("First"), None) + self.o.addInterface(interfaceA) + self.assertEqual(self.o.getInterface("Second"), interfaceB) + self.assertEqual(self.o.getInterface("First"), None) # 3) Also if another class not a subclass has the same function, # raise an error - interfaceC = InterfaceC(r, self.cs) - self.assertRaises(RuntimeError, o.addInterface, interfaceC) + interfaceC = InterfaceC(self.r, cs) + self.assertRaises(RuntimeError, self.o.addInterface, interfaceC) # 4) Check adding a different function Interface interfaceC.function = "C" - o.addInterface(interfaceC) - self.assertEqual(o.getInterface("Second"), interfaceB) - self.assertEqual(o.getInterface("Third"), interfaceC) + self.o.addInterface(interfaceC) + self.assertEqual(self.o.getInterface("Second"), interfaceB) + self.assertEqual(self.o.getInterface("Third"), interfaceC) def test_checkCsConsistency(self): - o, _r = test_reactors.loadTestReactor() - o._checkCsConsistency() # passes without error + self.o._checkCsConsistency() # passes without error - o.cs = o.cs.modified(newSettings={"nCycles": 66}) + self.o.cs = self.o.cs.modified(newSettings={"nCycles": 66}) with self.assertRaises(RuntimeError): - o._checkCsConsistency() + self.o._checkCsConsistency() def test_interfaceIsActive(self): - o, _r = test_reactors.loadTestReactor() - self.assertTrue(o.interfaceIsActive("main")) - self.assertFalse(o.interfaceIsActive("Fake-o")) + self.o, _r = test_reactors.loadTestReactor() + self.assertTrue(self.o.interfaceIsActive("main")) + self.assertFalse(self.o.interfaceIsActive("Fake-o")) def test_loadStateError(self): """The loadTestReactor() test tool does not have any history in the DB to load from""" - o, _r = test_reactors.loadTestReactor() # a first, simple test that this method fails correctly with self.assertRaises(RuntimeError): - o.loadState(0, 1) + self.o.loadState(0, 1) def test_couplingIsActive(self): - o, _r = test_reactors.loadTestReactor() - self.assertFalse(o.couplingIsActive()) + self.assertFalse(self.o.couplingIsActive()) def test_setStateToDefault(self): - o, _r = test_reactors.loadTestReactor() # reset the runType for testing - self.assertEqual(o.cs["runType"], "Standard") - o.cs = o.cs.modified(newSettings={"runType": "fake"}) - self.assertEqual(o.cs["runType"], "fake") + self.assertEqual(self.o.cs["runType"], "Standard") + self.o.cs = self.o.cs.modified(newSettings={"runType": "fake"}) + self.assertEqual(self.o.cs["runType"], "fake") # validate the method works - cs = o.setStateToDefault(o.cs) + cs = self.o.setStateToDefault(self.o.cs) self.assertEqual(cs["runType"], "Standard") def test_snapshotRequest(self): - o, _r = test_reactors.loadTestReactor() with TemporaryDirectoryChanger(): - o.snapshotRequest(0, 1) + self.o.snapshotRequest(0, 1) class CyclesSettingsTests(unittest.TestCase): diff --git a/armi/physics/neutronics/globalFlux/globalFluxInterface.py b/armi/physics/neutronics/globalFlux/globalFluxInterface.py index a1835cd5c..336110a9f 100644 --- a/armi/physics/neutronics/globalFlux/globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/globalFluxInterface.py @@ -39,7 +39,6 @@ RX_ABS_MICRO_LABELS = ["nGamma", "fission", "nalph", "np", "nd", "nt"] RX_PARAM_NAMES = ["rateCap", "rateFis", "rateProdN2n", "rateProdFis", "rateAbs"] - # pylint: disable=too-many-public-methods class GlobalFluxInterface(interfaces.Interface): """ @@ -286,7 +285,7 @@ def __init__(self, label: Optional[str] = None): self.real = True self.adjoint = False self.neutrons = True - self.photons = None + self.photons = False self.boundaryConditions = {} self.epsFissionSourceAvg = None self.epsFissionSourcePoint = None @@ -422,7 +421,8 @@ def _performGeometryTransformations(self, makePlots=False): converter = self.geomConverters.get("axial") if not converter: if self.options.detailedAxialExpansion or self.options.hasNonUniformAssems: - converter = uniformMesh.NeutronicsUniformMeshConverter( + converterCls = uniformMesh.converterFactory(self.options) + converter = converterCls( cs=self.options.cs, calcReactionRates=self.options.calcReactionRatesOnMeshConversion, ) diff --git a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py index 815b363ac..e537f7e3a 100644 --- a/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py +++ b/armi/physics/neutronics/globalFlux/tests/test_globalFluxInterface.py @@ -77,6 +77,18 @@ def getExecuterCls(self): return MockGlobalFluxExecuter +class MockGlobalFluxWithExecutersNonUniform(MockGlobalFluxWithExecuters): + def getExecuterOptions(self, label=None): + """ + Return modified executerOptions + """ + opts = globalFluxInterface.GlobalFluxInterfaceUsingExecuters.getExecuterOptions( + self, label=label + ) + opts.hasNonUniformAssems = True # to increase test coverage + return opts + + class MockGlobalFluxExecuter(globalFluxInterface.GlobalFluxExecuter): """Tests for code that uses Executers, which rely on OutputReaders to update state.""" @@ -180,6 +192,33 @@ def test_getExecuterCls(self): self.assertEqual(class0, globalFluxInterface.GlobalFluxExecuter) +class TestGlobalFluxInterfaceWithExecutersNonUniform(unittest.TestCase): + """Tests for global flux execution with non-uniform assemblies.""" + + @classmethod + def setUpClass(cls): + cs = settings.Settings() + _o, cls.r = test_reactors.loadTestReactor() + cls.r.core.p.keff = 1.0 + cls.gfi = MockGlobalFluxWithExecutersNonUniform(cls.r, cs) + + def test_executerInteractionNonUniformAssems(self): + gfi, r = self.gfi, self.r + gfi.interactBOC() + gfi.interactEveryNode(0, 0) + r.p.timeNode += 1 + gfi.interactEveryNode(0, 1) + gfi.interactEOC() + self.assertAlmostEqual(r.core.p.rxSwing, (1.02 - 1.01) / 1.01 * 1e5) + + def test_calculateKeff(self): + self.assertEqual(self.gfi.calculateKeff(), 1.05) # set in mock + + def test_getExecuterCls(self): + class0 = globalFluxInterface.GlobalFluxInterfaceUsingExecuters.getExecuterCls() + self.assertEqual(class0, globalFluxInterface.GlobalFluxExecuter) + + class TestGlobalFluxResultMapper(unittest.TestCase): """ Test that global flux result mappings run. diff --git a/armi/physics/neutronics/parameters.py b/armi/physics/neutronics/parameters.py index 4674f0dcd..9a9699aeb 100644 --- a/armi/physics/neutronics/parameters.py +++ b/armi/physics/neutronics/parameters.py @@ -99,6 +99,7 @@ def mgFlux(self, value): categories=[ parameters.Category.fluxQuantities, parameters.Category.multiGroupQuantities, + parameters.Category.gamma, ], default=None, ) @@ -129,7 +130,10 @@ def mgFlux(self, value): description="multigroup gamma source", location=ParamLocation.AVERAGE, saveToDB=True, - categories=[parameters.Category.multiGroupQuantities], + categories=[ + parameters.Category.multiGroupQuantities, + parameters.Category.gamma, + ], default=None, ) @@ -139,7 +143,8 @@ def mgFlux(self, value): description="gamma source", location=ParamLocation.AVERAGE, saveToDB=True, - default=None, + categories=[parameters.Category.gamma], + default=0.0, ) pb.defParam( @@ -183,7 +188,7 @@ def mgFlux(self, value): "pinMgFluxesGamma", units="g/s/cm$^2$", description="should be a blank 3-D array, but re-defined later (ng x nPins x nAxialSegments)", - categories=[parameters.Category.pinQuantities], + categories=[parameters.Category.pinQuantities, parameters.Category.gamma], saveToDB=False, default=None, ) @@ -289,13 +294,14 @@ def linPowByPinNeutron(self, value): else: self._p_linPowByPinNeutron = numpy.array(value) + # gamma category because linPowByPin is only split by neutron/gamma when gamma is activated pb.defParam( "linPowByPinNeutron", setter=linPowByPinNeutron, units="W/cm", description="Pin linear neutron heat rate. This is the neutron heating component of `linPowByPin`", location=ParamLocation.CHILDREN, - categories=[parameters.Category.pinQuantities], + categories=[parameters.Category.pinQuantities, parameters.Category.gamma], default=None, ) @@ -311,7 +317,7 @@ def linPowByPinGamma(self, value): units="W/cm", description="Pin linear gamma heat rate. This is the gamma heating component of `linPowByPin`", location=ParamLocation.CHILDREN, - categories=[parameters.Category.pinQuantities], + categories=[parameters.Category.pinQuantities, parameters.Category.gamma], default=None, ) @@ -320,6 +326,7 @@ def linPowByPinGamma(self, value): units="#/s", description='List of reaction rates in specified by setting "reactionsToDB"', location=ParamLocation.VOLUME_INTEGRATED, + categories=[parameters.Category.fluxQuantities], default=None, ) @@ -546,17 +553,25 @@ def linPowByPinGamma(self, value): "pdensGamma", units="W/cm^3", description="Average volumetric gamma power density", + categories=[parameters.Category.gamma], ) + # gamma category because pdens is only split by neutron/gamma when gamma is activated pb.defParam( "pdensNeutron", units="W/cm^3", description="Average volumetric neutron power density", + categories=[parameters.Category.gamma], ) pb.defParam("ppdens", units="W/cm^3", description="Peak power density") - pb.defParam("ppdensGamma", units="W/cm^3", description="Peak gamma density") + pb.defParam( + "ppdensGamma", + units="W/cm^3", + description="Peak gamma density", + categories=[parameters.Category.gamma], + ) # rx rate params that are derived during mesh conversion. # We'd like all things that can be derived from flux and XS to be @@ -603,15 +618,27 @@ def linPowByPinGamma(self, value): "powerGenerated", units=" W", description="Generated power. Different than b.p.power only when gamma transport is activated.", + categories=[parameters.Category.gamma], ) pb.defParam("power", units="W", description="Total power") pb.defParam("powerDecay", units="W", description="Total decay power") - pb.defParam("powerGamma", units="W", description="Total gamma power") + pb.defParam( + "powerGamma", + units="W", + description="Total gamma power", + categories=[parameters.Category.gamma], + ) - pb.defParam("powerNeutron", units="W", description="Total neutron power") + # gamma category because power is only split by neutron/gamma when gamma is activated + pb.defParam( + "powerNeutron", + units="W", + description="Total neutron power", + categories=[parameters.Category.gamma], + ) with pDefs.createBuilder(default=0.0) as pb: pb.defParam( @@ -663,6 +690,7 @@ def linPowByPinGamma(self, value): units="W/cm^3", description="Volume-averaged generated power density. Different than b.p.pdens only when gamma transport is activated.", location=ParamLocation.AVERAGE, + categories=[parameters.Category.gamma], ) return pDefs diff --git a/armi/reactor/components/component.py b/armi/reactor/components/component.py index 1ee28ed47..f511eac28 100644 --- a/armi/reactor/components/component.py +++ b/armi/reactor/components/component.py @@ -34,7 +34,6 @@ from armi.materials import void from armi.nucDirectory import nuclideBases from armi import materials -from armi.reactor.flags import Flags COMPONENT_LINK_REGEX = re.compile(r"^\s*(.+?)\s*\.\s*(.+?)\s*$") @@ -791,9 +790,8 @@ def setDimension(self, key, val, retainLink=False, cold=True): retainLink : bool, optional If True, the val will be applied to the dimension of linked component which indirectly changes this component's dimensions. - cold : book, optional - If True sets the component to the dimension that would cause - the hot dimension to be the specified value. + cold : bool, optional + If True sets the component cold dimension to the specified value. """ if not key: return @@ -801,13 +799,15 @@ def setDimension(self, key, val, retainLink=False, cold=True): linkedComp, linkedDimName = self.p[key] linkedComp.setDimension(linkedDimName, val, cold=cold) else: - expansionFactor = ( - self.getThermalExpansionFactor() - if key in self.THERMAL_EXPANSION_DIMS - else 1.0 - ) - val = val / expansionFactor if not cold else val + if not cold: + expansionFactor = ( + self.getThermalExpansionFactor() + if key in self.THERMAL_EXPANSION_DIMS + else 1.0 + ) + val /= expansionFactor self.p[key] = val + self.clearLinkedCache() def getDimension(self, key, Tc=None, cold=False): diff --git a/armi/reactor/converters/geometryConverters.py b/armi/reactor/converters/geometryConverters.py index cd8fc8957..a8d64b277 100644 --- a/armi/reactor/converters/geometryConverters.py +++ b/armi/reactor/converters/geometryConverters.py @@ -27,32 +27,33 @@ import collections import copy import math +import operator +import os + import matplotlib import matplotlib.pyplot as plt import numpy -import operator -import os from armi import materials from armi import runLog +from armi.physics.neutronics.fissionProductModel import lumpedFissionProduct from armi.reactor import assemblies from armi.reactor import blocks from armi.reactor import components -from armi.reactor import reactors +from armi.reactor import geometry +from armi.reactor import grids from armi.reactor import parameters +from armi.reactor import reactors +from armi.reactor.converters import blockConverters +from armi.reactor.converters import meshConverters +from armi.reactor.flags import Flags from armi.reactor.parameters import Category -from armi.reactor.parameters import ParamLocation from armi.reactor.parameters import NEVER +from armi.reactor.parameters import ParamLocation from armi.reactor.parameters import SINCE_LAST_GEOMETRY_TRANSFORMATION -from armi.reactor import geometry -from armi.reactor.converters import meshConverters +from armi.utils import hexagon from armi.utils import plotting from armi.utils import units -from armi.reactor import grids -from armi.reactor.flags import Flags -from armi.utils import hexagon -from armi.reactor.converters import blockConverters -from armi.reactor import assemblies BLOCK_AXIAL_MESH_SPACING = ( 20 # Block axial mesh spacing set for nodal diffusion calculation (cm) @@ -386,7 +387,6 @@ def __init__( ): GeometryConverter.__init__(self, cs) self.converterSettings = converterSettings - self._o = None self.meshConverter = None self._expandSourceReactor = expandReactor self._strictHomogenization = strictHomogenization @@ -398,6 +398,7 @@ def __init__( self._newBlockNum = 0 self.blockMap = collections.defaultdict(list) self.blockVolFracs = collections.defaultdict(dict) + self._homogenizeAxiallyByFlags = False def _generateConvertedReactorMesh(self): """Convert the source reactor using the converterSettings""" @@ -576,7 +577,6 @@ def _setupSourceReactorForConversion(self): self._sourceReactor.core.summarizeReactorStats() if self._expandSourceReactor: self._expandSourceReactorGeometry() - self._o = self._sourceReactor.o def _setupConvertedReactor(self, grid): self.convReactor = reactors.Reactor( @@ -603,7 +603,6 @@ def _setAssemsInRadialZone(self, radialIndex, lowerRing, upperRing): self._assemsInRadialZone keeps track of the unique assemblies that are in each radial ring. This ensures that no assemblies are duplicated when using self._getAssemsInRadialThetaZone() """ - lowerTheta = 0.0 for _thetaIndex, upperTheta in enumerate(self.meshConverter.thetaMesh): assemsInRadialThetaZone = self._getAssemsInRadialThetaZone( @@ -734,7 +733,6 @@ def _createRadialThetaZone( outerDiameter : float The outer diameter (in cm) of the radial zone just added """ - newAssembly = assemblies.ThRZAssembly("mixtureAssem") newAssembly.spatialLocator = self.convReactor.core.spatialGrid[ thetaIndex, radialIndex, 0 @@ -744,9 +742,10 @@ def _createRadialThetaZone( len(self.meshConverter.axialMesh), armiObject=newAssembly ) + lfp = lumpedFissionProduct.lumpedFissionProductFactory(self._cs) + lowerAxialZ = 0.0 for axialIndex, upperAxialZ in enumerate(self.meshConverter.axialMesh): - # Setup the new block data newBlockName = "B{:04d}{}".format( int(newAssembly.getNum()), chr(axialIndex + 65) @@ -793,6 +792,7 @@ def _createRadialThetaZone( } for nuc in self._sourceReactor.blueprints.allNuclidesInProblem: material.setMassFrac(nuc, 0.0) + newComponent = components.DifferentialRadialSegment( "mixture", material, **dims ) @@ -800,8 +800,7 @@ def _createRadialThetaZone( newBlock.p.zbottom = lowerAxialZ newBlock.p.ztop = upperAxialZ - fpi = self._o.getInterface("fissionProducts") - newBlock.setLumpedFissionProducts(fpi.getGlobalLumpedFissionProducts()) + newBlock.setLumpedFissionProducts(lfp) # Assign the new block cross section type and burn up group newBlock.setType(newBlockType) @@ -822,6 +821,7 @@ def _createRadialThetaZone( newAssembly.add(newBlock) lowerAxialZ = upperAxialZ + newAssembly.calculateZCoords() # builds mesh self.convReactor.core.add(newAssembly) @@ -1053,7 +1053,6 @@ def plotConvertedReactor(self, fNameBase=None): This makes plots of each individual theta mesh """ - runLog.info( "Generating plot(s) of the converted {} reactor".format( str(self.convReactor.core.geomType).upper() @@ -1190,7 +1189,6 @@ def _getBlockColor(colConverter, colGenerator, blockColors, blockType): def reset(self): """Clear out attribute data, including holding the state of the converted reactor core model.""" - self._o = None self.meshConverter = None self._radialMeshConversionType = None self._axialMeshConversionType = None @@ -1279,6 +1277,7 @@ def convert(self, r): self._sourceReactor.core.geomType, ) ) + edgeChanger = EdgeAssemblyChanger() edgeChanger.removeEdgeAssemblies(self._sourceReactor.core) runLog.info("Expanding to full core geometry") @@ -1446,7 +1445,6 @@ def removeEdgeAssemblies(self, core): See Also -------- addEdgeAssemblies : adds the edge assemblies - """ if core.isFullCore: return diff --git a/armi/reactor/converters/tests/test_uniformMesh.py b/armi/reactor/converters/tests/test_uniformMesh.py index e2aad3a97..93e4ee2cf 100644 --- a/armi/reactor/converters/tests/test_uniformMesh.py +++ b/armi/reactor/converters/tests/test_uniformMesh.py @@ -28,6 +28,28 @@ from armi.tests import TEST_ROOT, ISOAA_PATH +class DummyFluxOptions: + def __init__(self): + self.photons = False + + +class TestConverterFactory(unittest.TestCase): + def setUp(self): + self.o, self.r = test_reactors.loadTestReactor( + inputFilePath=os.path.join(TEST_ROOT, "detailedAxialExpansion"), + ) + self.dummyOptions = DummyFluxOptions() + + def test_converterFactory(self): + self.dummyOptions.photons = False + neutronConverter = uniformMesh.converterFactory(self.dummyOptions) + self.assertTrue(neutronConverter, uniformMesh.NeutronicsUniformMeshConverter) + + self.dummyOptions.photons = True + gammaConverter = uniformMesh.converterFactory(self.dummyOptions) + self.assertTrue(gammaConverter, uniformMesh.GammaUniformMeshConverter) + + class TestAssemblyUniformMesh(unittest.TestCase): """ Tests individual operations of the uniform mesh converter @@ -48,7 +70,10 @@ def test_makeAssemWithUniformMesh(self): self.converter._computeAverageAxialMesh() newAssem = self.converter.makeAssemWithUniformMesh( - sourceAssem, self.converter._uniformMesh + sourceAssem, + self.converter._uniformMesh, + blockParamNames=["power"], + mapNumberDensities=True, ) prevB = None @@ -89,7 +114,7 @@ def test_makeAssemWithUniformMeshSubmesh(self): self.r.core.updateAxialMesh() newAssem = self.converter.makeAssemWithUniformMesh( - sourceAssem, self.r.core.p.axialMesh[1:] + sourceAssem, self.r.core.p.axialMesh[1:], blockParamNames=["power"] ) self.assertNotEqual(len(newAssem), len(sourceAssem)) @@ -114,6 +139,7 @@ def test_makeAssemUniformMeshParamMappingSameMesh(self): sourceAssem, sourceAssem.getAxialMesh(), blockParamNames=["flux", "power", "mgFlux"], + mapNumberDensities=True, ) for b, origB in zip(newAssem, sourceAssem): self.assertEqual(b.p.flux, 1.0) @@ -329,6 +355,106 @@ def test_applyStateToOriginal(self): ) +class TestGammaUniformMesh(unittest.TestCase): + """ + Tests gamma uniform mesh converter + + Loads reactor once per test + """ + + @classmethod + def setUpClass(cls): + # random seed to support random mesh in unit tests below + random.seed(987324987234) + + def setUp(self): + self.o, self.r = test_reactors.loadTestReactor( + TEST_ROOT, customSettings={"xsKernel": "MC2v2"} + ) + self.r.core.lib = isotxs.readBinary(ISOAA_PATH) + self.r.core.p.keff = 1.0 + self.converter = uniformMesh.GammaUniformMeshConverter( + cs=self.o.cs, calcReactionRates=False + ) + + def test_convertNumberDensities(self): + refMass = self.r.core.getMass("U235") + applyNonUniformHeightDistribution( + self.r + ) # this changes the mass of everything in the core + perturbedCoreMass = self.r.core.getMass("U235") + self.assertNotEqual(refMass, perturbedCoreMass) + self.converter.convert(self.r) + + uniformReactor = self.converter.convReactor + uniformMass = uniformReactor.core.getMass("U235") + + self.assertAlmostEqual( + perturbedCoreMass, uniformMass + ) # conversion conserved mass + self.assertAlmostEqual( + self.r.core.getMass("U235"), perturbedCoreMass + ) # conversion didn't change source reactor mass + + def test_applyStateToOriginal(self): + applyNonUniformHeightDistribution(self.r) # note: this perturbs the ref. mass + + # set original parameters on pre-mapped core with non-uniform assemblies + for b in self.r.core.getBlocks(): + b.p.mgFlux = range(33) + b.p.adjMgFlux = range(33) + b.p.fastFlux = 2.0 + b.p.flux = 5.0 + b.p.power = 5.0 + + # set original parameters on pre-mapped core with non-uniform assemblies + self.converter.convert(self.r) + for b in self.converter.convReactor.core.getBlocks(): + b.p.powerGamma = 0.5 + b.p.powerNeutron = 0.5 + + # check integral and density params + assemblyPowers = [ + a.calcTotalParam("power") for a in self.converter.convReactor.core + ] + assemblyGammaPowers = [ + a.calcTotalParam("powerGamma") for a in self.converter.convReactor.core + ] + totalPower = self.converter.convReactor.core.calcTotalParam( + "power", generationNum=2 + ) + totalPowerGamma = self.converter.convReactor.core.calcTotalParam( + "powerGamma", generationNum=2 + ) + + self.converter.applyStateToOriginal() + + for b in self.r.core.getBlocks(): + # equal to original value because these were never mapped + self.assertEqual(b.p.fastFlux, 2.0) + self.assertEqual(b.p.flux, 5.0) + self.assertEqual(b.p.fastFlux, 2.0) + self.assertEqual(b.p.power, 5.0) + + # not equal because blocks are different size + self.assertNotEqual(b.p.powerGamma, 0.5) + self.assertNotEqual(b.p.powerNeutron, 0.5) + + # equal because these are mapped + for expectedPower, expectedGammaPower, a in zip( + assemblyPowers, assemblyGammaPowers, self.r.core + ): + self.assertAlmostEqual(a.calcTotalParam("power"), expectedPower) + self.assertAlmostEqual(a.calcTotalParam("powerGamma"), expectedGammaPower) + + self.assertAlmostEqual( + self.r.core.calcTotalParam("powerGamma", generationNum=2), totalPowerGamma + ) + self.assertAlmostEqual( + self.r.core.calcTotalParam("power", generationNum=2), totalPower + ) + + class TestParamConversion(unittest.TestCase): def setUp(self): """ diff --git a/armi/reactor/converters/uniformMesh.py b/armi/reactor/converters/uniformMesh.py index 516831a6f..de5cac3be 100644 --- a/armi/reactor/converters/uniformMesh.py +++ b/armi/reactor/converters/uniformMesh.py @@ -73,6 +73,13 @@ from armi.reactor.reactors import Reactor +def converterFactory(globalFluxOptions): + if globalFluxOptions.photons: + return GammaUniformMeshConverter + else: + return NeutronicsUniformMeshConverter + + class UniformMeshGeometryConverter(GeometryConverter): """ This geometry converter can be used to change the axial mesh structure of the reactor core. @@ -84,18 +91,44 @@ class UniformMeshGeometryConverter(GeometryConverter): - Creation of a new assembly with a new axial mesh applied. See: `` - Resetting the parameter state of an assembly back to the defaults for the provided block parameters. See: `` - Mapping number densities and block parameters between one assembly to another. See: `` + + This class is meant to be extended for specific physics calculations that require a uniform mesh. + The child types of this class should define custom `reactorParamsToMap` and `blockParamsToMap` attributes, and the `_setParamsToUpdate` method + to specify the precise parameters that need to be mapped in each direction between the non-uniform and uniform mesh assemblies. The definitions should avoid mapping + block parameters in both directions because the mapping process will cause numerical diffusion. The behavior of `setAssemblyStateFromOverlaps` is dependent on the + direction in which the mapping is being applied to prevent the numerical diffusion problem. + + - "in" is used when mapping parameters into the uniform assembly + from the non-uniform assembly. + - "out" is used when mapping parameters from the uniform assembly back + to the non-uniform assembly. + + .. warning:: + If a parameter is calculated by a physics solver while the reactor is in its + converted (uniform mesh) state, that parameter *must* be included in the list + of `reactorParamNames` or `blockParamNames` to be mapped back to the non-uniform + reactor; otherwise, it will be lost. These lists are defined through the + `_setParamsToUpdate` method, which uses the `reactorParamMappingCategories` and + `blockParamMappingCategories` attributes and applies custom logic to create a list of + parameters to be mapped in each direction. """ - REACTOR_PARAM_MAPPING_CATEGORIES = [] - BLOCK_PARAM_MAPPING_CATEGORIES = [] + reactorParamMappingCategories = { + "in": [], + "out": [], + } + blockParamMappingCategories = { + "in": [], + "out": [], + } _TEMP_STORAGE_NAME_SUFFIX = "-TEMP" def __init__(self, cs=None): GeometryConverter.__init__(self, cs) self._uniformMesh = None + self.calcReactionRates = False self.reactorParamNames = [] self.blockParamNames = [] - self.calcReactionRates = False # These dictionaries represent back-up data from the source reactor # that can be recovered if the data is not being brought back from @@ -120,6 +153,7 @@ def convert(self, r=None): completeStartTime = timer() self._sourceReactor = r + self._setParamsToUpdate("in") # Here we are taking a short cut to homogenizing the core by only focusing on the # core assemblies that need to be homogenized. This will have a large speed up @@ -132,7 +166,6 @@ def convert(self, r=None): ) self.convReactor = self._sourceReactor self.convReactor.core.updateAxialMesh() - self._setParamsToUpdate() for assem in self.convReactor.core.getAssemblies(self._nonUniformMeshFlags): homogAssem = self.makeAssemWithUniformMesh( assem, @@ -160,7 +193,6 @@ def convert(self, r=None): else: runLog.extra(f"Building copy of {r} with a uniform axial mesh.") self.convReactor = self.initNewReactor(r, self._cs) - self._setParamsToUpdate() self._computeAverageAxialMesh() self._buildAllUniformAssemblies() self._mapStateFromReactorToOther( @@ -215,6 +247,9 @@ def applyStateToOriginal(self): ) completeStartTime = timer() + # map the block parameters back to the non-uniform assembly + self._setParamsToUpdate("out") + # If we have non-uniform mesh assemblies then we need to apply a # different approach to undo the geometry transformations on an # assembly by assembly basis. @@ -261,7 +296,7 @@ def applyStateToOriginal(self): self._cachedReactorCoreParamData = {} self._clearStateOnReactor(self._sourceReactor, cache=True) self._mapStateFromReactorToOther( - self.convReactor, self._sourceReactor, mapNumberDensities=True + self.convReactor, self._sourceReactor, mapNumberDensities=False ) # We want to map the converted reactor core's library to the source reactor @@ -321,6 +356,8 @@ def makeAssemWithUniformMesh( runLog.debug(f"Creating a uniform mesh of {newAssem}") bottom = 0.0 + if blockParamNames is None: + blockParamNames = [] for topMeshPoint in newMesh: overlappingBlockInfo = sourceAssem.getBlocksBetweenElevations( bottom, topMeshPoint @@ -397,8 +434,8 @@ def makeAssemWithUniformMesh( def setAssemblyStateFromOverlaps( sourceAssembly, destinationAssembly, - blockParamNames=None, - mapNumberDensities=True, + blockParamNames, + mapNumberDensities=False, calcReactionRates=False, ): """ @@ -418,7 +455,7 @@ def setAssemblyStateFromOverlaps( assem that has the state destinationAssembly : Assembly assem that has is getting the state from sourceAssembly - blockParamNames : List[str], optional + blockParamNames : List[str] A list of block parameter names to map between the source assembly and the destination assembly. mapNumberDensities : bool, optional @@ -438,39 +475,6 @@ def setAssemblyStateFromOverlaps( -------- setNumberDensitiesFromOverlaps : does this but does smarter caching for number densities. """ - if blockParamNames is None: - blockParamNames = [] - - if not isinstance(blockParamNames, list): - raise TypeError( - f"The ``blockParamNames`` parameters names are not provided " - f"as a list. Value(s) given: {blockParamNames}" - ) - - cachedParams = UniformMeshGeometryConverter.clearStateOnAssemblies( - [destinationAssembly], - blockParamNames, - cache=True, - ) - for destBlock in destinationAssembly: - - # Check that the parameters on the destination block have been cleared first before attempting to - # map the data. These parameters should be cleared using ``UniformMeshGeometryConverter.clearStateOnAssemblies``. - existingDestBlockParamVals = BlockParamMapper.paramGetter( - destBlock, blockParamNames - ) - clearedValues = [ - True if not val else False for val in existingDestBlockParamVals - ] - if not all(clearedValues): - raise ValueError( - f"The state of {destBlock} on {destinationAssembly} " - f"was not cleared prior to calling ``setAssemblyStateFromOverlaps``. " - f"This indicates an implementation bug in the mesh converter that should " - f"be reported to the developers. The following parameters should be cleared:\n" - f"Parameters: {blockParamNames}\n" - f"Values: {existingDestBlockParamVals}" - ) # The destination assembly is the assembly that the results are being mapped to # whereas the source assembly is the assembly that is from the uniform model. This @@ -506,7 +510,8 @@ def setAssemblyStateFromOverlaps( setNumberDensitiesFromOverlaps(destBlock, sourceBlocksInfo) for sourceBlock, sourceBlockOverlapHeight in sourceBlocksInfo: sourceBlockVals = BlockParamMapper.paramGetter( - sourceBlock, blockParamNames + sourceBlock, + blockParamNames, ) sourceBlockHeight = sourceBlock.getHeight() @@ -540,10 +545,6 @@ def setAssemblyStateFromOverlaps( destBlock, updatedDestVals.values(), updatedDestVals.keys() ) - UniformMeshGeometryConverter._applyCachedParamValues( - destBlock, blockParamNames, cachedParams - ) - # If requested, the reaction rates will be calculated based on the # mapped neutron flux and the XS library. if calcReactionRates: @@ -560,45 +561,6 @@ def setAssemblyStateFromOverlaps( label="Block reaction rate calculation skipped due to insufficient multi-group flux data.", ) - @staticmethod - def _applyCachedParamValues(destBlock, paramNames, cachedParams): - """ - Applies the cached parameter values back to the destination block, if there are any. - - Notes - ----- - This is implemented to ensure that if certain parameters are not set on the original - block that the destination block has the parameter data recovered rather than zeroing - the data out. The destination block is cleared before the mapping occurs in ``clearStateOnAssemblies``. - """ - - # For parameters that have not been set on the destination block, recover these - # back to their originals based on the cached values. - for paramName in paramNames: - - # Skip over any parameter names that were not previously cached. - if paramName not in cachedParams[destBlock]: - continue - - if isinstance(destBlock.p[paramName], numpy.ndarray): - # Using just all() on the list/array is not sufficient because if a zero value exists - # in the data then this would then lead to overwritting the data. This is an edge case see - # in the testing, so this excludes zero values on the check. - if ( - not all([val for val in destBlock.p[paramName] if val != 0.0]) - or not destBlock.p[paramName].size > 0 - ): - destBlock.p[paramName] = cachedParams[destBlock][paramName] - elif isinstance(destBlock.p[paramName], list): - if ( - not all([val for val in destBlock.p[paramName] if val != 0.0]) - or not destBlock.p[paramName] - ): - destBlock.p[paramName] = cachedParams[destBlock][paramName] - else: - if not destBlock.p[paramName]: - destBlock.p[paramName] = cachedParams[destBlock][paramName] - @staticmethod def clearStateOnAssemblies(assems, blockParamNames=None, cache=True): """ @@ -673,8 +635,26 @@ def reset(self): self._cachedReactorCoreParamData = {} super().reset() - def _setParamsToUpdate(self): - """Activate conversion of various paramters.""" + def _setParamsToUpdate(self, direction): + """ + Activate conversion of the specified parameters. + + Notes + ----- + The parameters mapped into and out of the uniform mesh will vary depending on + the physics kernel using the uniform mesh. The parameters to be mapped in each + direction are defined as a class attribute. New options can be created by extending + the base class with different class attributes for parameters to map, and applying + special modifications to these categorized lists with the `_setParamsToUpdate` method. + The base class is meant to be extended, so this method only initializes the empty + lists and does not perform any significant function. + + Parameters + ---------- + direction : str + "in" or "out". The direction of mapping; "in" to the uniform mesh assembly, or "out" of it. + Different parameters are mapped in each direction. + """ self.reactorParamNames = [] self.blockParamNames = [] @@ -726,7 +706,9 @@ def _buildAllUniformAssemblies(self): f"with a uniform mesh of {self._uniformMesh}" ) for sourceAssem in self._sourceReactor.core: - newAssem = self.makeAssemWithUniformMesh(sourceAssem, self._uniformMesh) + newAssem = self.makeAssemWithUniformMesh( + sourceAssem, self._uniformMesh, self.blockParamNames + ) src = sourceAssem.spatialLocator newLoc = self.convReactor.core.spatialGrid[src.i, src.j, 0] self.convReactor.core.add(newAssem, newLoc) @@ -744,7 +726,7 @@ def _clearStateOnReactor(self, reactor, cache): reactor.core.p[rp] = 0.0 def _mapStateFromReactorToOther( - self, sourceReactor, destReactor, mapNumberDensities=True + self, sourceReactor, destReactor, mapNumberDensities=False ): """ Map parameters from one reactor to another. @@ -785,19 +767,35 @@ class NeutronicsUniformMeshConverter(UniformMeshGeometryConverter): Notes ----- - If a case runs where two mesh conversions happen one after the other - (e.g. a fixed source gamma transport step that needs appropriate - fission rates), it is essential that the neutronics params be - mapped onto the newly converted reactor as well as off of it - back to the source reactor. + This uniform mesh converter is intended for setting up an eigenvalue + (fission-source) neutronics solve. There are no block parameters that need + to be mapped in for a basic eigenvalue calculation, just number densities. + The results of the calculation are mapped out (i.e., back to the non-uniform + mesh). The results mapped out include things like flux, power, and reaction + rates. + + .. warning:: + If a parameter is calculated by a physics solver while the reactor is in its + converted (uniform mesh) state, that parameter *must* be included in the list + of `reactorParamNames` or `blockParamNames` to be mapped back to the non-uniform + reactor; otherwise, it will be lost. These lists are defined through the + `_setParamsToUpdate` method, which uses the `reactorParamMappingCategories` and + `blockParamMappingCategories` attributes and applies custom logic to create a list of + parameters to be mapped in each direction. """ - REACTOR_PARAM_MAPPING_CATEGORIES = [parameters.Category.neutronics] - BLOCK_PARAM_MAPPING_CATEGORIES = [ - parameters.Category.detailedAxialExpansion, - parameters.Category.multiGroupQuantities, - parameters.Category.pinQuantities, - ] + reactorParamMappingCategories = { + "in": [parameters.Category.neutronics], + "out": [parameters.Category.neutronics], + } + blockParamMappingCategories = { + "in": [], + "out": [ + parameters.Category.detailedAxialExpansion, + parameters.Category.multiGroupQuantities, + parameters.Category.pinQuantities, + ], + } def __init__(self, cs=None, calcReactionRates=True): """ @@ -814,21 +812,50 @@ def __init__(self, cs=None, calcReactionRates=True): UniformMeshGeometryConverter.__init__(self, cs) self.calcReactionRates = calcReactionRates - def _setParamsToUpdate(self): - """Activate conversion of various neutronics-only paramters.""" - UniformMeshGeometryConverter._setParamsToUpdate(self) + def _setParamsToUpdate(self, direction): + """ + Activate conversion of the specified parameters. + + Notes + ----- + For the fission-source neutronics calculation, there are no block parameters + that need to be mapped in. This function applies additional filters to the + list of categories defined in `blockParamMappingCategories[out]` to avoid mapping + out cumulative parameters like DPA or burnup. These parameters should not + exist on the neutronics uniform mesh assembly anyway, but this filtering + provides an added layer of safety to prevent data from being inadvertently + overwritten. + + Parameters + ---------- + direction : str + "in" or "out". The direction of mapping; "in" to the uniform mesh assembly, or "out" of it. + Different parameters are mapped in each direction. + """ + UniformMeshGeometryConverter._setParamsToUpdate(self, direction) - for category in self.REACTOR_PARAM_MAPPING_CATEGORIES: + for category in self.reactorParamMappingCategories[direction]: self.reactorParamNames.extend( self._sourceReactor.core.p.paramDefs.inCategory(category).names ) - b = self._sourceReactor.core.getFirstBlock() - for category in self.BLOCK_PARAM_MAPPING_CATEGORIES: - self.blockParamNames.extend(b.p.paramDefs.inCategory(category).names) + excludedCategories = [parameters.Category.gamma] + if direction == "out": + excludedCategories.append(parameters.Category.cumulative) + excludedParamNames = [] + for category in excludedCategories: + excludedParamNames.extend(b.p.paramDefs.inCategory(category).names) + for category in self.blockParamMappingCategories[direction]: + self.blockParamNames.extend( + [ + name + for name in b.p.paramDefs.inCategory(category).names + if not name in excludedParamNames + ] + ) def _mapStateFromReactorToOther( - self, sourceReactor, destReactor, mapNumberDensities=True + self, sourceReactor, destReactor, mapNumberDensities=False ): UniformMeshGeometryConverter._mapStateFromReactorToOther( self, @@ -867,6 +894,83 @@ def _mapStateFromReactorToOther( self._cachedReactorCoreParamData = {} +class GammaUniformMeshConverter(NeutronicsUniformMeshConverter): + """ + A uniform mesh converter that specifically maps gamma parameters. + + Notes + ----- + This uniform mesh converter is intended for setting up a fixed-source gamma transport solve. + Some block parameters from the neutronics solve, such as `b.p.mgFlux`, may need to be mapped + into the uniform mesh reactor so that the gamma source can be calculated by the ARMI plugin + performing gamma transport. Parameters that are updated with gamma transport results, such + as `powerGenerated`, `powerNeutron`, and `powerGamma`, need to be mapped back to the + non-uniform reactor. + + .. warning:: + If a parameter is calculated by a physics solver while the reactor is in its + converted (uniform mesh) state, that parameter *must* be included in the list + of `reactorParamNames` or `blockParamNames` to be mapped back to the non-uniform + reactor; otherwise, it will be lost. These lists are defined through the + `_setParamsToUpdate` method, which uses the `reactorParamMappingCategories` and + `blockParamMappingCategories` attributes and applies custom logic to create a list of + parameters to be mapped in each direction. + """ + + reactorParamMappingCategories = { + "in": [parameters.Category.neutronics], + "out": [parameters.Category.neutronics], + } + blockParamMappingCategories = { + "in": [ + parameters.Category.detailedAxialExpansion, + parameters.Category.multiGroupQuantities, + ], + "out": [ + parameters.Category.gamma, + ], + } + + def _setParamsToUpdate(self, direction): + """ + Activate conversion of the specified parameters. + + Notes + ----- + For gamma transport, only a small subset of neutronics parameters need to be + mapped out. The set is defined in this method. There are conditions on the + output blockParamMappingCategories: only non-cumulative, gamma parameters are mapped out. + This avoids numerical diffusion of cumulative parameters or those created by the + initial eigenvalue neutronics solve from being mapped in both directions by the + mesh converter for the fixed-source gamma run. + + Parameters + ---------- + direction : str + "in" or "out". The direction of mapping; "in" to the uniform mesh assembly, or "out" of it. + Different parameters are mapped in each direction. + """ + UniformMeshGeometryConverter._setParamsToUpdate(self, direction) + + for category in self.reactorParamMappingCategories[direction]: + self.reactorParamNames.extend( + self._sourceReactor.core.p.paramDefs.inCategory(category).names + ) + b = self._sourceReactor.core.getFirstBlock() + if direction == "out": + excludeList = b.p.paramDefs.inCategory(parameters.Category.cumulative).names + else: + excludeList = b.p.paramDefs.inCategory(parameters.Category.gamma).names + for category in self.blockParamMappingCategories[direction]: + self.blockParamNames.extend( + [ + name + for name in b.p.paramDefs.inCategory(category).names + if not name in excludeList + ] + ) + + class BlockParamMapper: """ Namespace for parameter setters/getters that can be used when diff --git a/armi/reactor/parameters/parameterDefinitions.py b/armi/reactor/parameters/parameterDefinitions.py index c1c62901d..c2d4b7359 100644 --- a/armi/reactor/parameters/parameterDefinitions.py +++ b/armi/reactor/parameters/parameterDefinitions.py @@ -58,17 +58,28 @@ class Category: - """A "namespace" for storing parameter categories.""" + """ + A "namespace" for storing parameter categories. + + Notes + ----- + * `cumulative` parameters are accumulated over many time steps + * `pinQuantities` parameters are defined on the pin level within a block + * `multiGroupQuantities` parameters have group dependence (often a 1D numpy array) + * `fluxQuantities` parameters are related to neutron or gamma flux + * `neutronics` parameters are calculated in a neutronics global flux solve + * `gamma` parameters are calculated in a fixed-source gamma solve + * `detailedAxialExpansion` parameters are marked as such so that they are mapped from the uniform mesh back to the non-uniform mesh + """ + cumulative = "cumulative" assignInBlueprints = "assign in blueprints" retainOnReplacement = "retain on replacement" pinQuantities = "pinQuantities" fluxQuantities = "fluxQuantities" multiGroupQuantities = "multi-group quantities" neutronics = "neutronics" - - # This is used to tell the UniformMesh converter to map these parameters back and - # forth between the source and destination meshes. + gamma = "gamma" detailedAxialExpansion = "detailedAxialExpansion" diff --git a/armi/utils/__init__.py b/armi/utils/__init__.py index a4896e680..2f44e40ac 100644 --- a/armi/utils/__init__.py +++ b/armi/utils/__init__.py @@ -319,6 +319,21 @@ def getBurnSteps(cs): return [len(steps) for steps in stepLengths] +def hasBurnup(cs): + """Is depletion being modeled? + + Parameters + ---------- + cs : case settings object + + Returns + ------- + bool + Are there any burnup steps? + """ + return sum(getBurnSteps(cs)) > 0 + + def getMaxBurnSteps(cs): burnSteps = getBurnSteps(cs) return max(burnSteps) diff --git a/armi/utils/tests/test_utils.py b/armi/utils/tests/test_utils.py index 947714e99..7d0a5cf14 100644 --- a/armi/utils/tests/test_utils.py +++ b/armi/utils/tests/test_utils.py @@ -37,6 +37,7 @@ getCycleNodeFromCumulativeNode, getPreviousTimeNode, getCumulativeNodeNum, + hasBurnup, ) @@ -300,6 +301,9 @@ def test_getBurnSteps(self): getBurnSteps(self.standaloneSimpleCS), self.burnStepsSimpleSolution ) + def test_hasBurnup(self): + self.assertTrue(hasBurnup(self.standaloneDetailedCS)) + def test_getMaxBurnSteps(self): self.assertEqual( getMaxBurnSteps(self.standaloneDetailedCS), diff --git a/doc/release/0.2.rst b/doc/release/0.2.rst index 35013f57a..25b6cfa1c 100644 --- a/doc/release/0.2.rst +++ b/doc/release/0.2.rst @@ -15,12 +15,14 @@ What's new in ARMI #. Split algorithms specific to hex assemblies out of ``FuelHandler``. (`PR#962 `_) #. Add ability to load from a db using negative node index #. Add group structures for 21- and 94-groups used in photon transport +#. Fix numerical diffusion bug in uniform mesh converter that affects number densities and cumulative parameters like DPA. (`PR#992 `_) Bug fixes --------- #. Fixed bug in ``referenceBlockAxialMesh`` and ``axialMesh`` during process loading. (`PR#980 `) #. Removed Barriers in temp directory changers and output cache to avoid deadlocks in MPI cases - +#. Fixed bug with database writing and tight coupling. (`PR#1005 https://github.com/terrapower/armi/pull/1005`) +#. Fixed a bug where snapshot load would not respect the new cs["power"] setting. ARMI v0.2.5 =========== diff --git a/tox.ini b/tox.ini index 893d8f026..45cdf0bb4 100644 --- a/tox.ini +++ b/tox.ini @@ -19,7 +19,7 @@ commands = pytest --ignore=armi/utils/tests/test_gridGui.py -n 4 armi [testenv:doc] -whitelist_externals = +allowlist_externals = /usr/bin/git /usr/bin/make deps= @@ -37,7 +37,7 @@ deps= mpi4py -r{toxinidir}/requirements.txt -r{toxinidir}/requirements-testing.txt -whitelist_externals = +allowlist_externals = /usr/bin/mpiexec commands = mpiexec -n 2 --use-hwthread-cpus coverage run --rcfile=.coveragerc -m pytest --cov=armi --cov-config=.coveragerc --ignore=venv --cov-fail-under=80 armi/tests/test_mpiFeatures.py @@ -49,7 +49,7 @@ deps= mpi4py -r{toxinidir}/requirements.txt -r{toxinidir}/requirements-testing.txt -whitelist_externals = +allowlist_externals = /usr/bin/mpiexec commands = coverage run --rcfile=.coveragerc -m pytest -n 4 --cov=armi --cov-config=.coveragerc --cov-append --ignore=armi/utils/tests/test_gridGui.py --ignore=venv armi @@ -61,7 +61,7 @@ commands = deps= pip >= 20.2 mpi4py -whitelist_externals = +allowlist_externals = /usr/bin/mpiexec commands = pip install -r requirements.txt @@ -76,14 +76,13 @@ commands = [testenv:report] skip_install = true deps= - coveralls + pip>=20.2 + mpi4py + -r{toxinidir}/requirements.txt + -r{toxinidir}/requirements-testing.txt commands = coverage report coverage html - coveralls --service=github -depends = - cov -passenv = TOXENV CI GITHUB_* [testenv:manifest] basepython = {env:PYTHON3_PATH:python3}