def _discussSkipped(self, skipped, errors): runLog.warning("Skipped {} objects".format(skipped)) runLog.warning( "errored out on {0} objects:\n {1}".format( len(errors), "\n".join([repr(ei)[:30] for ei in errors]) ) )
def _load_config(self): if armi.MPI_SIZE > 1: # prevent race conditions in parallel cases. Can happen in MPI or other parallel runs (e.g. unit tests) runLog.warning( "Skipping loading configuration while MPI_SIZE = {}, it should not need a config" .format(armi.MPI_SIZE)) return t_parsers = self.find_cfg_files(TEMPLATE_DATA) try: if not os.path.exists(armi.APP_DATA): os.makedirs(armi.APP_DATA) u_parsers = self.find_cfg_files(armi.APP_DATA) self._configs = self.align_user_cfgs(t_parsers, u_parsers) self.save() except OSError: # permission denied runLog.warning( "No accessible appdata resources found nor is creation possible" ) self._configs = ( t_parsers # can't do anything besides use the template config values ) for filename, cfg in self._configs.items(): self._expose_file(filename, cfg)
def applyInputParams(self, U233_wt_frac=None, *args, **kwargs): runLog.warning( "Material {} has not yet been tested for accuracy".format("ThU")) if U233_wt_frac is not None: self.adjustMassEnrichment(U233_wt_frac) material.FuelMaterial.applyInputParams(self, *args, **kwargs)
def __getitem__(self, group): try: return self.groups[group] except KeyError: runLog.warning( "Cannot locate group {} in report {}".format(group.title, self.title) )
def _compareMiscData(self): """Generates difference information for the registered misc data storages in the src & ref""" runLog.important("Misc. data comparisons...") names = iterables.Overlap( self.src._getDataNamesToCompare(), # pylint: disable=protected-access self.ref._getDataNamesToCompare(), # pylint: disable=protected-access ) self.differences.add("Known Misc Data", structure=names) names_matched = sorted(list(names.matched)) for name in names_matched: try: src_state_data = self.src.readDataFromDB(name) ref_state_data = self.ref.readDataFromDB(name) except KeyError: runLog.warning( "Table {} is listed as common between DB but is not " "actually in both databases. This indicates an inconsistent " "DB structure.".format(name)) continue keys = iterables.Overlap(src_state_data.keys(), ref_state_data.keys()) diffs = self._compareSingularStateData( keys, src_state_data, ref_state_data, weightKey=self.weights.get(name, ""), ) self.differences.add(name, structure=keys, diffs=diffs)
def checkTempRange(self, minV, maxV, val, label=""): r""" Checks if the given temperature (val) is between the minV and maxV temperature limits supplied. Label identifies what material type or element is being evaluated in the check. Parameters ---------- minV, maxV : float The minimum and maximum values that val is allowed to have. val : float The value to check whether it is between minV and maxV. label : str The name of the function or property that is being checked. """ if not minV <= val <= maxV: msg = "Temperature {0} out of range ({1} to {2}) for {3} {4}".format( val, minV, maxV, self.name, label) if FAIL_ON_RANGE or numpy.isnan(val): runLog.error(msg) raise ValueError else: runLog.warning( msg, single=True, label="T out of bounds for {} {}".format(self.name, label), )
def compare(lib1, lib2, tolerance=0.0, verbose=False): """ Compare two XSLibraries, and return True if equal, or False if not. Notes ----- Tolerance allows the user to ignore small changes that may be caused by small library differences or floating point cacluations the closer to zero the more differences will be shown 10**-5 is a good tolerance to use if not using default. Verbose shows the XS matrixes that are not equal """ equal = True # first check the lib properties (also need to unlock to prevent from getting an exception). equal &= xsLibraries.compareLibraryNeutronEnergies(lib1, lib2, tolerance) # compare the meta data equal &= lib1.isotxsMetadata.compare(lib2.isotxsMetadata, lib1, lib2) # check the nuclides for nucName in set(lib1.nuclideLabels + lib2.nuclideLabels): nuc1 = lib1.get(nucName, None) nuc2 = lib2.get(nucName, None) if nuc1 is None or nuc2 is None: warning = "Nuclide {:>20} in library {} is not present in library {} and cannot be compared" if nuc1: runLog.warning(warning.format(nuc1, 1, 2)) if nuc2: runLog.warning(warning.format(nuc2, 2, 1)) equal = False continue equal &= compareNuclideXS(nuc1, nuc2, tolerance, verbose) return equal
def _updateReactorParams(self, reactor, dbTimeStep): """Update reactor-/core-level parameters from the database""" # These are the names present in the reactors section of the DB dbParamNames = self.getReactorParamNames() reactorNames = set( pDef.name for pDef in parameters.ALL_DEFINITIONS.forType(reactors.Reactor) ) coreNames = set( pDef.name for pDef in parameters.ALL_DEFINITIONS.forType(reactors.Core) ) for paramName in dbParamNames: if paramName == "TimeStep": continue runLog.debug("Reading scalar {0}".format(paramName)) # get time-ordered list of scalar vals. Pick the relevant one. val = self.readReactorParam(paramName, dbTimeStep)[0] if val is parameters.NoDefault: continue if paramName in ["cycle", "timeNode"]: # int(float('0.000E+00')) works, but int('0.00E+00') val = int(float(val)) if paramName in reactorNames: reactor.p[paramName] = val elif paramName in coreNames: reactor.core.p[paramName] = val else: runLog.warning( 'The parameter "{}" was present in the database, but is not ' "recognized as a Reactor or Core parameter".format(paramName) )
def imposeBurnChain(burnChainStream): """ Apply transmutation and decay information to each nuclide. Notes ----- You cannot impose a burn chain twice. Doing so would require that you clean out the transmutations and decays from all the module-level nuclide bases, which generally requires that you rebuild them. But rebuilding those is not an option because some of them get set as class-level attributes and would be orphaned. If a need to change burn chains mid-run re-arises, then a better nuclideBase-level burnchain cleanup should be implemented so the objects don't have to change identity. Notes ----- We believe the transmutation information would probably be better stored on a less fundamental place (e.g. not on the NuclideBase). See Also -------- armi.nucDirectory.transmutations : describes file format """ global _burnChainImposed # pylint: disable=global-statement if _burnChainImposed: # the only time this should happen is if in a unit test that has already # processed conftest.py and is now building a Case that also imposes this. runLog.warning("Burn chain already imposed. Skipping reimposition.") return _burnChainImposed = True burnData = yaml.load(burnChainStream, Loader=yaml.FullLoader) for nucName, burnInfo in burnData.items(): nuclide = byName[nucName] # think of this protected stuff as "module level protection" rather than class. nuclide._processBurnData(burnInfo) # pylint: disable=protected-access
def _setBOLBond(assemblies): """Set initial bond fractions for each block in the core.""" assemsWithoutMatchingBond = set() for a in assemblies: for b in a: coolant = b.getComponent(Flags.COOLANT, quiet=True) bond = b.getComponent(Flags.BOND, quiet=True) if not bond: b.p.bondBOL = 0.0 continue b.p.bondBOL = sum( [bond.getNumberDensity(nuc) for nuc in bond.getNuclides()]) if not isinstance(bond.material, coolant.material.__class__): assemsWithoutMatchingBond.add(( a.getType(), b.getType(), bond.material.getName(), coolant.material.getName(), )) if assemsWithoutMatchingBond: runLog.warning( "The following have mismatching `{}` and `{}` materials:\n".format( Flags.BOND, Flags.COOLANT) + tabulate.tabulate( list(assemsWithoutMatchingBond), headers=[ "Assembly Type", "Block Type", "Bond Material", "Coolant Material", ], tablefmt="armi", ))
def readReactorParam(self, param, ts=None): """Read reactor param at all or one timesteps.""" timesteps = [ts] if ts is not None else self.getAllTimesteps() # need to try both since Reactor and Core are squashed in the DB. try: # pylint: disable=protected-access paramDef = reactors.Reactor.paramCollectionType.pDefs[param] except KeyError: # pylint: disable=protected-access try: paramDef = reactors.Core.paramCollectionType.pDefs[param] except KeyError: # Dead parameter? runLog.warning( "Reactor/Core parameter `{}` was unrecognized and is being " "ignored.".format(param) ) all_vals = [] for timestep in timesteps: value = self._get_1d_dataset("{}/reactors/{}".format(timestep, param)) if value is not None: # unpack if there's a value (like if there's just one reactor) all_vals.append(value[0]) else: # Go to the paramDef's default in case # it's not None (e.g. time default is 0.0) all_vals.append(paramDef.default) return all_vals
def removeInterface(self, interface=None, interfaceName=None): """ Remove a single interface from the interface stack. Parameters ---------- interface : Interface, optional An actual interface object to remove. interfaceName : str, optional The name of the interface to remove. Returns ------- success : boolean True if the interface was removed False if it was not (because it wasn't there to be removed) """ if interfaceName: interface = self.getInterface(interfaceName) if interface and interface in self.interfaces: self.interfaces.remove(interface) interface.detachReactor() return True else: runLog.warning( "Cannot remove interface {0} because it is not in the interface stack." .format(interface)) return False
def _modifyUnrepresentedXSIDs(self, blockCollectionsByXsGroup): """ adjust the xsID of blocks in the groups that are not represented Try to just adjust the burnup group up to something that is represented (can happen to structure in AA when only AB, AC, AD still remain). """ for xsID in self._unrepresentedXSIDs: missingXsType, _missingBuGroup = xsID for otherXsID in self.representativeBlocks: # order gets closest BU repType, repBuGroup = otherXsID if repType == missingXsType: nonRepBlocks = blockCollectionsByXsGroup.get(xsID) if nonRepBlocks: runLog.extra( "Changing XSID of {0} blocks from {1} to {2}" "".format(len(nonRepBlocks), xsID, otherXsID) ) for b in nonRepBlocks: b.p.buGroup = repBuGroup break else: runLog.warning( "No representative blocks with XS type {0} exist in the core. " "These XS cannot be generated and must exist in the working " "directory or the run will fail.".format(xsID) )
def density3(self, Tk=None, Tc=None): """ Return density that preserves mass when thermally expanded in 3D. Notes ----- Since refDens is specified at the material-dep reference case, we don't need to specify the reference temperature. It is already consistent with linearExpansion Percent. - p*(dp/p(T) + 1) =p*( p + dp(T) )/p = p + dp(T) = p(T) - dp/p = (1-(1 + dL/L)**3)/(1 + dL/L)**3 """ Tk = getTk(Tc, Tk) dLL = self.linearExpansionPercent(Tk=Tk) refD = self.p.refDens if refD is None: runLog.warning( "{0} has no reference density".format(self), single=True, label="No refD " + self.getName(), ) return None f = (1.0 + dLL / 100.0)**3 dRhoOverRho = (1.0 - f) / f return refD * (dRhoOverRho + 1)
def _mergeComponents(b): solventNamesToMergeInto = set(c.p.mergeWith for c in b if c.p.mergeWith) if solventNamesToMergeInto: runLog.warning( "Component(s) {} in block {} has merged components inside it. The merge was valid at hot " "temperature, but the merged component only has the basic thermal expansion factors " "of the component(s) merged into. Expansion properties or dimensions of non hot " "temperature may not be representative of how the original components would have acted had " "they not been merged. It is recommended that merging happen right before " "a physics calculation using a block converter to avoid this." "".format(solventNamesToMergeInto, b.name), single=True, ) for solventName in solventNamesToMergeInto: soluteNames = [] for c in b: if c.p.mergeWith == solventName: soluteNames.append(c.name) converter = blockConverters.MultipleComponentMerger( b, soluteNames, solventName) b = converter.convert() return b
def _updateMassFractionsFromParamValues(reactor, blockParamNames, blockList): """ Set the block densities based on the already-updated n block-params. The DB reads in params that represent the nuclide densities on each block, but they cannot be applied to the component until the temperatures are updated. """ runLog.info("Updating component mass fractions from DB params") # Set all number densities on a block at a time so we don't have to compute volume fractions N times. allNucNamesInProblem = set(reactor.blueprints.allNuclidesInProblem) allNucBasesInProblem = { nuclideBases.byName[nucName] for nucName in allNucNamesInProblem } nucBasesInBlockParams = { nb for nb in allNucBasesInProblem if nb.getDatabaseName() in blockParamNames } if settings.getMasterCs()["zeroOutNuclidesNotInDB"]: nucBasesNotInBlockParams = allNucBasesInProblem - nucBasesInBlockParams zeroOut = { nb.getDatabaseName(): 0.0 for nb in nucBasesNotInBlockParams } if zeroOut: runLog.important( "Zeroing out {0} because they are not in the db.".format( nucBasesNotInBlockParams)) else: zeroOut = {} for b in blockList: ndens = { nuc.name: b.p[nuc.getDatabaseName()] for nuc in nucBasesInBlockParams } ndens.update(zeroOut) # apply all non-zero number densities no matter what. # zero it out if it was already there and is now set to zero. ndens = { name: val for name, val in ndens.items() if val or name in set(b.getNuclides()) } b.setNumberDensities(ndens) allNucsNamesInDB = { nuclideBases.nucNameFromDBName(paramName) for paramName in blockParamNames.intersection( nuclideBases.byDBName.keys()) } nucNamesInDataBaseButNotProblem = allNucsNamesInDB - allNucNamesInProblem for nucName in nucNamesInDataBaseButNotProblem: runLog.warning( "Nuclide {0} exists in the database but not the problem. It is being ignored" "".format(nucName))
def density(self, Tk=None, Tc=None): """Return density that preserves mass when thermally expanded in 2D. Warning ------- This density will not agree with the component density since this method only expands in 2 dimensions. The component has been manually expanded axially with the manually entered block hot height. The density returned by this should be a factor of 1 + dLL higher than the density on the component. density3 should be in agreement at both cold and hot temperatures as long as the block height is correct for the specified temperature. In the case of Fluids, density and density3 are the same as density is not driven by linear expansion, but rather an exilicit density function dependent on Temperature. linearExpansionPercent is zero for a fluid. See Also -------- armi.materials.density3: component density should be in agreement with this density armi.reactor.blueprints._applyBlockDesign: 2D expansion and axial density reduction occurs here. """ Tk = getTk(Tc, Tk) dLL = self.linearExpansionPercent(Tk=Tk) if self.p.refDens is None: runLog.warning( "{0} has no reference density".format(self), single=True, label="No refD " + self.getName(), ) self.p.refDens = 0.0 f = (1.0 + dLL / 100.0)**2 # dRhoOverRho = (1.0 - f)/f # rho = rho + dRho = (1 + dRho/rho) * rho return self.p.refDens / f # g/cm^3
def _check(self): """ Check current directory for valid output files. Also check convergence flag and warn if not converged. Notes ----- Some users may prefer an error if the file is not converged, and this could become a user option if desired. Even unconverged files are useful in some analyses. Unfortunately, DIF3D 11.0 does not write the ``ITPS`` item in the standard RZFLUX file for convergence (this can be verified clearly by looking at ``wrzflx.f`` in the source code. Thus the convergence check must come from elsewhere. It does write this information to the code-specific ``DIF3D`` file, which is both and input and an output to a DIF3D run. """ if not os.path.exists("DIF3D"): raise RuntimeError( "No valid DIF3D output found. Check DIF3D stdout for errors.") self._dif3dData = dif3dFile.Dif3dStream.readBinary(dif3dFile.DIF3D) runLog.info("Found DIF3D output with:\n\t" + "\n\t".join(self._dif3dData.makeSummary())) if self._dif3dData.convergence != dif3dFile.Convergence.CONVERGED: runLog.warning( f"DIF3D run did not converge. Convergence state is {self._dif3dData.convergence}." )
def buGroup(self, buGroupChar): # pylint: disable=method-hidden if isinstance(buGroupChar, (int, float)): intValue = int(buGroupChar) runLog.warning( "Attempting to set `b.p.buGroup` to int value ({}). Possibly loading from old database".format( buGroupChar ), single=True, label="bu group as int " + str(intValue), ) self.buGroupNum = intValue return elif not isinstance(buGroupChar, six.string_types): raise Exception( "Wrong type for buGroupChar {}: {}".format( buGroupChar, type(buGroupChar) ) ) buGroupNum = ord(buGroupChar) - ASCII_LETTER_A self._p_buGroup = ( buGroupChar # pylint: disable=attribute-defined-outside-init ) self._p_buGroupNum = ( buGroupNum # pylint: disable=attribute-defined-outside-init ) buGroupNumDef = parameters.ALL_DEFINITIONS["buGroupNum"] buGroupNumDef.assigned = parameters.SINCE_ANYTHING
def interactEveryNode(self, cycle, node): if self.cs["zoneFlowSummary"]: reportingUtils.summarizeZones(self.r.core, self.cs) if self.cs["assemPowSummary"]: reportingUtils.summarizePower(self.r.core) self.r.core.calcBlockMaxes() reportingUtils.summarizePowerPeaking(self.r.core) runLog.important("Cycle {}, node {} Summary: ".format(cycle, node)) runLog.important( " time= {0:8.2f} years, keff= {1:.12f} maxPD= {2:-8.2f} MW/m^2, " "maxBuI= {3:-8.4f} maxBuF= {4:8.4f}".format( self.r.p.time, self.r.core.p.keff, self.r.core.p.maxPD, self.r.core.p.maxBuI, self.r.core.p.maxBuF, )) if self.cs["plots"]: adjoint = self.cs["neutronicsType"] == neutronics.ADJREAL_CALC figName = (self.cs.caseTitle + "_{0}_{1}".format(cycle, node) + ".mgFlux." + self.cs["outputFileExtension"]) if self.r.core.getFirstBlock(Flags.FUEL).p.mgFlux is not None: from armi.reactor import blocks blocks.Block.plotFlux(self.r.core, fName=figName, peak=True, adjoint=adjoint) else: runLog.warning("No mgFlux to plot in reports")
def __new__(mcs, name, bases, attrs): # pylint: disable=no-member pm = armi.getPluginManager() if pm is None: runLog.warning( "Blueprints were instantiated before the framework was " "configured with plugins. Blueprints cannot be imported before " "ARMI has been configured." ) else: pluginSections = pm.hook.defineBlueprintsSections() for plug in pluginSections: for (attrName, section, resolver) in plug: assert isinstance(section, yamlize.Attribute) if attrName in attrs: raise plugins.PluginError( "There is already a section called '{}' in the reactor " "blueprints".format(attrName) ) attrs[attrName] = section attrs["_resolveFunctions"].append(resolver) newType = yamlize.objects.ObjectType.__new__(mcs, name, bases, attrs) return newType
def __getitem__(self, name): try: return self.data[name] except KeyError: runLog.warning( "Given name {} not present in report group {}".format(name, self.title) )
def density(self, Tk=None, Tc=None): """Calculate the mass density in g/cc of U-Zr alloy with various percents""" zrFrac = self.p.zrFrac thFrac = self.p.thFrac uFrac = 1 - zrFrac - thFrac if zrFrac is None: runLog.warning( "Cannot get UZr density without Zr%. Set ZIRC massFrac", single=True, label="no zrfrac", ) return None Tk = getTk(Tc, Tk) u0 = 19.1 zr0 = 6.52 th0 = 11.68 # use vegard's law to mix densities by weight fraction at 50C # uzr0 = 1.0/(zrFrac/zr0+(1-zrFrac)/u0) uThZr0 = 1.0 / (zrFrac / zr0 + (uFrac) / u0 + thFrac / th0) # runLog.debug('Cold density: {0} g/cc'.format(uzr0)) dLL = self.linearExpansionPercent(Tk=Tk) f = (1 + dLL / 100.0) ** 2 density = uThZr0 * (1.0 + (1.0 - f) / f) return density
def updateDeltaDPApastIncubation(self, totalDPA, deltaDPA): r""" If a material has passed its incubation dose, this method updates deltaDPA. The concern here is when a step in DPA crosses the incubation threshold, the amount of DPA input into a calculation is more than is actually contributing to deformation. Parameters ---------- totalDPA : float Total DPA accumulated in the material. deltaDPA : float Change in DPA over a time step. Returns ------- deltaDPA past the incubation dose of the material. """ if not self.modelConst["Rincu"]: msg = "Material missing incubation dose" runLog.warning(msg, single=True, label="Missing incubation dose") elif (totalDPA > self.modelConst["Rincu"]) and ( (totalDPA - self.modelConst["Rincu"]) < deltaDPA): return totalDPA - self.modelConst["Rincu"] else: return deltaDPA
def rename(_cs, name, value): if name in RENAMES: runLog.warning("Invalid setting {} found. Renaming to {}.".format( name, RENAMES[name])) name = RENAMES[name] return {name: value}
def getNameFromMC2(mc2LibLabel=None, mc2Label=None): r""" maps an MC2 label to an ARMI label Tries to maintain some backwards compatibility with old ISOTXS libs with B-10AA, CARBAA, etc. Parameters ---------- mc2LibLabel : str THe library ID on the MC**2 binary file (e.g. U-235S) mc2Label : str The mc**2 prefix to look up (e.g. U235) """ nuclide = None if mc2LibLabel: nuclide = nuclideBases.byMccId[mc2LibLabel] else: nuclide = nuclideBases.byLabel[mc2Label] return nuclide.name # TODO: Not sure if this is the desired behaviour. # if not a warning, this fails on checking the LFP components to see if they're already # in the problem. runLog.warning( "Nuclide with mc2LibName/mc2Label {}/{} had no corresponding ARMI nuclide Name" "".format(mc2LibLabel, mc2Label) ) return None
def loadState(self, cycle, timeNode, timeStepName="", fileName=None, updateMassFractions=None): """ Convenience method reroute to the database interface state reload method See also -------- armi.bookeeping.db.loadOperator: A method for loading an operator given a database. loadOperator does not require an operator prior to loading the state of the reactor. loadState does, and therefore armi.init must be called which requires access to the blueprints, settings, and geometry files. These files are stored implicitly on the database, so loadOperator creates the reactor first, and then attaches it to the operator. loadState should be used if you are in the middle of an ARMI calculation and need load a different time step. If you are loading from a fresh ARMI session, either method is sufficient if you have access to all the input files. """ dbi = self.getInterface("database") if not dbi: raise RuntimeError( "Cannot load from snapshot without a database interface") if updateMassFractions is not None: runLog.warning( "deprecated: updateMassFractions is no longer a valid option for loadState" ) dbi.loadState(cycle, timeNode, timeStepName, fileName)
def apply(self, material): """ Apply specific isotopic compositions to a component. Generically, materials have composition-dependent bulk properties such as mass density. Note that this operation does not update these material properties. Use with care. Parameters ---------- material : Material An ARMI Material instance. """ material.p.massFrac = dict(self.massFracs) if self.density is not None: if not isinstance(material, materials.Custom): runLog.warning( "You specified a custom mass density on `{}` with custom isotopics `{}`. " "This has no effect; you can only set this on `Custom` " "materials. Continuing to use {} mass density.".format( material, self.name, material ) ) return # specifically, non-Custom materials only use refDensity and dLL, .p.density has no effect material.p.density = self.density
def copyOrWarn(fileDescription, sourcePath, destinationPath): """Copy a file, or warn if the file doesn't exist. Parameters ---------- fileDescription : str a description of the file and/or operation being performed. sourcePath : str Path of the file to be copied. destinationPath : str Path for the copied file. """ try: shutil.copy(sourcePath, destinationPath) runLog.debug( "Copied {}: {} -> {}".format(fileDescription, sourcePath, destinationPath) ) except Exception as e: runLog.warning( "Could not copy {} from {} to {}\nError was: {}".format( fileDescription, sourcePath, destinationPath, e ) )
def getRingZoneRings(self): """ Get rings in each ring zone as a list of lists. Returns ------- ringZones : list List of lists. Each entry is the ring numbers in a ring zone. If there are no ring zones defined, returns a list of all rings. """ core = self.core if not self.cs["ringZones"]: # no ring zones defined. Return all rings. return [range(1, core.getNumRings() + 1)] # ringZones are upper limits, defining ring zones from the center. so if they're # [3, 5, 8, 90] then the ring zones are from 1 to 3, 4 to 5, 6 to 8, etc. # AKA, the upper bound is included in that particular zone. # check validity of ringZones. Increasing order and integers. ring0 = 0 for i, ring in enumerate(self.cs["ringZones"]): if ring <= ring0 or not isinstance(ring, int): runLog.warning( "ring zones {0} are invalid. Must be integers, increasing in order. " "Can not return ring zone rings.".format(self.cs["ringZones"]) ) return ring0 = ring if i == len(self.cs["ringZones"]) - 1: # this is the final ring zone if ring < (core.getNumRings() + 1): finalRing = core.getNumRings() else: finalRing = None # modify the ringZones to definitely include all assemblies if finalRing: runLog.debug( "Modifying final ring zone definition to include all assemblies. New max: {0}".format( finalRing ), single=True, label="Modified ring zone definition", ) self.cs["ringZones"][-1] = finalRing # build the ringZone list ring0 = 0 ringZones = [] for upperRing in self.cs["ringZones"]: ringsInThisZone = range( ring0 + 1, upperRing + 1 ) # the rings in this ring zone as defined above. ringZones.append(ringsInThisZone) ring0 = upperRing return ringZones