From 68268c153ec9e9b73dcc0dbe5fa1cdc1a1298731 Mon Sep 17 00:00:00 2001 From: Jake Hader Date: Tue, 3 Dec 2024 13:58:22 -0800 Subject: [PATCH 1/9] Fixing the affect of custom isotopics on material density (#1822) --- armi/reactor/blueprints/blockBlueprint.py | 7 +- armi/reactor/blueprints/componentBlueprint.py | 73 +++++++++++++------ armi/reactor/blueprints/isotopicOptions.py | 8 +- .../blueprints/tests/test_blueprints.py | 3 +- .../blueprints/tests/test_customIsotopics.py | 24 ++++-- doc/release/0.5.rst | 1 + 6 files changed, 82 insertions(+), 34 deletions(-) diff --git a/armi/reactor/blueprints/blockBlueprint.py b/armi/reactor/blueprints/blockBlueprint.py index 692ae84af..a1e7a5543 100644 --- a/armi/reactor/blueprints/blockBlueprint.py +++ b/armi/reactor/blueprints/blockBlueprint.py @@ -28,6 +28,7 @@ from armi.reactor.composites import Composite from armi.reactor.converters import blockConverters from armi.reactor.flags import Flags +from armi.settings.fwSettings.globalSettings import CONF_INPUT_HEIGHTS_HOT def _configureGeomOptions(): @@ -144,7 +145,11 @@ def construct( filteredMaterialInput, byComponentMatModKeys = self._filterMaterialInput( materialInput, componentDesign ) - c = componentDesign.construct(blueprint, filteredMaterialInput) + c = componentDesign.construct( + blueprint, + filteredMaterialInput, + cs[CONF_INPUT_HEIGHTS_HOT], + ) components[c.name] = c # check that the mat mods for this component are valid options diff --git a/armi/reactor/blueprints/componentBlueprint.py b/armi/reactor/blueprints/componentBlueprint.py index 41dcc0dce..289a0de8f 100644 --- a/armi/reactor/blueprints/componentBlueprint.py +++ b/armi/reactor/blueprints/componentBlueprint.py @@ -186,7 +186,7 @@ def shape(self, shape): mergeWith = yamlize.Attribute(type=str, default=None) area = yamlize.Attribute(type=float, default=None) - def construct(self, blueprint, matMods): + def construct(self, blueprint, matMods, inputHeightsConsideredHot): """Construct a component or group. .. impl:: User-defined on material alterations are applied here. @@ -205,6 +205,17 @@ def construct(self, blueprint, matMods): The ``applyInputParams()`` method of that material class is then called, passing in the associated material modifications data, which the material class can then use to modify the isotopics as necessary. + + Parameters + ---------- + blueprint : Blueprints + Blueprints object containing various detailed information, such as nuclides to model + + matMods : dict + Material modifications to apply to the component. + + inputHeightsConsideredHot : bool + See the case setting of the same name. """ runLog.debug("Constructing component {}".format(self.name)) kwargs = self._conformKwargs(blueprint, matMods) @@ -214,7 +225,9 @@ class can then use to modify the isotopics as necessary. constructedObject = composites.Composite(self.name) for groupedComponent in group: componentDesign = blueprint.componentDesigns[groupedComponent.name] - component = componentDesign.construct(blueprint, matMods=dict()) + component = componentDesign.construct( + blueprint, {}, inputHeightsConsideredHot + ) # override free component multiplicity if it's set based on the group definition component.setDimension("mult", groupedComponent.mult) _setComponentFlags(component, self.flags, blueprint) @@ -229,29 +242,33 @@ class can then use to modify the isotopics as necessary. constructedObject.material.getTD() ) - # set the custom density for non-custom material components after construction - self.setCustomDensity(constructedObject, blueprint, matMods) + self._setComponentCustomDensity( + constructedObject, + blueprint, + matMods, + inputHeightsConsideredHot, + ) return constructedObject - def setCustomDensity(self, constructedComponent, blueprint, matMods): + def _setComponentCustomDensity( + self, comp, blueprint, matMods, inputHeightsConsideredHot + ): """Apply a custom density to a material with custom isotopics but not a 'custom material'.""" if self.isotopics is None: # No custom isotopics specified return - density = blueprint.customIsotopics[self.isotopics].density - if density is None: + densityFromCustomIsotopic = blueprint.customIsotopics[self.isotopics].density + if densityFromCustomIsotopic is None: # Nothing to do return - if density <= 0: + if densityFromCustomIsotopic <= 0: runLog.error( "A zero or negative density was specified in a custom isotopics input. " "This is not permitted, if a 0 density material is needed, use 'Void'. " - "The component is {} and the isotopics entry is {}.".format( - constructedComponent, self.isotopics - ) + f"The component is {comp} and the isotopics entry is {self.isotopics}." ) raise ValueError( "A zero or negative density was specified in the custom isotopics for a component" @@ -261,30 +278,40 @@ def setCustomDensity(self, constructedComponent, blueprint, matMods): if not isinstance(mat, materials.Custom): # check for some problem cases if "TD_frac" in matMods.keys(): - runLog.warning( - "Both TD_frac and a custom density (custom isotopics) has been specified for " - "material {}. The custom density will override the density calculated using " - "TD_frac.".format(self.material) + runLog.error( + f"Both TD_frac and a custom isotopic with density {blueprint.customIsotopics[self.isotopics]} " + f"has been specified for material {self.material}. This is an overspecification." ) if not mat.density(Tc=self.Tinput) > 0: runLog.error( - "A custom density has been assigned to material '{}', which has no baseline " + f"A custom density has been assigned to material '{self.material}', which has no baseline " "density. Only materials with a starting density may be assigned a density. " - "This comes up e.g. if isotopics are assigned to 'Void'.".format( - self.material - ) + "This comes up e.g. if isotopics are assigned to 'Void'." ) raise ValueError( "Cannot apply custom densities to materials without density." ) - densityRatio = density / constructedComponent.density() - constructedComponent.changeNDensByFactor(densityRatio) + # Apply a density scaling to account for the temperature change between Tinput + # Thot. There may be a better place in the initialization to determine + # if the block height will be interpreted as hot dimensions, which would + # allow us to not have to pass the case settings down this far + dLL = comp.material.linearExpansionFactor( + Tc=comp.temperatureInC, T0=comp.inputTemperatureInC + ) + if inputHeightsConsideredHot: + f = 1.0 / (1 + dLL) ** 2 + else: + f = 1.0 / (1 + dLL) ** 3 + + scaledDensity = comp.density() / f + densityRatio = densityFromCustomIsotopic / scaledDensity + comp.changeNDensByFactor(densityRatio) runLog.important( "A custom material density was specified in the custom isotopics for non-custom " - "material {}. The component density has been altered to " - "{}.".format(mat, constructedComponent.density()), + f"material {mat}. The component density has been altered to " + f"{comp.density()} at temperature {comp.temperatureInC} C", single=True, ) diff --git a/armi/reactor/blueprints/isotopicOptions.py b/armi/reactor/blueprints/isotopicOptions.py index 37ded7621..e8b2b3544 100644 --- a/armi/reactor/blueprints/isotopicOptions.py +++ b/armi/reactor/blueprints/isotopicOptions.py @@ -395,10 +395,10 @@ def apply(self, material): if self.density is not None: if not isinstance(material, materials.Custom): runLog.important( - "A custom density or number densities has been specified for non-custom " - "material {}. The material object's density will not be updated to prevent unintentional " - "density changes across the model. Only custom materials may have a density " - "specified.".format(material), + "A custom isotopic with associated density has been specified for non-`Custom` " + f"material {material}. The reference density of materials in the materials library " + "will not be changed, but the associated components will use the density " + "implied by the custom isotopics.", single=True, ) # specifically, non-Custom materials only use refDensity and dLL, mat.customDensity has no effect diff --git a/armi/reactor/blueprints/tests/test_blueprints.py b/armi/reactor/blueprints/tests/test_blueprints.py index d63c12467..7827dd5e1 100644 --- a/armi/reactor/blueprints/tests/test_blueprints.py +++ b/armi/reactor/blueprints/tests/test_blueprints.py @@ -32,6 +32,7 @@ from armi.utils import directoryChangers from armi.utils import textProcessors from armi.reactor.blueprints.gridBlueprint import saveToStream +from armi.settings.fwSettings.globalSettings import CONF_INPUT_HEIGHTS_HOT class TestBlueprints(unittest.TestCase): @@ -612,7 +613,7 @@ def test_topLevelComponentInput(self): # which is required during construction of a component design._resolveNuclides(cs) componentDesign = design.componentDesigns["freefuel"] - topComponent = componentDesign.construct(design, matMods=dict()) + topComponent = componentDesign.construct(design, {}, cs[CONF_INPUT_HEIGHTS_HOT]) self.assertEqual(topComponent.getDimension("od", cold=True), 4.0) self.assertGreater(topComponent.getVolume(), 0.0) self.assertGreater(topComponent.getMass("U235"), 0.0) diff --git a/armi/reactor/blueprints/tests/test_customIsotopics.py b/armi/reactor/blueprints/tests/test_customIsotopics.py index f20cec81b..d971cb658 100644 --- a/armi/reactor/blueprints/tests/test_customIsotopics.py +++ b/armi/reactor/blueprints/tests/test_customIsotopics.py @@ -309,7 +309,12 @@ class TestCustomIsotopics(unittest.TestCase): @classmethod def setUpClass(cls): cs = settings.Settings() - cs = cs.modified(newSettings={CONF_XS_KERNEL: "MC2v2"}) + cs = cs.modified( + newSettings={ + CONF_XS_KERNEL: "MC2v2", + "inputHeightsConsideredHot": False, + } + ) cls.bp = blueprints.Blueprints.load(cls.yamlString) cls.a = cls.bp.constructAssem(cs, name="fuel a") @@ -356,14 +361,19 @@ def test_densitiesAppliedToNonCustomMaterials(self): # A block with custom density set via number density fuel8 = self.a[8].getComponent(Flags.FUEL) + dLL = fuel2.material.linearExpansionFactor(Tc=600, T0=25) + # the exponent here is 3 because inputHeightsConsideredHot = False. + # if inputHeightsConsideredHot were True, then we would use a factor of 2 instead + f = 1 / ((1 + dLL) ** 3) + # Check that the density is set correctly on the custom density block, # and that it is not the same as the original - self.assertAlmostEqual(19.1, fuel2.density()) + self.assertAlmostEqual(19.1 * f, fuel2.density()) self.assertNotAlmostEqual(fuel0.density(), fuel2.density(), places=2) # Check that the custom density block has the correct material self.assertEqual("UZr", fuel2.material.name) # Check that the block with only number densities set has a new density - self.assertAlmostEqual(19.1, fuel8.density()) + self.assertAlmostEqual(19.1 * f, fuel8.density()) # original material density should not be changed after setting a custom density component, # so a new block without custom isotopics and density should have the same density as the original self.assertAlmostEqual(fuel6.density(), fuel0.density()) @@ -387,12 +397,16 @@ def test_customDensityLogsAndErrors(self): # Check for log messages streamVal = mockLog.getStdout() - self.assertIn("Both TD_frac and a custom density", streamVal, msg=streamVal) + self.assertIn( + "Both TD_frac and a custom isotopic with density", + streamVal, + msg=streamVal, + ) self.assertIn( "A custom material density was specified", streamVal, msg=streamVal ) self.assertIn( - "A custom density or number densities has been specified", + "A custom isotopic with associated density has been specified for non-`Custom`", streamVal, msg=streamVal, ) diff --git a/doc/release/0.5.rst b/doc/release/0.5.rst index 0b92c954d..f730979d6 100644 --- a/doc/release/0.5.rst +++ b/doc/release/0.5.rst @@ -98,6 +98,7 @@ Bug Fixes #. Fixed hex block rotation in ``plotBlockDiagram``. (`PR#1926 `_) #. Fixed edge case in ``assemblyBlueprint._checkParamConsistency()``. (`PR#1928 `_) #. Fixed wetted perimeter for hex inner ducts. (`PR#1985 `_) +#. Fixing number densities when custom isotopics and material properties are combined. (`PR#1822 `_) Quality Work ------------ From 9ca12b3d2be8c855dee8c898a5120dd77fcf5426 Mon Sep 17 00:00:00 2001 From: ckeckler Date: Tue, 10 Dec 2024 09:47:17 -0600 Subject: [PATCH 2/9] Force plotAssemblyTypes to always use instantiated assemblies, and allow user to specify hot or not --- armi/bookkeeping/report/newReportUtils.py | 18 +++++++--- armi/bookkeeping/report/reportingUtils.py | 20 ++++++++--- armi/reactor/converters/uniformMesh.py | 3 +- armi/utils/plotting.py | 44 +++++++++++------------ armi/utils/tests/test_plotting.py | 9 +++-- 5 files changed, 54 insertions(+), 40 deletions(-) diff --git a/armi/bookkeeping/report/newReportUtils.py b/armi/bookkeeping/report/newReportUtils.py index 98c14bf92..3750925cc 100644 --- a/armi/bookkeeping/report/newReportUtils.py +++ b/armi/bookkeeping/report/newReportUtils.py @@ -628,9 +628,17 @@ def insertCoreAndAssemblyMaps( generateFullCoreMap : bool, default False showBlockAxMesh : bool, default True """ - assemPrototypes = set() + assemPrototypes = [] for aKey in blueprint.assemDesigns.keys(): - assemPrototypes.add(blueprint.constructAssem(cs, name=aKey)) + a = blueprint.constructAssem(cs, name=aKey) + # since we will be plotting cold input heights, we need to make sure that + # that these new assemblies have access to a blueprints somewhere up the + # composite chain. normally this would happen through an assembly's parent + # reactor, but because these newly created assemblies are in the load queue, + # they will not have a parent reactor. to get around this, we just attach + # the blueprints to the assembly directly. + a.blueprints = blueprint + assemPrototypes.append(a) counts = { assemDesign.name: len(r.core.getChildrenOfType(assemDesign.name)) @@ -648,7 +656,7 @@ def insertCoreAndAssemblyMaps( report[DESIGN]["Assembly Designs"] = newReports.Section("Assembly Designs") currentSection = report[DESIGN]["Assembly Designs"] for plotNum, assemBatch in enumerate( - iterables.chunk(list(assemPrototypes), MAX_ASSEMS_PER_ASSEM_PLOT), start=1 + iterables.chunk(assemPrototypes, MAX_ASSEMS_PER_ASSEM_PLOT), start=1 ): assemPlotImage = newReports.Image( imageCaption, @@ -656,11 +664,11 @@ def insertCoreAndAssemblyMaps( ) assemPlotName = os.path.abspath(f"{core.name}AssemblyTypes{plotNum}.png") plotting.plotAssemblyTypes( - blueprint, - assemPlotName, assemBatch, + assemPlotName, maxAssems=MAX_ASSEMS_PER_ASSEM_PLOT, showBlockAxMesh=showBlockAxMesh, + hot=False, ) currentSection.addChildElement(assemPlotImage, assemPlotName) diff --git a/armi/bookkeeping/report/reportingUtils.py b/armi/bookkeeping/report/reportingUtils.py index 9ac13287c..1a58cc14e 100644 --- a/armi/bookkeeping/report/reportingUtils.py +++ b/armi/bookkeeping/report/reportingUtils.py @@ -1062,10 +1062,22 @@ def makeCoreAndAssemblyMaps(r, cs, generateFullCoreMap=False, showBlockAxMesh=Tr generateFullCoreMap : bool, default False showBlockAxMesh : bool, default True """ - assemsInCore = list(r.blueprints.assemblies.values()) + assems = [] + blueprints = r.blueprints + for aKey in blueprints.assemDesigns.keys(): + a = blueprints.constructAssem(cs, name=aKey) + # since we will be plotting cold input heights, we need to make sure that + # that these new assemblies have access to a blueprints somewhere up the + # composite chain. normally this would happen through an assembly's parent + # reactor, but because these newly created assemblies are in the load queue, + # they will not have a parent reactor. to get around this, we just attach + # the blueprints to the assembly directly. + a.blueprints = blueprints + assems.append(a) + core = r.core for plotNum, assemBatch in enumerate( - iterables.chunk(assemsInCore, MAX_ASSEMS_PER_ASSEM_PLOT), start=1 + iterables.chunk(assems, MAX_ASSEMS_PER_ASSEM_PLOT), start=1 ): assemPlotImage = copy(report.ASSEM_TYPES) assemPlotImage.title = assemPlotImage.title + " ({})".format(plotNum) @@ -1073,11 +1085,11 @@ def makeCoreAndAssemblyMaps(r, cs, generateFullCoreMap=False, showBlockAxMesh=Tr report.data.Report.componentWellGroups.insert(-1, assemPlotImage) assemPlotName = os.path.abspath(f"{core.name}AssemblyTypes{plotNum}.png") plotting.plotAssemblyTypes( - core.parent.blueprints, - assemPlotName, assemBatch, + assemPlotName, maxAssems=MAX_ASSEMS_PER_ASSEM_PLOT, showBlockAxMesh=showBlockAxMesh, + hot=False, ) # Create radial core map diff --git a/armi/reactor/converters/uniformMesh.py b/armi/reactor/converters/uniformMesh.py index 8bb949771..234d1a082 100644 --- a/armi/reactor/converters/uniformMesh.py +++ b/armi/reactor/converters/uniformMesh.py @@ -984,9 +984,8 @@ def plotConvertedReactor(self): ): assemPlotName = f"{self.convReactor.core.name}AssemblyTypes{plotNum}-rank{armi.MPI_RANK}.png" plotting.plotAssemblyTypes( - self.convReactor.blueprints, - assemPlotName, assemBatch, + assemPlotName, maxAssems=6, showBlockAxMesh=True, ) diff --git a/armi/utils/plotting.py b/armi/utils/plotting.py index 147e1c20c..84d47c5e4 100644 --- a/armi/utils/plotting.py +++ b/armi/utils/plotting.py @@ -706,28 +706,25 @@ def updatePageDepthColor(self, newVal): def plotAssemblyTypes( - blueprints=None, - fileName=None, assems=None, + fileName=None, maxAssems=None, showBlockAxMesh=True, yAxisLabel=None, title=None, + hot=True, ) -> plt.Figure: """ Generate a plot showing the axial block and enrichment distributions of each assembly type in the core. Parameters ---------- - blueprints: Blueprints - The blueprints to plot assembly types of. (Either this or ``assems`` must be non-None.) + assems: list + list of assembly objects to be plotted. fileName : str or None Base for filename to write, or None for just returning the fig - assems: list - list of assembly objects to be plotted. (Either this or ``blueprints`` must be non-None.) - maxAssems: integer maximum number of assemblies to plot in the assems list. @@ -740,24 +737,14 @@ def plotAssemblyTypes( title: str Optionally, provide a title for the plot. + hot : bool, optional + If True, plot the hot block heights. If False, use cold heights from the inputs. + Returns ------- fig : plt.Figure The figure object created """ - # input validation - if assems is None and blueprints is None: - raise ValueError( - "At least one of these inputs must be non-None: blueprints, assems" - ) - - # handle defaults - if assems is None: - assems = list(blueprints.assemblies.values()) - - if not isinstance(assems, (list, set, tuple)): - assems = [assems] - if maxAssems is not None and not isinstance(maxAssems, int): raise TypeError("Maximum assemblies should be an integer") @@ -792,6 +779,7 @@ def plotAssemblyTypes( xAssemLoc, xAssemEndLoc, showBlockAxMesh, + hot, ) xAxisLabel = re.sub(" ", "\n", assem.getType().upper()) ax.text( @@ -840,6 +828,7 @@ def _plotBlocksInAssembly( xAssemLoc, xAssemEndLoc, showBlockAxMesh, + hot, ): # Set dictionary of pre-defined block types and colors for the plot lightsage = "xkcd:light sage" @@ -865,11 +854,18 @@ def _plotBlocksInAssembly( xTextLoc = xBlockLoc + blockWidth / 20.0 for b in assem: # get block height - try: - blockHeight = b.getInputHeight() - except AttributeError: - runLog.debug(f"No ancestor of {b} has blueprints", single=True) + if hot: blockHeight = b.getHeight() + else: + try: + blockHeight = b.getInputHeight() + except AttributeError: + raise ValueError( + f"Cannot plot cold height for block {b} in assembly {assem} " + "because it does not have access to a blueprints through any " + "of its parents. Either make sure that a blueprints is accessible " + " or plot the hot heights instead." + ) # Get the basic text label for the block try: diff --git a/armi/utils/tests/test_plotting.py b/armi/utils/tests/test_plotting.py index 2c930b91e..90e878993 100644 --- a/armi/utils/tests/test_plotting.py +++ b/armi/utils/tests/test_plotting.py @@ -60,7 +60,9 @@ def test_plotDepthMap(self): # indirectly tests plot face map def test_plotAssemblyTypes(self): with TemporaryDirectoryChanger(): plotPath = "coreAssemblyTypes1.png" - plotting.plotAssemblyTypes(self.r.core.parent.blueprints, plotPath) + plotting.plotAssemblyTypes( + list(self.r.core.parent.blueprints.assemblies.values()), plotPath + ) self._checkFileExists(plotPath) if os.path.exists(plotPath): @@ -68,7 +70,7 @@ def test_plotAssemblyTypes(self): plotPath = "coreAssemblyTypes2.png" plotting.plotAssemblyTypes( - self.r.core.parent.blueprints, + list(self.r.core.parent.blueprints.assemblies.values()), plotPath, yAxisLabel="y axis", title="title", @@ -78,9 +80,6 @@ def test_plotAssemblyTypes(self): if os.path.exists(plotPath): os.remove(plotPath) - with self.assertRaises(ValueError): - plotting.plotAssemblyTypes(None, plotPath, None) - if os.path.exists(plotPath): os.remove(plotPath) From 060d93d8dfcce4dba8fc64cc5f1567a5bbae5e0d Mon Sep 17 00:00:00 2001 From: ckeckler Date: Tue, 10 Dec 2024 09:57:28 -0600 Subject: [PATCH 3/9] Fix unit test call --- armi/utils/tests/test_plotting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/armi/utils/tests/test_plotting.py b/armi/utils/tests/test_plotting.py index 90e878993..47cd564c3 100644 --- a/armi/utils/tests/test_plotting.py +++ b/armi/utils/tests/test_plotting.py @@ -94,6 +94,7 @@ def test_plotBlocksInAssembly(self): 0.5, 5.6, True, + hot=True, ) self.assertEqual(xBlockLoc, 0.5) self.assertEqual(yBlockHeights[0], 25.0) From 9e8b4de78f9700f7cd9ce122e7d3967893a68a27 Mon Sep 17 00:00:00 2001 From: ckeckler Date: Tue, 10 Dec 2024 10:08:47 -0600 Subject: [PATCH 4/9] Fix the call to plotAssemblyTypes in the doc build --- doc/gallery-src/framework/run_programmaticReactorDefinition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/gallery-src/framework/run_programmaticReactorDefinition.py b/doc/gallery-src/framework/run_programmaticReactorDefinition.py index e270afeb7..369f13998 100644 --- a/doc/gallery-src/framework/run_programmaticReactorDefinition.py +++ b/doc/gallery-src/framework/run_programmaticReactorDefinition.py @@ -226,7 +226,7 @@ def buildSystems(): # build ARMI objects o = case.initializeOperator() fig = plotting.plotAssemblyTypes( - case.bp, + list(case.bp.assemblies.values()), None, showBlockAxMesh=True, ) From 9945e29da715e215df6816025c652d04442a3c80 Mon Sep 17 00:00:00 2001 From: bsculac <102382931+bsculac@users.noreply.github.com> Date: Wed, 11 Dec 2024 11:12:25 -0800 Subject: [PATCH 5/9] Accounting for symmetry in volume-integrated parameters (#2017) --- armi/bookkeeping/tests/test_historyTracker.py | 4 +- armi/physics/fuelCycle/fuelHandlers.py | 13 ++++- .../fuelCycle/tests/test_fuelHandlers.py | 3 +- armi/reactor/assemblies.py | 22 ++++++++ armi/reactor/blockParameters.py | 55 +++++++++++-------- armi/reactor/blocks.py | 11 +--- armi/reactor/composites.py | 26 ++------- armi/reactor/tests/test_assemblies.py | 31 ++++++++++- armi/reactor/tests/test_blocks.py | 18 ++---- armi/tests/tutorials/data_model.ipynb | 6 +- 10 files changed, 115 insertions(+), 74 deletions(-) diff --git a/armi/bookkeeping/tests/test_historyTracker.py b/armi/bookkeeping/tests/test_historyTracker.py index 0b615a37d..e27a92395 100644 --- a/armi/bookkeeping/tests/test_historyTracker.py +++ b/armi/bookkeeping/tests/test_historyTracker.py @@ -184,7 +184,6 @@ def test_historyParameters(self): params[param] = [] for ts, years in enumerate(timesInYears): cycle, node = utils.getCycleNodeFromCumulativeNode(ts, self.o.cs) - params[param].append( hti.getBlockHistoryVal(bName, param, (cycle, node)) ) @@ -196,7 +195,8 @@ def test_historyParameters(self): # verify the power parameter is retrievable from the history self.assertEqual(o.cs["power"], 1000000000.0) self.assertAlmostEqual(params["power"][0], 360, delta=0.1) - self.assertEqual(params["power"][0], params["power"][1]) + # assembly was moved to the central location with 1/3rd symmetry + self.assertEqual(params["power"][0] / 3, params["power"][1]) # verify the power density parameter is retrievable from the history self.assertAlmostEqual(params["pdens"][0], 0.0785, delta=0.001) diff --git a/armi/physics/fuelCycle/fuelHandlers.py b/armi/physics/fuelCycle/fuelHandlers.py index abe079d0b..51e5eb81b 100644 --- a/armi/physics/fuelCycle/fuelHandlers.py +++ b/armi/physics/fuelCycle/fuelHandlers.py @@ -38,6 +38,7 @@ from armi.physics.fuelCycle.settings import CONF_ASSEMBLY_ROTATION_ALG from armi.reactor.flags import Flags from armi.utils.customExceptions import InputError +from armi.reactor.parameters import ParamLocation class FuelHandler: @@ -212,10 +213,18 @@ def _compareAssem(candidate, current): @staticmethod def _getParamMax(a, paramName, blockLevelMax=True): """Get parameter with Block-level maximum.""" + multiplier = a.getSymmetryFactor() + if multiplier != 1: + # handle special case: volume-integrated parameters where symmetry factor is not 1 + isVolumeIntegrated = ( + a.getBlocks()[0].p.paramDefs[paramName].location + == ParamLocation.VOLUME_INTEGRATED + ) + multiplier = a.getSymmetryFactor() if isVolumeIntegrated else 1.0 if blockLevelMax: - return a.getChildParamValues(paramName).max() + return a.getChildParamValues(paramName).max() * multiplier - return a.p[paramName] + return a.p[paramName] * multiplier def findAssembly( self, diff --git a/armi/physics/fuelCycle/tests/test_fuelHandlers.py b/armi/physics/fuelCycle/tests/test_fuelHandlers.py index 5248aa4de..52116ea9b 100644 --- a/armi/physics/fuelCycle/tests/test_fuelHandlers.py +++ b/armi/physics/fuelCycle/tests/test_fuelHandlers.py @@ -261,8 +261,9 @@ def test_width(self): for ring, power in zip(range(1, 8), range(10, 80, 10)): aList = assemsByRing[ring] for a in aList: + sf = a.getSymmetryFactor() # center assembly is only 1/3rd in the core for b in a: - b.p.power = power + b.p.power = power / sf paramName = "power" # 1 ring outer and inner from ring 3 diff --git a/armi/reactor/assemblies.py b/armi/reactor/assemblies.py index 29e8d8f94..da55c7694 100644 --- a/armi/reactor/assemblies.py +++ b/armi/reactor/assemblies.py @@ -22,6 +22,7 @@ import pickle from random import randint from typing import ClassVar, Optional, Type +from collections.abc import Iterable import numpy as np from scipy import interpolate @@ -206,6 +207,7 @@ def insert(self, index, obj): def moveTo(self, locator): """Move an assembly somewhere else.""" + oldSymmetryFactor = self.getSymmetryFactor() composites.Composite.moveTo(self, locator) if self.lastLocationLabel != self.DATABASE: self.p.numMoves += 1 @@ -213,6 +215,26 @@ def moveTo(self, locator): self.parent.childrenByLocator[locator] = self # symmetry may have changed (either moving on or off of symmetry line) self.clearCache() + self.scaleParamsToNewSymmetryFactor(oldSymmetryFactor) + + def scaleParamsToNewSymmetryFactor(self, oldSymmetryFactor): + scalingFactor = oldSymmetryFactor / self.getSymmetryFactor() + if scalingFactor == 1: + return + + volIntegratedParamsToScale = self.getBlocks()[0].p.paramDefs.atLocation( + ParamLocation.VOLUME_INTEGRATED + ) + for b in self.getBlocks(): + for param in volIntegratedParamsToScale: + name = param.name + if b.p[name] is None or isinstance(b.p[name], str): + continue + elif isinstance(b.p[name], Iterable): + b.p[name] = [value * scalingFactor for value in b.p[name]] + else: + # numpy array or other + b.p[name] = b.p[name] * scalingFactor def getNum(self): """Return unique integer for this assembly.""" diff --git a/armi/reactor/blockParameters.py b/armi/reactor/blockParameters.py index ab65301a0..8952bfc97 100644 --- a/armi/reactor/blockParameters.py +++ b/armi/reactor/blockParameters.py @@ -81,24 +81,6 @@ def getBlockParameterDefinitions(): description="Ratio of fissile mass to heavy metal mass at block-level", ) - pb.defParam( - "molesHmBOL", - units=f"{units.MOLES}", - description="Total number of atoms of heavy metal at BOL assuming a full assembly", - ) - - pb.defParam( - "massHmBOL", - units=units.GRAMS, - description="Mass of heavy metal at BOL", - ) - - pb.defParam( - "initialB10ComponentVol", - units=f"{units.CM}^3", - description="cc's of un-irradiated, cold B10 containing component (includes full volume if any B10)", - ) - pb.defParam( "molesHmBOLByPin", units=f"{units.MOLES}", @@ -108,12 +90,6 @@ def getBlockParameterDefinitions(): location=ParamLocation.CHILDREN, ) - pb.defParam( - "molesHmNow", - units=f"{units.MOLES}", - description="Total number of atoms of heavy metal", - ) - pb.defParam( "newDPA", units=units.DPA, @@ -161,6 +137,37 @@ def getBlockParameterDefinitions(): categories=["cumulative"], ) + with pDefs.createBuilder( + default=0.0, location=ParamLocation.VOLUME_INTEGRATED, categories=["depletion"] + ) as pb: + + pb.defParam( + "molesHmNow", + units=f"{units.MOLES}", + description="Total number of atoms of heavy metal", + ) + + pb.defParam( + "molesHmBOL", + units=f"{units.MOLES}", + description="Total number of atoms of heavy metal at BOL.", + ) + + pb.defParam( + "massHmBOL", + units=units.GRAMS, + description="Mass of heavy metal at BOL", + ) + + pb.defParam( + "initialB10ComponentVol", + units=f"{units.CM}^3", + description=( + "cc's of un-irradiated, cold B10 containing component " + "(includes full volume of any components with B10)" + ), + ) + pDefs.add( Parameter( name="depletionMatrix", diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py index e88e7cd01..023e86361 100644 --- a/armi/reactor/blocks.py +++ b/armi/reactor/blocks.py @@ -790,21 +790,16 @@ def completeInitialLoading(self, bolBlock=None): self.p.enrichmentBOL = self.getFissileMassEnrich() massHmBOL = 0.0 - sf = self.getSymmetryFactor() for child in self: - # multiplying by sf ends up cancelling out the symmetry factor used in - # Component.getMass(). So massHmBOL does not respect the symmetry factor. - hmMass = child.getHMMass() * sf + hmMass = child.getHMMass() massHmBOL += hmMass # Components have the following parameters but not every composite will # massHmBOL, molesHmBOL, puFrac if isinstance(child, components.Component): child.p.massHmBOL = hmMass - # to stay consistent with massHmBOL, molesHmBOL and puFrac should be - # independent of sf. As such, the need to be scaled by 1/sf. - child.p.molesHmBOL = child.getHMMoles() / sf + child.p.molesHmBOL = child.getHMMoles() child.p.puFrac = ( - self.getPuMoles() / sf / child.p.molesHmBOL + self.getPuMoles() / child.p.molesHmBOL if child.p.molesHmBOL > 0.0 else 0.0 ) diff --git a/armi/reactor/composites.py b/armi/reactor/composites.py index f61ad5580..62a0e329d 100644 --- a/armi/reactor/composites.py +++ b/armi/reactor/composites.py @@ -1947,29 +1947,18 @@ def getHMMass(self): def getHMMoles(self): """ - Get the number of moles of heavy metal in this object in full symmetry. + Get the number of moles of heavy metal in this object. Notes ----- - If an object is on a symmetry line, the number of moles will be scaled up by the - symmetry factor. This is done because this is typically used for tracking - burnup, and BOL moles are computed in full objects too so there are no - complications as things move on and off of symmetry lines. - - Warning - ------- - getHMMoles is different than every other get mass call since it multiplies by - symmetry factor but getVolume() on the block level divides by symmetry factor - causing them to cancel out. - - This was needed so that HM moles mass did not change based on if the - block/assembly was on a symmetry line or not. + If an object is on a symmetry line, the volume reported by getVolume + is reduced to reflect that the block is not wholly within the reactor. This + reduction in volume reduces the reported HM moles. """ return ( self.getHMDens() / units.MOLES_PER_CC_TO_ATOMS_PER_BARN_CM * self.getVolume() - * self.getSymmetryFactor() ) def getHMDens(self): @@ -3129,12 +3118,7 @@ def getPuMoles(self): nucNames = [nuc.name for nuc in elements.byZ[94].nuclides] puN = sum(self.getNuclideNumberDensities(nucNames)) - return ( - puN - / units.MOLES_PER_CC_TO_ATOMS_PER_BARN_CM - * self.getVolume() - * self.getSymmetryFactor() - ) + return puN / units.MOLES_PER_CC_TO_ATOMS_PER_BARN_CM * self.getVolume() class StateRetainer: diff --git a/armi/reactor/tests/test_assemblies.py b/armi/reactor/tests/test_assemblies.py index c18850ed0..787dd129c 100644 --- a/armi/reactor/tests/test_assemblies.py +++ b/armi/reactor/tests/test_assemblies.py @@ -204,7 +204,6 @@ def setUp(self): ) self.assembly = makeTestAssembly(NUM_BLOCKS, self.assemNum, r=self.r) - self.r.core.add(self.assembly) # Use these if they are needed self.blockParams = { @@ -267,6 +266,7 @@ def setUp(self): self.assembly.add(b) self.blockList.append(b) + self.r.core.add(self.assembly) self.assembly.calculateZCoords() def test_isOnWhichSymmetryLine(self): @@ -345,6 +345,35 @@ def test_moveTo(self): cur = self.assembly.spatialLocator self.assertEqual(cur, ref) + def test_scaleParamsWhenMoved(self): + """Volume integrated parameters must be scaled when an assembly is placed on a core boundary.""" + blockParams = { + # volume integrated parameters + "massHmBOL": 9.0, + "molesHmBOL": np.array([[1, 2, 3], [4, 5, 6]]), # ndarray for testing + "adjMgFlux": [1, 2, 3], # Should normally be an ndarray, list for testing + "lastMgFlux": "foo", # Should normally be an ndarray, str for testing + } + for b in self.assembly.getBlocks(Flags.FUEL): + b.p.update(blockParams) + + i, j = grids.HexGrid.getIndicesFromRingAndPos(1, 1) + locator = self.r.core.spatialGrid[i, j, 0] + self.assertEqual(self.assembly.getSymmetryFactor(), 1) + self.assembly.moveTo(locator) + self.assertEqual(self.assembly.getSymmetryFactor(), 3) + for b in self.assembly.getBlocks(Flags.FUEL): + # float + assert_allclose(b.p["massHmBOL"] / blockParams["massHmBOL"], 1 / 3) + # np.ndarray + assert_allclose(b.p["molesHmBOL"] / blockParams["molesHmBOL"], 1 / 3) + # list + assert_allclose( + np.array(b.p["adjMgFlux"]) / np.array(blockParams["adjMgFlux"]), 1 / 3 + ) + # string + self.assertEqual(b.p["lastMgFlux"], blockParams["lastMgFlux"]) + def test_getName(self): cur = self.assembly.getName() ref = self.name diff --git a/armi/reactor/tests/test_blocks.py b/armi/reactor/tests/test_blocks.py index 8205bf1d7..db2adf8a8 100644 --- a/armi/reactor/tests/test_blocks.py +++ b/armi/reactor/tests/test_blocks.py @@ -1230,13 +1230,7 @@ def test_completeInitialLoading(self, mock_sf): sf = self.block.getSymmetryFactor() cur = self.block.p.molesHmBOL - ref = ( - self.block.getHMDens() - / MOLES_PER_CC_TO_ATOMS_PER_BARN_CM - * height - * area - * sf - ) + ref = self.block.getHMDens() / MOLES_PER_CC_TO_ATOMS_PER_BARN_CM * height * area self.assertAlmostEqual(cur, ref, places=12) totalHMMass = 0.0 @@ -1244,20 +1238,16 @@ def test_completeInitialLoading(self, mock_sf): nucs = c.getNuclides() hmNucs = [nuc for nuc in nucs if nucDir.isHeavyMetal(nuc)] hmNDens = {hmNuc: c.getNumberDensity(hmNuc) for hmNuc in hmNucs} - hmMass = densityTools.calculateMassDensity(hmNDens) * c.getVolume() + # use sf to account for only a 1/sf portion of the component being in the block + hmMass = densityTools.calculateMassDensity(hmNDens) * c.getVolume() / sf totalHMMass += hmMass if hmMass: - # hmMass does not need to account for sf since what's calculated in blocks.completeInitialLoading - # ends up cancelling out sf self.assertAlmostEqual(c.p.massHmBOL, hmMass, places=12) - # since sf is cancelled out in massHmBOL, there needs to be a factor 1/sf here to cancel out the - # factor of sf in getHMMoles. self.assertAlmostEqual( c.p.molesHmBOL, sum(ndens for ndens in hmNDens.values()) / units.MOLES_PER_CC_TO_ATOMS_PER_BARN_CM - * c.getVolume() - / sf, + * c.getVolume(), places=12, ) else: diff --git a/armi/tests/tutorials/data_model.ipynb b/armi/tests/tutorials/data_model.ipynb index 9b6dbe8fd..739e5e2de 100644 --- a/armi/tests/tutorials/data_model.ipynb +++ b/armi/tests/tutorials/data_model.ipynb @@ -294,14 +294,18 @@ "mgFluxBase = np.arange(5)\n", "def setFakePower(core):\n", " for a in core:\n", + " sf = a.getSymmetryFactor()\n", " for b in a:\n", " vol = b.getVolume()\n", " coords = b.spatialLocator.getGlobalCoordinates()\n", " r = np.linalg.norm(abs(coords-center))\n", " fuelFlag = 10 if b.isFuel() else 1.0\n", - " b.p.power = peakPower / r**2 * fuelFlag\n", + " # Use the symmetry factor to account for the central assembly being split\n", + " b.p.power = peakPower / r**2 * fuelFlag / sf\n", " b.p.pdens = b.p.power/vol\n", " b.p.mgFlux = mgFluxBase*b.p.pdens\n", + " if b.isFuel():\n", + " print(b.p.power, b.getLocation())\n", "setFakePower(core)" ] }, From c078cde00832c559586cbcc255356bac445a7a30 Mon Sep 17 00:00:00 2001 From: bsculac <102382931+bsculac@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:05:36 -0800 Subject: [PATCH 6/9] Fixing _getParamMax for Volume-Integrated Assembly Params (#2033) --- armi/physics/fuelCycle/fuelHandlers.py | 13 +++++++--- .../fuelCycle/tests/test_fuelHandlers.py | 25 ++++++++++++++++--- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/armi/physics/fuelCycle/fuelHandlers.py b/armi/physics/fuelCycle/fuelHandlers.py index 51e5eb81b..9f99d5a8f 100644 --- a/armi/physics/fuelCycle/fuelHandlers.py +++ b/armi/physics/fuelCycle/fuelHandlers.py @@ -212,19 +212,24 @@ def _compareAssem(candidate, current): @staticmethod def _getParamMax(a, paramName, blockLevelMax=True): - """Get parameter with Block-level maximum.""" + """Get assembly/block-level maximum parameter value in assembly.""" multiplier = a.getSymmetryFactor() if multiplier != 1: # handle special case: volume-integrated parameters where symmetry factor is not 1 + if blockLevelMax: + paramCollection = a.getBlocks()[0].p + else: + paramCollection = a.p isVolumeIntegrated = ( - a.getBlocks()[0].p.paramDefs[paramName].location + paramCollection.paramDefs[paramName].location == ParamLocation.VOLUME_INTEGRATED ) multiplier = a.getSymmetryFactor() if isVolumeIntegrated else 1.0 + if blockLevelMax: return a.getChildParamValues(paramName).max() * multiplier - - return a.p[paramName] * multiplier + else: + return a.p[paramName] * multiplier def findAssembly( self, diff --git a/armi/physics/fuelCycle/tests/test_fuelHandlers.py b/armi/physics/fuelCycle/tests/test_fuelHandlers.py index 52116ea9b..b6bbc14a0 100644 --- a/armi/physics/fuelCycle/tests/test_fuelHandlers.py +++ b/armi/physics/fuelCycle/tests/test_fuelHandlers.py @@ -43,6 +43,7 @@ from armi.settings import caseSettings from armi.tests import TEST_ROOT, ArmiTestHelper, mockRunLogs from armi.utils import directoryChangers +from armi.reactor.parameters import ParamLocation class FuelHandlerTestHelper(ArmiTestHelper): @@ -153,14 +154,32 @@ def interactBOC(self, cycle=None): class TestFuelHandler(FuelHandlerTestHelper): - def test_getParamMax(self): + @patch("armi.reactor.assemblies.Assembly.getSymmetryFactor") + def test_getParamMax(self, mockGetSymmetry): + a = self.assembly + mockGetSymmetry.return_value = 1 + expectedValue = 0.5 + a.p["kInf"] = expectedValue + for b in a: + b.p["kInf"] = expectedValue + + # symmetry factor == 1 + res = fuelHandlers.FuelHandler._getParamMax(a, "kInf", True) + self.assertEqual(res, expectedValue) + + res = fuelHandlers.FuelHandler._getParamMax(a, "kInf", False) + self.assertEqual(res, expectedValue) + # symmetry factor == 3 + mockGetSymmetry.return_value = 3 + a.p.paramDefs["kInf"].location = ParamLocation.VOLUME_INTEGRATED + a.getBlocks()[0].p.paramDefs["kInf"].location = ParamLocation.VOLUME_INTEGRATED res = fuelHandlers.FuelHandler._getParamMax(a, "kInf", True) - self.assertEqual(res, 0.0) + self.assertAlmostEqual(res, expectedValue * 3) res = fuelHandlers.FuelHandler._getParamMax(a, "kInf", False) - self.assertEqual(res, 0.0) + self.assertAlmostEqual(res, expectedValue * 3) def test_interactBOC(self): # set up mock interface From f528eee67db0c2558740d97b04f80290200bd7d9 Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Fri, 20 Dec 2024 09:08:22 -0800 Subject: [PATCH 7/9] Removing dependence on six (#2037) --- armi/cases/case.py | 4 ++-- armi/cli/entryPoint.py | 5 +---- armi/mpiActions.py | 7 +++---- armi/nuclearDataIO/cccc/tests/test_cccc.py | 4 +--- armi/nuclearDataIO/tests/test_xsLibraries.py | 8 ++++---- .../neutronics/tests/test_crossSectionManager.py | 8 ++++---- armi/reactor/blockParameters.py | 4 +--- armi/reactor/composites.py | 6 ++---- armi/reactor/parameters/parameterCollections.py | 15 +++++++-------- armi/reactor/tests/test_reactors.py | 8 ++++---- armi/tests/mockRunLogs.py | 14 +++++++------- armi/utils/iterables.py | 8 +++----- 12 files changed, 39 insertions(+), 52 deletions(-) diff --git a/armi/cases/case.py b/armi/cases/case.py index faebff6dd..588b020c4 100644 --- a/armi/cases/case.py +++ b/armi/cases/case.py @@ -28,6 +28,7 @@ import ast import cProfile import glob +import io import os import pathlib import pstats @@ -38,7 +39,6 @@ import trace import coverage -import six from armi import context from armi import getPluginManager @@ -492,7 +492,7 @@ def _endProfiling(profiler=None): profiler.disable() profiler.dump_stats("profiler.{:0>3}.stats".format(context.MPI_RANK)) - statsStream = six.StringIO() + statsStream = io.StringIO() summary = pstats.Stats(profiler, stream=statsStream).sort_stats("cumulative") summary.print_stats() if context.MPI_SIZE > 0 and context.MPI_COMM is not None: diff --git a/armi/cli/entryPoint.py b/armi/cli/entryPoint.py index bef74f07d..d6832c708 100644 --- a/armi/cli/entryPoint.py +++ b/armi/cli/entryPoint.py @@ -20,8 +20,6 @@ import argparse from typing import Optional, Union -import six - from armi import context, runLog, settings @@ -46,8 +44,7 @@ def __new__(mcs, name, bases, attrs): return type.__new__(mcs, name, bases, attrs) -@six.add_metaclass(_EntryPointEnforcer) -class EntryPoint: +class EntryPoint(metaclass=_EntryPointEnforcer): """ Generic command line entry point. diff --git a/armi/mpiActions.py b/armi/mpiActions.py index 8026ddf1d..ca3fc9ca3 100644 --- a/armi/mpiActions.py +++ b/armi/mpiActions.py @@ -56,10 +56,9 @@ import collections import gc import math +import pickle import timeit -from six.moves import cPickle - from armi import context from armi import interfaces from armi import runLog @@ -138,7 +137,7 @@ def _mpiOperationHelper(self, obj, mpiFunction): self.o = self.r = self.cs = None try: return mpiFunction(obj, root=0) - except cPickle.PicklingError as error: + except pickle.PicklingError as error: runLog.error("Failed to {} {}.".format(mpiFunction.__name__, obj)) runLog.error(error) raise @@ -539,7 +538,7 @@ def invokeHook(self): # or how the interfaces are distributed. self.r._markSynchronized() - except (cPickle.PicklingError, TypeError) as error: + except (pickle.PicklingError, TypeError) as error: runLog.error("Failed to transmit on distribute state root MPI bcast") runLog.error(error) # workers are still waiting for a reactor object diff --git a/armi/nuclearDataIO/cccc/tests/test_cccc.py b/armi/nuclearDataIO/cccc/tests/test_cccc.py index 6f9ea18ee..33b16c2b8 100644 --- a/armi/nuclearDataIO/cccc/tests/test_cccc.py +++ b/armi/nuclearDataIO/cccc/tests/test_cccc.py @@ -15,8 +15,6 @@ import io import unittest -import six - from armi.nuclearDataIO import cccc @@ -104,4 +102,4 @@ def setUpClass(cls): cls.readerClass = cccc.AsciiRecordReader def setUp(self): - self.streamCls = six.StringIO + self.streamCls = io.StringIO diff --git a/armi/nuclearDataIO/tests/test_xsLibraries.py b/armi/nuclearDataIO/tests/test_xsLibraries.py index ae4b55229..88fd59ad3 100644 --- a/armi/nuclearDataIO/tests/test_xsLibraries.py +++ b/armi/nuclearDataIO/tests/test_xsLibraries.py @@ -15,11 +15,11 @@ import copy import filecmp import os +import pickle import traceback import unittest import numpy as np -from six.moves import cPickle from armi.nucDirectory import nuclideBases from armi.nuclearDataIO import xsLibraries @@ -87,15 +87,15 @@ def setUpClass(cls): cls.xsLibGenerationErrorStack = traceback.format_exc() def test_canPickleAndUnpickleISOTXS(self): - pikAA = cPickle.loads(cPickle.dumps(self.isotxsAA)) + pikAA = pickle.loads(pickle.dumps(self.isotxsAA)) self.assertTrue(xsLibraries.compare(pikAA, self.isotxsAA)) def test_canPickleAndUnpickleGAMISO(self): - pikAA = cPickle.loads(cPickle.dumps(self.gamisoAA)) + pikAA = pickle.loads(pickle.dumps(self.gamisoAA)) self.assertTrue(xsLibraries.compare(pikAA, self.gamisoAA)) def test_canPickleAndUnpicklePMATRX(self): - pikAA = cPickle.loads(cPickle.dumps(self.pmatrxAA)) + pikAA = pickle.loads(pickle.dumps(self.pmatrxAA)) self.assertTrue(xsLibraries.compare(pikAA, self.pmatrxAA)) def test_compareWorks(self): diff --git a/armi/physics/neutronics/tests/test_crossSectionManager.py b/armi/physics/neutronics/tests/test_crossSectionManager.py index ec86ab3ea..219d852fb 100644 --- a/armi/physics/neutronics/tests/test_crossSectionManager.py +++ b/armi/physics/neutronics/tests/test_crossSectionManager.py @@ -19,12 +19,11 @@ """ import copy import os +import pickle import unittest from io import BytesIO from unittest.mock import MagicMock -from six.moves import cPickle - from armi import settings from armi.physics.neutronics import crossSectionGroupManager from armi.physics.neutronics.const import CONF_CROSS_SECTION @@ -73,9 +72,9 @@ def test_getBlocksInGroup(self): def test_is_pickleable(self): self.bc.weightingParam = "test" buf = BytesIO() - cPickle.dump(self.bc, buf) + pickle.dump(self.bc, buf) buf.seek(0) - newBc = cPickle.load(buf) + newBc = pickle.load(buf) self.assertEqual(self.bc.weightingParam, newBc.weightingParam) @@ -85,6 +84,7 @@ def setUp(self): for bi, b in enumerate(self.blockList): b.setType("fuel") b.p.percentBu = bi / 4.0 * 100 + self.blockList[0], self.blockList[2] = self.blockList[2], self.blockList[0] self.bc = MedianBlockCollection( self.blockList[0].core.r.blueprints.allNuclidesInProblem diff --git a/armi/reactor/blockParameters.py b/armi/reactor/blockParameters.py index 8952bfc97..e96010cbd 100644 --- a/armi/reactor/blockParameters.py +++ b/armi/reactor/blockParameters.py @@ -13,8 +13,6 @@ # limitations under the License. """Parameter definitions for Blocks.""" -import six - from armi import runLog from armi.physics.neutronics import crossSectionGroupManager from armi.reactor import parameters @@ -207,7 +205,7 @@ def envGroup(self, envGroupChar): ) self.envGroupNum = intValue return - elif not isinstance(envGroupChar, six.string_types): + elif not isinstance(envGroupChar, str): raise Exception( f"Wrong type for envGroupChar {envGroupChar}: {type(envGroupChar)}" ) diff --git a/armi/reactor/composites.py b/armi/reactor/composites.py index 62a0e329d..1b2bad687 100644 --- a/armi/reactor/composites.py +++ b/armi/reactor/composites.py @@ -39,7 +39,6 @@ from typing import Dict, List, Optional, Tuple, Type, Union import numpy as np -import six from armi import context, runLog, utils from armi.nucDirectory import elements, nucDir, nuclideBases @@ -759,10 +758,9 @@ def hasFlags(self, typeID: TypeSpec, exact=False): """ if not typeID: return not exact - if isinstance(typeID, six.string_types): + if isinstance(typeID, str): raise TypeError( - "Must pass Flags, or an iterable of Flags; Strings are no longer " - "supported" + "Must pass Flags, or an iterable of Flags; Strings are no longer supported" ) elif not isinstance(typeID, Flags): diff --git a/armi/reactor/parameters/parameterCollections.py b/armi/reactor/parameters/parameterCollections.py index e3cc0d886..c588563cc 100644 --- a/armi/reactor/parameters/parameterCollections.py +++ b/armi/reactor/parameters/parameterCollections.py @@ -12,13 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Any, Optional, List, Set, Iterator, Callable import copy import pickle -from typing import Any, Optional, List, Set, Iterator, Callable import sys import numpy as np -import six from armi import runLog from armi.reactor.parameters import parameterDefinitions, exceptions @@ -38,10 +37,10 @@ This is a counter of the number of instances of all types. They are useful for tracking items through the history of a database. -.. warning:: - - This is not MPI safe. We also have not done anything to make it thread safe, - except that the GIL exists. +Warning +------- +This is not MPI safe. We also have not done anything to make it thread safe, except that the GIL +exists. """ @@ -365,7 +364,7 @@ def __setitem__(self, name, value): ) def __delitem__(self, name): - if isinstance(name, six.string_types): + if isinstance(name, str): pd = self.paramDefs[name] if hasattr(self, pd.fieldName): pd.assigned = SINCE_ANYTHING @@ -374,7 +373,7 @@ def __delitem__(self, name): del self._hist[name] def __contains__(self, name): - if isinstance(name, six.string_types): + if isinstance(name, str): return hasattr(self, "_p_" + name) else: return name in self._hist diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 6187155e6..ea03fbaa5 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -15,12 +15,12 @@ import copy import logging import os +import pickle import unittest from math import sqrt from unittest.mock import patch from numpy.testing import assert_allclose, assert_equal -from six.moves import cPickle from armi import operators from armi import runLog @@ -170,7 +170,7 @@ def loadTestReactor( if isPickeledReactor and TEST_REACTOR: # return test reactor only if no custom settings are needed. - o, r, assemNum = cPickle.loads(TEST_REACTOR) + o, r, assemNum = pickle.loads(TEST_REACTOR) o.reattach(r, o.cs) return o, r @@ -196,7 +196,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, o.r.p.maxAssemNum), protocol=2) + TEST_REACTOR = pickle.dumps((o, o.r, o.r.p.maxAssemNum), protocol=2) return o, o.r @@ -934,7 +934,7 @@ def test_getMass(self): assert_allclose(mass1, mass2) def test_isPickleable(self): - loaded = cPickle.loads(cPickle.dumps(self.r)) + loaded = pickle.loads(pickle.dumps(self.r)) # ensure we didn't break the current reactor self.assertIs(self.r.core.spatialGrid.armiObject, self.r.core) diff --git a/armi/tests/mockRunLogs.py b/armi/tests/mockRunLogs.py index bc10db3a5..38b5c7462 100644 --- a/armi/tests/mockRunLogs.py +++ b/armi/tests/mockRunLogs.py @@ -12,20 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This module contains subclasses of the armi.runLog._RunLog class that can be used to determine whether or not -one of the specific methods were called. These should only be used in testing. +This module contains subclasses of the armi.runLog._RunLog class that can be used to determine +whether or not one of the specific methods were called. These should only be used in testing. """ -import six +import io import sys from armi import runLog class BufferLog(runLog._RunLog): - r"""Log which captures the output in attributes instead of emitting them. + """Log which captures the output in attributes instead of emitting them. - Used mostly in testing to ensure certain things get output, or to prevent any output - from showing. + Used mostly in testing to ensure certain things get output, or to prevent any output from + showing. """ def __init__(self, *args, **kwargs): @@ -34,7 +34,7 @@ def __init__(self, *args, **kwargs): self._outputStream = "" self._singleMessageCounts = {} self._singleWarningMessageCounts = {} - self._errStream = six.StringIO() + self._errStream = io.StringIO() sys.stderr = self._errStream self.setVerbosity(0) diff --git a/armi/utils/iterables.py b/armi/utils/iterables.py index ce204aa0a..70fafc0a2 100644 --- a/armi/utils/iterables.py +++ b/armi/utils/iterables.py @@ -13,11 +13,9 @@ # limitations under the License. """Module of utilities to help dealing with iterable objects in Python.""" -from itertools import tee, chain +from itertools import chain, filterfalse, tee import struct -from six.moves import filterfalse, map, xrange, filter - import numpy as np @@ -43,7 +41,7 @@ def chunk(lst, n): >>> list(chunk([1,2,3,4,5,6,7,8,9,10], 4)) [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10]] """ - for i in xrange(0, len(lst), n): + for i in range(0, len(lst), n): yield lst[i : i + n] @@ -84,7 +82,7 @@ def split(a, n, padWith=()): k, m = divmod(N, n) chunked = [ - a[i * k + min(i, m) : (i + 1) * k + min(i + 1, m)] or padWith for i in xrange(n) + a[i * k + min(i, m) : (i + 1) * k + min(i + 1, m)] or padWith for i in range(n) ] return chunked From f86ef61964cab1be8064468627906d85519788eb Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Fri, 20 Dec 2024 09:29:12 -0800 Subject: [PATCH 8/9] Identifying all first-order dependencies (#2039) --- pyproject.toml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a1b4c6a2f..330094aa1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,8 +31,8 @@ authors = [ ] dependencies = [ "coverage>=7.2.0", # Code coverage tool. Sadly baked into every Case. - "h5py>=3.9 ; python_version >= '3.11.0'", # Needed because our database files are H5 format "h5py>=3.0,<=3.9 ; python_version < '3.11.0'", + "h5py>=3.9 ; python_version >= '3.11.0'", # Needed because our database files are H5 format "htmltree>=0.7.6", # Our reports have HTML output "matplotlib>=3.5.3,<3.8.0", # Important plotting library "numpy>=1.21", # Important math library @@ -40,8 +40,9 @@ dependencies = [ "pluggy>=1.2.0", # Central tool behind the ARMI Plugin system "pyDOE>=0.3.8", # We import a Latin-hypercube algorithm to explore a phase space "pyevtk>=1.2.0", # Handles binary VTK visualization files - "ruamel.yaml.clib ; python_version >= '3.11.0'", # C-based core of ruamel below + "python-dateutil>=2.2", # To read a datetime string more easily. "ruamel.yaml ; python_version >= '3.11.0'", # Our foundational YAML library + "ruamel.yaml.clib ; python_version >= '3.11.0'", # C-based core of ruamel below "ruamel.yaml.clib<=0.2.7 ; python_version < '3.11.0'", # C-based core of ruamel below "ruamel.yaml<=0.17.21 ; python_version < '3.11.0'", # Our foundational YAML library "scipy>=1.7.0", # Used for curve-fitting and matrix math @@ -81,9 +82,10 @@ test = [ "ipykernel>=6.0.0", # IPython Kernel (We run test notebooks from the doc tutorials.) "jupyter_client>=7.0.0", # Reference implementation of the Jupyter protocol "nbconvert>=7.0.0", # Converting Jupyter Notebooks to other formats - "pytest>=7.0.0", # Our primary test tooling + "nbformat>=5.5.0", # Jupyter Notebook reader "pytest-cov>=4.0.0", # coverage plugin "pytest-xdist>=3.0.0", # To spread our tests over multiple CPUs + "pytest>=7.0.0", # Our primary test tooling "ruff==0.5.1", # Linting and code formatting (version-pinned) ] docs = [ From ca7907e2f0efbf295cb2002fc182b997606d7bab Mon Sep 17 00:00:00 2001 From: ckeckler Date: Thu, 2 Jan 2025 10:48:14 -0600 Subject: [PATCH 9/9] Add release note --- doc/release/0.5.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/release/0.5.rst b/doc/release/0.5.rst index 0b92c954d..361f9fd6e 100644 --- a/doc/release/0.5.rst +++ b/doc/release/0.5.rst @@ -16,6 +16,7 @@ API Changes Bug Fixes --------- +#. Fix assembly plots to use initial block heights. (`PR#1998 `_) #. TBD Quality Work