From 67df5a00a858bac3d655e054ae003dbefe19bf90 Mon Sep 17 00:00:00 2001 From: jake hader Date: Mon, 14 Feb 2022 22:10:20 -0800 Subject: [PATCH] WIP --- .../latticePhysics/latticePhysicsWriter.py | 8 +- armi/reactor/converters/geometryConverters.py | 286 +++++++++++------- .../tests/test_geometryConverters.py | 33 +- armi/reactor/flags.py | 14 +- armi/reactor/reactors.py | 70 +++-- armi/reactor/tests/test_flags.py | 29 ++ armi/reactor/tests/test_reactors.py | 4 +- armi/utils/tests/test_flags.py | 2 +- 8 files changed, 276 insertions(+), 170 deletions(-) diff --git a/armi/physics/neutronics/latticePhysics/latticePhysicsWriter.py b/armi/physics/neutronics/latticePhysics/latticePhysicsWriter.py index bbb810f3b..4e4181427 100644 --- a/armi/physics/neutronics/latticePhysics/latticePhysicsWriter.py +++ b/armi/physics/neutronics/latticePhysics/latticePhysicsWriter.py @@ -204,11 +204,9 @@ def _getAllNuclidesByCategory(self, component=None): """ dfpDensities = self._getDetailedFPDensities() - ( - coolantNuclides, - fuelNuclides, - structureNuclides, - ) = self.r.core.getNuclideCategories() + coolantNuclides = self.r.core.nuclideCategories["coolant"] + fuelNuclides = self.r.core.nuclideCategories["fuel"] + structureNuclides = self.r.core.nuclideCategories["structure"] nucDensities = {} subjectObject = component or self.block depletableNuclides = nuclideBases.getDepletableNuclides( diff --git a/armi/reactor/converters/geometryConverters.py b/armi/reactor/converters/geometryConverters.py index fcfd0d625..566b30af5 100644 --- a/armi/reactor/converters/geometryConverters.py +++ b/armi/reactor/converters/geometryConverters.py @@ -11,6 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from unicodedata import category """ Change a reactor from one geometry to another. @@ -52,6 +53,7 @@ from armi.reactor.flags import Flags from armi.utils import hexagon from armi.reactor.converters import blockConverters +from armi.physics.neutronics.crossSectionGroupManager import getBlockNuclideTemperatureAvgTerms BLOCK_AXIAL_MESH_SPACING = ( 20 # Block axial mesh spacing set for nodal diffusion calculation (cm) @@ -340,22 +342,22 @@ class HexToRZThetaConverter(GeometryConverter): domainType=geometry.DomainType.FULL_CORE, boundaryType=geometry.BoundaryType.NO_SYMMETRY, ) - _BLOCK_MIXTURE_TYPE_MAP = { - "mixture control": [Flags.CONTROL], - "mixture fuel": [Flags.FUEL], - "mixture radial shield": [Flags.RADIAL | Flags.SHIELD], - "mixture axial shield": [Flags.AXIAL | Flags.SHIELD, Flags.SHIELD], - "mixture structure": [ + _BLOCK_MIXTURE_FLAG_MAP = { + Flags.CONTROL : [Flags.CONTROL], + Flags.FUEL: [Flags.FUEL], + Flags.RADIAL | Flags.SHIELD: [Flags.RADIAL | Flags.SHIELD], + Flags.AXIAL | Flags.SHIELD: [Flags.AXIAL | Flags.SHIELD, Flags.SHIELD], + Flags.STRUCTURE: [ Flags.GRID_PLATE, Flags.REFLECTOR, Flags.INLET_NOZZLE, Flags.HANDLING_SOCKET, ], - "mixture duct": [Flags.DUCT], - "mixture plenum": [Flags.PLENUM], + Flags.DUCT: [Flags.DUCT], + Flags.PLENUM: [Flags.PLENUM], } - _BLOCK_MIXTURE_TYPE_EXCLUSIONS = [ + _BLOCK_MIXTURE_FLAG_EXCLUSIONS = [ Flags.CONTROL, Flags.FUEL, Flags.RADIAL | Flags.SHIELD, @@ -569,8 +571,8 @@ def _setupConvertedReactor(self, grid): # the same nuclides. This comes into play in areas of the framework # (like in lattice physics), where the categorization makes an impact # for physics modeling. - self.convReactor.core._nuclideCategories = ( - self._sourceReactor.core._nuclideCategories + self.convReactor.core.nuclideCategories = ( + self._sourceReactor.core.nuclideCategories ) def _setAssemsInRadialZone(self, radialIndex, lowerRing, upperRing): @@ -725,22 +727,11 @@ def _createRadialThetaZone( lowerAxialZ = 0.0 for axialIndex, upperAxialZ in enumerate(self.meshConverter.axialMesh): - - # Setup the new block data - newBlockName = "B{:04d}{}".format( + newBlockName = "B{:05d}{}".format( int(newAssembly.getNum()), chr(axialIndex + 65) ) - newBlock = blocks.ThRZBlock(newBlockName) - - # Compute the homogenized block data - ( - newBlockAtoms, - newBlockType, - newBlockTemp, - newBlockVol, - ) = self.createHomogenizedRZTBlock( - newBlock, lowerAxialZ, upperAxialZ, zoneAssems - ) + # Create a new block. + newBlock, _homBlockVolume = self.createHomogenizedRZTBlock(newBlockName, lowerAxialZ, upperAxialZ, zoneAssems) # Compute radial zone outer diameter axialSegmentHeight = upperAxialZ - lowerAxialZ radialZoneVolume = self._calcRadialRingVolume( @@ -754,45 +745,32 @@ def _createRadialThetaZone( outerDiameter = blockConverters.getOuterDiamFromIDAndArea( innerDiameter, radialRingArea ) - - # Set new homogenized block parameters - material = materials.material.Material() - material.name = "mixture " - material.p.refDens = 1.0 # generic density. Will cancel out. - dims = { - "inner_radius": innerDiameter / 2.0, - "radius_differential": (outerDiameter - innerDiameter) / 2.0, - "inner_axial": lowerAxialZ, - "height": axialSegmentHeight, - "inner_theta": lowerTheta, - "azimuthal_differential": (upperTheta - lowerTheta), - "mult": 1.0, - "Tinput": newBlockTemp, - "Thot": newBlockTemp, - } - for nuc in self._sourceReactor.blueprints.allNuclidesInProblem: - material.setMassFrac(nuc, 0.0) - newComponent = components.DifferentialRadialSegment( - f"component {newBlockType}", material, **dims - ) + # It order to preserve the temperatures of nuclides that have been + # homogenized, we aim to break the number of materials and components + # into a set that defines the `FUEL`, `COOLANT`, and `STRUCTURE` compositions. + # For nuclides that have an overlap among these materials we set the + # number densities based on the volume fractions of the regions they + # came from. + dims = {"innerDiameter": innerDiameter, + "outerDiameter": outerDiameter, + "lowerAxialZ": lowerAxialZ, + "upperAxialZ": upperAxialZ, + "lowerTheta": lowerTheta, + "upperTheta": upperTheta,} + self._addNewComponents(newBlock, **dims) + + # Clear the cache so that volumes and other volume-dependent parameters + # can be re-calculated based on the added components. + newBlock.p.height = axialSegmentHeight newBlock.p.axMesh = int(axialSegmentHeight / BLOCK_AXIAL_MESH_SPACING) + 1 newBlock.p.zbottom = lowerAxialZ newBlock.p.ztop = upperAxialZ - + # Assign the new block cross section type and burn up group - newBlock.setType(newBlockType) newXsType, newBuGroup = self._createBlendedXSID(newBlock) newBlock.p.xsType = newXsType newBlock.p.buGroup = newBuGroup - - # Update the block dimensions and set the block densities - newComponent.updateDims() # ugh. - newBlock.p.height = axialSegmentHeight - newBlock.clearCache() - newBlock.add(newComponent) - for nuc, atoms in newBlockAtoms.items(): - newBlock.setNumberDensity(nuc, atoms / newBlockVol) - + self._writeRadialThetaZoneInfo(axialIndex + 1, axialSegmentHeight, newBlock) self._checkVolumeConservation(newBlock) @@ -802,6 +780,105 @@ def _createRadialThetaZone( self.convReactor.core.add(newAssembly) return outerDiameter + + def _addNewComponents(self, newBlock, innerDiameter, outerDiameter, + lowerAxialZ, upperAxialZ, lowerTheta, upperTheta): + """ + Add a set of components to the new homogenized block that was created. + + Notes + ----- + This is useful for maintaining temperature-dependence of components and the underlying + materials/nuclides rather than only having a block-averaged temperature that is applied + to all of its children. + """ + nuclideCategories = self.convReactor.core.nuclideCategories + + # Get the category label for each nuclide in the core. + categoryByNuclide = {} + for category, nucs in nuclideCategories.items(): + for nuc in nucs: + categoryByNuclide[nuc] = category + + # Set up some dictionary contains for the temperatures and + # number densities for each of the categories. These are assigned + # to each new component below after they are populated. + tempByNuclideCategory = collections.defaultdict(float) + ndensByNuclideCategory = collections.defaultdict(dict) + volumeFracsByNuclideCategory = collections.defaultdict(float) + for category in nuclideCategories: + ndensByNuclideCategory[category] = collections.defaultdict(float) + + print(newBlock) + for b in self.blockMap[newBlock]: + # Calculate the average temperatures of each category of nuclides. + for category, nucs in nuclideCategories.items(): + nvt, nv = getBlockNuclideTemperatureAvgTerms(b, nucs) + # Update the nvt and nv arrays to remove any indices in nv where the division + # of nvt/nv would result in a `nan`. + nnvt = [] + nnv = [] + for i, nuc in enumerate(nucs): + if nv[i] == 0.0: + continue + nnvt.append(nvt[i]) + nnv.append(nv[i]) + + tempByNuclideCategory[category] = numpy.mean(numpy.array(nnvt)/numpy.array(nnv)) + + # Determine the volume fractions of the each component/category and the number densities + for c in b: + + # This is the volume fraction of the component within the new homogenized block, + # taking into account the fact that the block that this component belongs to + # may have been sliced axially based on the mesh boundaries. + volFracInNewBlock = c.getVolumeFraction() * self.blockVolFracs[newBlock][b] + + ndens = c.getNumberDensities() + for nuc, category in categoryByNuclide.items(): + nucNumberDensity = ndens.get(nuc, 0.0) + ndensByNuclideCategory[category][nuc] += nucNumberDensity * volFracInNewBlock + + + #### THIS IS WRONG -- I am stumped how to get the right volume fractions for the + # new components within the homogenized block so that mass is preserved.... + if c.getName() == "coolant": + volumeFracsByNuclideCategory["coolant"] += volFracInNewBlock + elif "fuel" in c.getName(): + volumeFracsByNuclideCategory["fuel"] += volFracInNewBlock + else: + volumeFracsByNuclideCategory["structure"] += volFracInNewBlock + + +# for category in nuclideCategories: +# volumeFracsByNuclideCategory[category] /= sum(volumeFracsByNuclideCategory.values()) + print(sum(volumeFracsByNuclideCategory.values()), volumeFracsByNuclideCategory) + print(tempByNuclideCategory) + + # Create a new component for each category and add it to the + # homogenized block. + for category in volumeFracsByNuclideCategory: + material = materials.material.Material() + material.name = f"{category}" + material.p.refDens = 1.0 # Assign a density - This will be overwritten by `setNumberDensities` on the component-level. + dims = { + "inner_radius": innerDiameter / 2.0, + "radius_differential": (outerDiameter - innerDiameter) / 2.0, + "inner_axial": lowerAxialZ, + "height": (upperAxialZ - lowerAxialZ), + "inner_theta": lowerTheta, + "azimuthal_differential": (upperTheta - lowerTheta), + "mult": volumeFracsByNuclideCategory[category], + "Tinput": tempByNuclideCategory[category], + "Thot": tempByNuclideCategory[category], + } + newComponent = components.DifferentialRadialSegment( + f"{category}", material, **dims + ) + newComponent.setNumberDensities(ndensByNuclideCategory[category]) + newComponent.updateDims() + newBlock.clearCache() + newBlock.add(newComponent) def _calcRadialRingVolume(self, lowerZ, upperZ, radialIndex): """Compute the total volume of a list of assemblies within a ring between two axial heights.""" @@ -828,7 +905,7 @@ def _checkVolumeConservation(self, newBlock): ) def createHomogenizedRZTBlock( - self, homBlock, lowerAxialZ, upperAxialZ, radialThetaZoneAssems + self, blockName, lowerAxialZ, upperAxialZ, radialThetaZoneAssems ): """ Create the homogenized RZT block by computing the average atoms in the zone. @@ -836,30 +913,24 @@ def createHomogenizedRZTBlock( Additional calculations are performed to determine the homogenized block type, the block average temperature, and the volume fraction of each hex block that is in the new homogenized block. """ + homBlock = blocks.ThRZBlock(blockName) homBlockXsTypes = set() - numHexBlockByType = collections.Counter() - homBlockAtoms = collections.defaultdict(int) - homBlockVolume = 0.0 - homBlockTemperature = 0.0 + numHexBlockByFlag = collections.Counter() + homBlockVolume = 0.0 for assem in radialThetaZoneAssems: blocksHere = assem.getBlocksBetweenElevations(lowerAxialZ, upperAxialZ) for b, heightHere in blocksHere: homBlockXsTypes.add(b.p.xsType) - numHexBlockByType[b.getType().lower()] += 1 + numHexBlockByFlag[b.p.flags] += 1 blockVolumeHere = b.getVolume() * heightHere / b.getHeight() + homBlockVolume += blockVolumeHere if blockVolumeHere == 0.0: raise ValueError( "Geometry conversion failed. Block {} has zero volume".format(b) ) - homBlockVolume += blockVolumeHere - homBlockTemperature += b.getAverageTempInC() * blockVolumeHere - - numDensities = b.getNumberDensities() - - for nucName, nDen in numDensities.items(): - homBlockAtoms[nucName] += nDen * blockVolumeHere self.blockMap[homBlock].append(b) self.blockVolFracs[homBlock][b] = blockVolumeHere + # Notify if blocks with different xs types are being homogenized. May be undesired behavior. if len(homBlockXsTypes) > 1: msg = "Blocks {} with dissimilar XS IDs are being homogenized in {} between axial heights {} " "cm and {} cm. ".format( @@ -875,69 +946,64 @@ def createHomogenizedRZTBlock( else: runLog.important(msg) - homBlockType = self._getHomogenizedBlockType(numHexBlockByType) - homBlockTemperature = homBlockTemperature / homBlockVolume + homBlockFlag = self._getHomogenizedBlockFlag(numHexBlockByFlag) + homBlock.setType(Flags.toString(homBlockFlag)) for b in self.blockMap[homBlock]: self.blockVolFracs[homBlock][b] = ( self.blockVolFracs[homBlock][b] / homBlockVolume ) + + return homBlock, homBlockVolume - return homBlockAtoms, homBlockType, homBlockTemperature, homBlockVolume - - def _getHomogenizedBlockType(self, numHexBlockByType): + def _getHomogenizedBlockFlag(self, numHexBlockByFlags): """ - Generate the homogenized block mixture type based on the frequency of hex block types that were merged + Generate the homogenized block flag based on the frequency of hex block types that were merged together. Notes ----- - self._BLOCK_MIXTURE_TYPE_EXCLUSIONS: - The normal function of this method is to assign the mixture name based on the number of occurrences of the - block type. This list stops that and assigns the mixture based on the first occurrence. - (i.e. if the mixture has a set of blocks but it comes across one with the name of 'control' the process will - stop and the new mixture type will be set to 'mixture control' - - self._BLOCK_MIXTURE_TYPE_MAP: - A dictionary that provides the name of blocks that are condensed together + self._BLOCK_MIXTURE_FLAG_EXCLUSIONS: + The normal function of this method is to assign the flags based on the number of occurrences of the + block flag. This list stops that and assigns the mixture based on the first occurrence. + + self._BLOCK_MIXTURE_FLAG_MAP: + A dictionary that provides the flags of blocks that are condensed together. """ - assignedMixtureBlockType = None + assignedMixtureBlockFlag = None - # Find the most common block type out of the types in the block mixture type exclusions list - excludedBlockTypesInBlock = set( + # Find the most common block type out of the types in the block flags exclusions list + excludedBlockFlagsInBlock = set( [ - Flags.toString(x) - for x in self._BLOCK_MIXTURE_TYPE_EXCLUSIONS - for y in numHexBlockByType - if Flags.toString(x) in y + x + for x in self._BLOCK_MIXTURE_FLAG_EXCLUSIONS + for y in numHexBlockByFlags + if x in y ] ) - if excludedBlockTypesInBlock: - for blockFlags in self._BLOCK_MIXTURE_TYPE_EXCLUSIONS: - blockType = Flags.toString(blockFlags) - if blockType in excludedBlockTypesInBlock: - assignedMixtureBlockType = "mixture " + blockType - return assignedMixtureBlockType + if excludedBlockFlagsInBlock: + for blockFlags in self._BLOCK_MIXTURE_FLAG_EXCLUSIONS: + if blockFlags in excludedBlockFlagsInBlock: + assignedMixtureBlockFlag = blockFlags + return assignedMixtureBlockFlag # Assign block type by most common hex block type - mostCommonHexBlockType = sorted(numHexBlockByType.most_common(1))[0][ + mostCommonHexBlockType = sorted(numHexBlockByFlags.most_common(1))[0][ 0 ] # sort needed for tie break - - for mixtureType in sorted(self._BLOCK_MIXTURE_TYPE_MAP): - validBlockFlagsInMixture = self._BLOCK_MIXTURE_TYPE_MAP[mixtureType] - validBlockTypesInMixture = Flags.toString(validBlockFlagsInMixture) - for validBlockType in validBlockTypesInMixture: - if validBlockType in mostCommonHexBlockType: - assignedMixtureBlockType = mixtureType - return assignedMixtureBlockType - - assignedMixtureBlockType = "mixture structure" + + for flags in self._BLOCK_MIXTURE_FLAG_MAP: + validBlockFlagsInMixture = self._BLOCK_MIXTURE_FLAG_MAP[flags] + for validBlockFlag in validBlockFlagsInMixture: + if validBlockFlag in mostCommonHexBlockType: + return validBlockFlagsInMixture + + assignedMixtureBlockFlag = Flags.STRUCTURE runLog.debug( f"The mixture type for this homogenized block {mostCommonHexBlockType} " - f"was not determined and is defaulting to {assignedMixtureBlockType}" + f"was not determined and is defaulting to {assignedMixtureBlockFlag}" ) - return assignedMixtureBlockType + return assignedMixtureBlockFlag def _createBlendedXSID(self, newBlock): """ diff --git a/armi/reactor/converters/tests/test_geometryConverters.py b/armi/reactor/converters/tests/test_geometryConverters.py index 3e2fe7291..2c7228ce2 100644 --- a/armi/reactor/converters/tests/test_geometryConverters.py +++ b/armi/reactor/converters/tests/test_geometryConverters.py @@ -146,7 +146,7 @@ def tearDown(self): del self.cs del self.r - def testConvert(self): + def test_convert(self): converterSettings = { "radialConversionType": "Ring Compositions", "axialConversionType": "Axial Coordinates", @@ -168,7 +168,7 @@ def testConvert(self): self._checkNuclideMasses(expectedMassDict, newR) self._checkBlockAtMeshPoint(geomConv) self._checkReactorMeshCoordinates(geomConv) - figs = geomConv.plotConvertedReactor() + _figs = geomConv.plotConvertedReactor() with directoryChangers.TemporaryDirectoryChanger(): geomConv.plotConvertedReactor("fname") @@ -209,13 +209,11 @@ def _getExpectedData(self): return expectedMassDict, expectedNuclideList def _checkBlockComponents(self, newR): + expectedNumComponents = len(newR.core.nuclideCategories.keys()) for b in newR.core.getBlocks(): - if len(b) != 1: - raise ValueError( - "Block {} has {} components and should only have 1".format( - b, len(b) - ) - ) + if len(b) > expectedNumComponents: + raise ValueError(f"Block {b} has {len(b)} components and " + f"should have {expectedNumComponents}.") def _checkNuclidesMatch(self, expectedNuclideList, newR): """Check that the nuclide lists match before and after conversion""" @@ -253,26 +251,21 @@ def _checkNuclideMasses(self, expectedMassDict, newR): def _checkNuclideCategoriesAreSame(self, newR): """Check that the nuclide categories between the original core and the converted core are identical.""" self.assertDictEqual( - self.r.core._nuclideCategories, newR.core._nuclideCategories + self.r.core.nuclideCategories, newR.core.nuclideCategories ) def test_createHomogenizedRZTBlock(self): - newBlock = blocks.ThRZBlock("testBlock", self.cs) + """Test the creation of a new homogenized RZT block.""" a = self.r.core[0] converterSettings = {} geomConv = geometryConverters.HexToRZConverter( self.cs, converterSettings, expandReactor=self._expandReactor ) - volumeExpected = a.getVolume() - ( - _atoms, - _newBlockType, - _newBlockTemp, - newBlockVol, - ) = geomConv.createHomogenizedRZTBlock(newBlock, 0, a.getHeight(), [a]) - - # The volume of the radialZone and the radialThetaZone should be equal for RZ geometry - self.assertAlmostEqual(volumeExpected, newBlockVol) + newBlock, homBlockVolume = geomConv.createHomogenizedRZTBlock("mixture fuel", 0, a.getHeight(), [a]) + + self.assertEqual(a.getVolume(), homBlockVolume) + self.assertEqual(newBlock.p.type, "FUEL") + self.assertEqual(newBlock.p.flags, Flags.FUEL) class TestEdgeAssemblyChanger(unittest.TestCase): diff --git a/armi/reactor/flags.py b/armi/reactor/flags.py index 4bc5c8dfb..2944a2e78 100644 --- a/armi/reactor/flags.py +++ b/armi/reactor/flags.py @@ -279,7 +279,18 @@ class Flags(Flag): # Allows movement of lower plenum with control rod MOVEABLE = auto() - + + def __lt__(self, other): + """ + Allows the ``Flags`` to be sorted. + + Notes + ----- + Sorting can be required to have deterministic behavior + when looping over a set/list of ``Flags``. + """ + return self._value < other._value + @classmethod def fromStringIgnoreErrors(cls, typeSpec): return _fromStringIgnoreErrors(cls, typeSpec) @@ -292,7 +303,6 @@ def fromString(cls, typeSpec): def toString(cls, typeSpec): return _toString(cls, typeSpec) - class InvalidFlagsError(KeyError): """Raised when code attempts to look for an undefined flag.""" diff --git a/armi/reactor/reactors.py b/armi/reactor/reactors.py index 96c436d9b..c958fda0a 100644 --- a/armi/reactor/reactors.py +++ b/armi/reactor/reactors.py @@ -340,6 +340,23 @@ def refAssem(self): return assems[0] + @property + def nuclideCategories(self): + if not self._nuclideCategories: + self._getNuclideCategories() + + expectedNuclideCategories = ["coolant", "fuel", "structure"] + assert sorted(self._nuclideCategories.keys()) == expectedNuclideCategories, ( + f"Nuclide categories in {self} do not match the expected " + f"values of {expectedNuclideCategories}. " + f"Current values: {self._nuclideCategories.keys()}" + ) + return self._nuclideCategories + + @nuclideCategories.setter + def nuclideCategories(self, value): + self._nuclideCategories = value + def summarizeReactorStats(self): """Writes a summary of the reactor to check the mass and volume of all of the blocks.""" totalMass = 0.0 @@ -1238,7 +1255,7 @@ def getAllXsSuffixes(self): """Return all XS suffices (e.g. AA, AB, etc.) in the core.""" return sorted(set(b.getMicroSuffix() for b in self.getBlocks())) - def getNuclideCategories(self): + def _getNuclideCategories(self): """ Categorize nuclides as coolant, fuel and structure. @@ -1267,35 +1284,28 @@ def getNuclideCategories(self): set of nuclide names """ - if not self._nuclideCategories: - coolantNuclides = set() - fuelNuclides = set() - structureNuclides = set() - for c in self.iterComponents(): - if c.getName() == "coolant": - coolantNuclides.update(c.getNuclides()) - elif "fuel" in c.getName(): - fuelNuclides.update(c.getNuclides()) - else: - structureNuclides.update(c.getNuclides()) - structureNuclides -= coolantNuclides - structureNuclides -= fuelNuclides - remainingNuclides = ( - set(self.parent.blueprints.allNuclidesInProblem) - - structureNuclides - - coolantNuclides - ) - fuelNuclides.update(remainingNuclides) - self._nuclideCategories["coolant"] = coolantNuclides - self._nuclideCategories["fuel"] = fuelNuclides - self._nuclideCategories["structure"] = structureNuclides - self.summarizeNuclideCategories() - - return ( - self._nuclideCategories["coolant"], - self._nuclideCategories["fuel"], - self._nuclideCategories["structure"], + coolantNuclides = set() + fuelNuclides = set() + structureNuclides = set() + for c in self.iterComponents(): + if c.getName() == "coolant": + coolantNuclides.update(c.getNuclides()) + elif "fuel" in c.getName(): + fuelNuclides.update(c.getNuclides()) + else: + structureNuclides.update(c.getNuclides()) + structureNuclides -= coolantNuclides + structureNuclides -= fuelNuclides + remainingNuclides = ( + set(self.parent.blueprints.allNuclidesInProblem) + - structureNuclides + - coolantNuclides ) + fuelNuclides.update(remainingNuclides) + self._nuclideCategories["coolant"] = sorted(coolantNuclides) + self._nuclideCategories["fuel"] = sorted(fuelNuclides) + self._nuclideCategories["structure"] = sorted(structureNuclides) + self.summarizeNuclideCategories() def summarizeNuclideCategories(self): """Write summary table of the various nuclide categories within the reactor.""" @@ -2284,7 +2294,7 @@ def processLoading(self, cs): self.numRings = self.getNumRings() # TODO: why needed? - self.getNuclideCategories() + self.nuclideCategories # some blocks will not move in the core like grid plates... Find them and fix them in place stationaryBlocks = [] diff --git a/armi/reactor/tests/test_flags.py b/armi/reactor/tests/test_flags.py index 1e9a0ecf6..64f64b3c7 100644 --- a/armi/reactor/tests/test_flags.py +++ b/armi/reactor/tests/test_flags.py @@ -104,6 +104,35 @@ def test_isPickleable(self): stream = pickle.dumps(flags.Flags.BOND | flags.Flags.A) flag = pickle.loads(stream) self.assertEqual(flag, flags.Flags.BOND | flags.Flags.A) + + def test_areHashable(self): + """ + Test that the implemented ``Flags`` can be hashed. + + This is important for inserting flags into the keys of a dictionary. + """ + self.assertTrue(hash(flags.Flags.FUEL)) + + def test_areSortable(self): + """Test that flags can be ordered.""" + self.assertEqual(flags.Flags.FUEL, flags.Flags.FUEL) + #self.assertGreater(flags.Flags.CONTROL, flags.Flags.FUEL) + #self.assertGreaterEqual(flags.Flags.CONTROL, flags.Flags.FUEL) + #self.assertLessEqual(flags.Flags.FUEL, flags.Flags.CONTROL) + #self.assertLess(flags.Flags.FUEL, flags.Flags.CONTROL) + self.assertNotEqual(flags.Flags.FUEL, flags.Flags.CONTROL) + + strings = ["control", "axial shield", "radial shield", "fuel", "structure", + "plenum", "duct"] + sortedStrings = ["axial shield", "control", "duct", "fuel", "plenum", "radial shield", + "structure"] + self.assertEqual(sorted(strings), sortedStrings) + + print(sorted([flags.Flags.fromString(x) for x in strings])) + print([flags.Flags.fromString(x) for x in sortedStrings]) + self.assertEqual(sorted([flags.Flags.fromString(x) for x in strings]), + [flags.Flags.fromString(x) for x in sortedStrings]) + if __name__ == "__main__": diff --git a/armi/reactor/tests/test_reactors.py b/armi/reactor/tests/test_reactors.py index 43d12aa33..a6693e7b7 100644 --- a/armi/reactor/tests/test_reactors.py +++ b/armi/reactor/tests/test_reactors.py @@ -759,7 +759,7 @@ def test_getAssembliesInSquareRing(self, exclusions=[2]): self.assertSequenceEqual(actualAssemsInRing, expectedAssemsInRing) def test_getNuclideCategoriesLogging(self): - """Simplest possible test of the getNuclideCategories method and its logging""" + """Simplest possible test of the setting the ``nuclideCategories`` on the core and its logging""" log = mockRunLogs.BufferLog() # this strange namespace-stomping is used to the test to set the logger in reactors.Core @@ -769,7 +769,7 @@ def test_getNuclideCategoriesLogging(self): runLog.LOG = log # run the actual method in question - self.r.core.getNuclideCategories() + self.r.core.nuclideCategories messages = log.getStdoutValue() self.assertIn("Nuclide categorization", messages) diff --git a/armi/utils/tests/test_flags.py b/armi/utils/tests/test_flags.py index 3566ce0dd..de68ce765 100644 --- a/armi/utils/tests/test_flags.py +++ b/armi/utils/tests/test_flags.py @@ -130,4 +130,4 @@ def test_hashable(self): self.assertNotEqual(hash(f1), hash(f2)) def test_getitem(self): - self.assertEqual(ExampleFlag["FOO"], ExampleFlag.FOO) + self.assertEqual(ExampleFlag["FOO"], ExampleFlag.FOO) \ No newline at end of file