From 0d039f2298612100f57dbb0e28b1e738144de419 Mon Sep 17 00:00:00 2001 From: Michael Jarrett Date: Wed, 14 Dec 2022 14:29:22 -0800 Subject: [PATCH] Fix numerical diffusion in uniformMesh converter (#992) Mapping anything from a non-uniform assembly to a uniform assembly and then back will cause numerical diffusion of that value. This could be a block parameter (like mgFlux, detailedDpa, etc.) or number densities. This commit avoids numerical diffusion by restricting the parameter mapping between the non-uniform and uniform assembly to only the parameters that are necessary inputs or designated outputs of interest for a given physics solver using the uniform mesh converter. Co-authored-by: jstilley --- .../globalFlux/globalFluxInterface.py | 6 +- .../tests/test_globalFluxInterface.py | 39 +++ armi/physics/neutronics/parameters.py | 44 ++- .../converters/tests/test_uniformMesh.py | 130 ++++++- armi/reactor/converters/uniformMesh.py | 322 ++++++++++++------ .../parameters/parameterDefinitions.py | 19 +- doc/release/0.2.rst | 1 + 7 files changed, 435 insertions(+), 126 deletions(-) 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/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/doc/release/0.2.rst b/doc/release/0.2.rst index 9adbe1e35..ff25b272e 100644 --- a/doc/release/0.2.rst +++ b/doc/release/0.2.rst @@ -15,6 +15,7 @@ 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 ---------