Skip to content

Commit

Permalink
Overhauling ARMI Zones system (#943)
Browse files Browse the repository at this point in the history
Overhaul tooling in the reactor.zones module and remove application-specific zoning logic, which may be implemented as desired in downstream Interfaces.

This satisfies the main intent of #811. Enhancements may be added in the future.

Related issues:
#563
#800
  • Loading branch information
joshuavictorchen authored Oct 26, 2022
1 parent 39766e6 commit 47658eb
Show file tree
Hide file tree
Showing 20 changed files with 634 additions and 1,114 deletions.
2 changes: 0 additions & 2 deletions armi/bookkeeping/report/reportInterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@ def interactBOL(self):
runLog.info(report.ALL[report.RUN_META])

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)

Expand Down
86 changes: 0 additions & 86 deletions armi/bookkeeping/report/reportingUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,92 +594,6 @@ def summarizePower(core):
)


def summarizeZones(core, cs):
r"""Summarizes the active zone and other zone.
Parameters
----------
core: armi.reactor.reactors.Core
cs: armi.settings.caseSettings.Settings
"""

totPow = core.getTotalBlockParam("power")
if not totPow:
# protect against divide-by-zero
return
powList = [] # eventually will be a sorted list of power
for a in core.getAssemblies():
if a.hasFlags(Flags.FUEL):
aPow = a.calcTotalParam("power")
powList.append((aPow / totPow, a))
powList.sort() # lowest power assems first.

# now build "low power region" and high power region.
# at BOL (cycle 0) just take all feeds as low power. (why not just use power fractions?,
# oh, because if you do that, a few igniters will make up the 1st 5% of the power.)
totFrac = 0.0
lowPow = []
highPow = []
pFracList = [] # list of power fractions in the high power zone.

for pFrac, a in powList:
if core.r.p.cycle > 0 and totFrac <= cs["lowPowerRegionFraction"]:
lowPow.append(a)
elif (
core.r.p.cycle == 0
and a.hasFlags(Flags.FEED | Flags.FUEL)
and a.getMaxUraniumMassEnrich() > 0.01
):
lowPow.append(a)
else:
highPow.append(a)
pFracList.append(pFrac)
totFrac += pFrac

if not pFracList:
# sometimes this is empty (why?), which causes an error below when
# calling max(pFracList)
return

if abs(totFrac - 1.0) < 1e-4:
runLog.warning("total power fraction not equal to sum of assembly powers.")

peak = max(pFracList) # highest power assembly
peakIndex = pFracList.index(peak)
peakAssem = highPow[peakIndex]

avgPFrac = sum(pFracList) / len(pFracList) # true mean power fraction
# the closest-to-average pfrac in the list
_avgAssemPFrac, avgIndex = findClosest(pFracList, avgPFrac, indx=True)
avgAssem = highPow[avgIndex] # the actual average assembly

# ok, now need counts, and peak and avg. flow and power in high power region.
mult = core.powerMultiplier

summary = "Zone Summary For Safety Analysis cycle {0}\n".format(core.r.p.cycle)
summary += " Assemblies in high-power zone: {0}\n".format(len(highPow) * mult)
summary += " Assemblies in low-power zone: {0}\n".format(len(lowPow) * mult)
summary += " " * 13 + "{0:15s} {1:15s} {2:15s} {3:15s}\n".format(
"Location", "Power (W)", "Flow (kg/s)", "Pu frac"
)

for lab, a in [("Peak", peakAssem), ("Average", avgAssem)]:
flow = a.p.THmassFlowRate
if not flow:
runLog.warning("No TH data. Reporting zero flow.")
# no TH for some reason
flow = 0.0
puFrac = a.getPuFrac()
ring, pos = a.spatialLocator.getRingPos()
summary += (
" {0:10s} ({ring:02d}, {pos:02d}) {1:15.6E} {2:15.6E} {pu:15.6E}\n".format(
lab, a.calcTotalParam("power"), flow, ring=ring, pos=pos, pu=puFrac
)
)
runLog.important(summary)


def makeCoreDesignReport(core, cs):
r"""Builds report to summarize core design inputs
Expand Down
6 changes: 0 additions & 6 deletions armi/bookkeeping/report/tests/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
summarizePinDesign,
summarizePower,
summarizePowerPeaking,
summarizeZones,
writeAssemblyMassSummary,
writeCycleSummary,
)
Expand Down Expand Up @@ -123,11 +122,6 @@ def test_reactorSpecificReporting(self):
self.assertIn("End of Cycle", mock._outputStream)
mock._outputStream = ""

# this report won't do much for the test reactor - improve test reactor
summarizeZones(r.core, o.cs)
self.assertEqual(len(mock._outputStream), 0)
mock._outputStream = ""

# this report won't do much for the test reactor - improve test reactor
makeBlockDesignReport(r)
self.assertEqual(len(mock._outputStream), 0)
Expand Down
2 changes: 1 addition & 1 deletion armi/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@
Metadata describing an ARMI distribution.
"""

__version__ = "0.2.4"
__version__ = "0.2.5"
10 changes: 0 additions & 10 deletions armi/operators/settingsValidation.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,16 +363,6 @@ def _inspectSettings(self):
lambda: self._assignCS("outputFileExtension", "png"),
)

self.addQuery(
lambda: (
self.cs[globalSettings.CONF_ZONING_STRATEGY] == "manual"
and not self.cs["zoneDefinitions"]
),
"`manual` zoningStrategy requires that `zoneDefinitions` setting be defined. Run will have "
"no zones.",
"",
self.NO_ACTION,
)
self.addQuery(
lambda: (
(
Expand Down
1 change: 1 addition & 0 deletions armi/physics/fuelCycle/fuelHandlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,7 @@ def getParamWithBlockLevelMax(a, paramName):
# this assembly is in the excluded location list. skip it.
continue

# only continue of the Assembly is in a Zone
if zoneList:
found = False # guilty until proven innocent
for zone in zoneList:
Expand Down
9 changes: 8 additions & 1 deletion armi/reactor/converters/geometryConverters.py
Original file line number Diff line number Diff line change
Expand Up @@ -1296,16 +1296,23 @@ def convert(self, r):
for a in self._sourceReactor.core.getAssemblies():
# make extras and add them too. since the input is assumed to be 1/3 core.
otherLocs = grid.getSymmetricEquivalents(a.spatialLocator.indices)
thisZone = (
self._sourceReactor.core.zones.findZoneItIsIn(a)
if len(self._sourceReactor.core.zones) > 0
else None
)
angle = 2 * math.pi / (len(otherLocs) + 1)
count = 1
for i, j in otherLocs:
newAssem = copy.deepcopy(a)
newAssem.makeUnique()
newAssem.rotate(count * angle)
count = count + 1
count += 1
self._sourceReactor.core.add(
newAssem, self._sourceReactor.core.spatialGrid[i, j, 0]
)
if thisZone:
thisZone.addLoc(newAssem.getLocation())
self._newAssembliesAdded.append(newAssem)

if a.getLocation() == "001-001":
Expand Down
8 changes: 4 additions & 4 deletions armi/reactor/grids.py
Original file line number Diff line number Diff line change
Expand Up @@ -1704,21 +1704,21 @@ def generateSortedHexLocationList(self, nLocs):
nLocs = int(nLocs) # need to make this an integer

# next, generate a list of locations and corresponding distances
locList = []
locs = []
for ring in range(1, hexagon.numRingsToHoldNumCells(nLocs) + 1):
positions = self.getPositionsInRing(ring)
for position in range(1, positions + 1):
i, j = self.getIndicesFromRingAndPos(ring, position)
locList.append(self[(i, j, 0)])
locs.append(self[(i, j, 0)])
# round to avoid differences due to floating point math
locList.sort(
locs.sort(
key=lambda loc: (
round(numpy.linalg.norm(loc.getGlobalCoordinates()), 6),
loc.i, # loc.i=ring
loc.j,
)
) # loc.j= pos
return locList[:nLocs]
return locs[:nLocs]

# TODO: this is only used by testing and another method that just needs the count of assemblies
# in a ring, not the actual positions
Expand Down
60 changes: 45 additions & 15 deletions armi/reactor/reactors.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ def __init__(self, name):
self.locParams = {} # location-based parameters
# overridden in case.py to include pre-reactor time.
self.timeOfStart = time.time()
self.zones = None
self.zones = zones.Zones() # initialize with empty Zones object
# initialize the list that holds all shuffles
self.moveList = {}
self.scalarVals = {}
Expand Down Expand Up @@ -1019,15 +1019,14 @@ def getAssemblies(
includeAll : bool, optional
Will include ALL assemblies.
zones : str or iterable, optional
Only include assemblies that are in this zone/these zones
zones : iterable, optional
Only include assemblies that are in this these zones
Notes
-----
Attempts have been made to make this a generator but there were some Cython
incompatibilities that we could not get around and so we are sticking with a
list.
"""
if includeAll:
includeBolAssems = includeSFP = True
Expand Down Expand Up @@ -1211,9 +1210,6 @@ def regenAssemblyLists(self):
"""
self._getAssembliesByName()
self._genBlocksByName()
runLog.important("Regenerating Core Zones")
# TODO: this call is questionable... the cs should correspond to analysis
self.buildZones(settings.getMasterCs())
self._genChildByLocationLookupTable()

def getAllXsSuffixes(self):
Expand Down Expand Up @@ -1762,11 +1758,6 @@ def getAssembliesOnSymmetryLine(self, symmetryLineID):
assembliesOnLine.sort(key=lambda a: a.spatialLocator.getRingPos())
return assembliesOnLine

def buildZones(self, cs):
"""Update the zones on the reactor."""
self.zones = zones.buildZones(self, cs)
self.zones = zones.splitZones(self, cs, self.zones)

def getCoreRadius(self):
"""Returns a radius that the core would fit into."""
return self.getNumRings(indexBased=True) * self.getFirstBlock().getPitch()
Expand Down Expand Up @@ -2313,15 +2304,54 @@ def processLoading(self, cs, dbLoad: bool = False):

self.stationaryBlockFlagsList = stationaryBlockFlags

# Perform initial zoning task
self.buildZones(cs)

self.p.maxAssemNum = self.getMaxParam("assemNum")

getPluginManagerOrFail().hook.onProcessCoreLoading(
core=self, cs=cs, dbLoad=dbLoad
)

def buildManualZones(self, cs):
"""
Build the Zones that are defined manually in the given CaseSettings file,
in the `zoneDefinitions` setting.
Parameters
----------
cs : CaseSettings
The standard ARMI settings object
Examples
--------
Manual zones will be defined in a special string format, e.g.:
zoneDefinitions:
- ring-1: 001-001
- ring-2: 002-001, 002-002
- ring-3: 003-001, 003-002, 003-003
Notes
-----
This function will just define the Zones it sees in the settings, it does
not do any validation against a Core object to ensure those manual zones
make sense.
"""
runLog.debug(
"Building Zones by manual definitions in `zoneDefinitions` setting"
)
stripper = lambda s: s.strip()
self.zones = zones.Zones()

# parse the special input string for zone definitions
for zoneString in cs["zoneDefinitions"]:
zoneName, zoneLocs = zoneString.split(":")
zoneLocs = zoneLocs.split(",")
zone = zones.Zone(zoneName.strip())
zone.addLocs(map(stripper, zoneLocs))
self.zones.addZone(zone)

if not len(self.zones):
runLog.debug("No manual zones defined in `zoneDefinitions` setting")

def _applyThermalExpansion(
self, assems: list, dbLoad: bool, referenceAssembly=None
):
Expand Down
26 changes: 26 additions & 0 deletions armi/reactor/tests/test_reactors.py
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,32 @@ def test_updateBlockBOLHeights_DBLoad(self):
for param in equalParameters:
self.assertAlmostEqual(oldBlockParameters[param][b], b.p[param])

def test_buildManualZones(self):
# define some manual zones in the settings
newSettings = {}
newSettings["zoneDefinitions"] = [
"ring-1: 001-001",
"ring-2: 002-001, 002-002",
"ring-3: 003-001, 003-002, 003-003",
]
cs = self.o.cs.modified(newSettings=newSettings)
self.r.core.buildManualZones(cs)

zonez = self.r.core.zones
self.assertEqual(len(list(zonez)), 3)
self.assertIn("002-001", zonez["ring-2"])
self.assertIn("003-002", zonez["ring-3"])

def test_buildManualZonesEmpty(self):
# ensure there are no zone definitions in the settings
newSettings = {}
newSettings["zoneDefinitions"] = []
cs = self.o.cs.modified(newSettings=newSettings)

# verify that buildZones behaves well when no zones are defined
self.r.core.buildManualZones(cs)
self.assertEqual(len(list(self.r.core.zones)), 0)


class CartesianReactorTests(ReactorTests):
def setUp(self):
Expand Down
Loading

0 comments on commit 47658eb

Please sign in to comment.