diff --git a/armi/reactor/blueprints/gridBlueprint.py b/armi/reactor/blueprints/gridBlueprint.py index 48d524150..391011a4f 100644 --- a/armi/reactor/blueprints/gridBlueprint.py +++ b/armi/reactor/blueprints/gridBlueprint.py @@ -105,7 +105,7 @@ """ import copy import itertools -from typing import Sequence, Optional +from typing import Sequence, Optional, Tuple import numpy import yamlize @@ -293,16 +293,7 @@ def _constructSpatialGrid(self): # Note that the "through center" symmetry check cannot be performed when # the grid contents has not been provided (i.e., None or empty). if self.gridContents: - nx = ( - max(key[0] for key in self.gridContents) - - min(key[0] for key in self.gridContents) - + 1 - ) - ny = ( - max(key[1] for key in self.gridContents) - - min(key[1] for key in self.gridContents) - + 1 - ) + nx, ny = _getGridSize(self.gridContents.keys()) if nx == ny and nx % 2 == 1: symmetry.isThroughCenterAssembly = True @@ -393,16 +384,33 @@ def _readGridContentsLattice(self): This update the gridContents attribute, which is a dict mapping grid i,j,k indices to textual specifiers (e.g. ``IC``)) """ + symmetry = geometry.SymmetryType.fromStr(self.symmetry) + geom = geometry.GeomType.fromStr(self.geom) latticeCls = asciimaps.asciiMapFromGeomAndSym(self.geom, self.symmetry) asciimap = latticeCls() asciimap.readAscii(self.latticeMap) self.gridContents = dict() + iOffset = 0 + jOffset = 0 + if ( + geom == geometry.GeomType.CARTESIAN + and symmetry.domain == geometry.DomainType.FULL_CORE + ): + # asciimaps is not smart about where the center should be, so we need to + # offset appropriately to get (0,0) in the middle + nx, ny = _getGridSize(asciimap.keys()) + + # turns out this works great for even and odd cases. love it when integer + # math works in your favor + iOffset = int(-nx / 2) + jOffset = int(-ny / 2) + for (i, j), spec in asciimap.items(): if spec == "-": # skip placeholders continue - self.gridContents[i, j] = spec + self.gridContents[i + iOffset, j + jOffset] = spec def getLocators(self, spatialGrid: grids.Grid, latticeIDs: list): """ @@ -454,3 +462,18 @@ def _isMonotonicUnique(l: Sequence[float]) -> bool: return False return True + + +def _getGridSize(idx) -> Tuple[int, int]: + """ + Return the number of spaces between the min and max of a collection of (int, int) + tuples, inclusive. + + This essentially returns the number of grid locations along the i, and j dimesions, + given the (i,j) indices of each occupied location. This is useful for determining + certain grid offset behavior. + """ + nx = max(key[0] for key in idx) - min(key[0] for key in idx) + 1 + ny = max(key[1] for key in idx) - min(key[1] for key in idx) + 1 + + return nx, ny diff --git a/armi/reactor/blueprints/tests/test_gridBlueprints.py b/armi/reactor/blueprints/tests/test_gridBlueprints.py index 04c968010..dca0554f3 100644 --- a/armi/reactor/blueprints/tests/test_gridBlueprints.py +++ b/armi/reactor/blueprints/tests/test_gridBlueprints.py @@ -57,6 +57,27 @@ 2 1 3 1 2 2 3 1 1 2 2 2 2 2 2 + +sfp quarter: + geom: cartesian + symmetry: quarter through center assembly + lattice map: | + 2 2 2 2 2 + 2 1 1 1 2 + 2 1 3 1 2 + 2 3 1 1 2 + 2 2 2 2 2 + +sfp even: + geom: cartesian + symmetry: full + lattice map: | + 1 2 2 2 2 2 + 1 2 1 1 1 2 + 1 2 1 4 1 2 + 1 2 2 1 1 2 + 1 2 2 2 2 2 + 1 1 1 1 1 1 """ RZT_BLUEPRINT = """ @@ -240,9 +261,30 @@ def test_simple_read(self): _grid = gridDesign.construct() self.assertEqual(gridDesign.gridContents[0, -8], "6") + # Cartesian full, odd gridDesign2 = self.grids["sfp"] _grid = gridDesign2.construct() - self.assertEqual(gridDesign2.gridContents[1, 1], "3") + self.assertEqual(gridDesign2.gridContents[1, 1], "1") + self.assertEqual(gridDesign2.gridContents[0, 0], "3") + self.assertEqual(gridDesign2.gridContents[-1, -1], "3") + + # Cartesian quarter, odd + gridDesign3 = self.grids["sfp quarter"] + _grid = gridDesign3.construct() + self.assertEqual(gridDesign3.gridContents[0, 0], "2") + self.assertEqual(gridDesign3.gridContents[1, 1], "3") + self.assertEqual(gridDesign3.gridContents[2, 2], "3") + self.assertEqual(gridDesign3.gridContents[3, 3], "1") + + # Cartesian full, even/odd hybrid + gridDesign4 = self.grids["sfp even"] + _grid = gridDesign4.construct() + self.assertEqual(gridDesign4.gridContents[0, 0], "4") + self.assertEqual(gridDesign4.gridContents[-1, -1], "2") + self.assertEqual(gridDesign4.gridContents[2, 2], "2") + self.assertEqual(gridDesign4.gridContents[-3, -3], "1") + with self.assertRaises(KeyError): + self.assertEqual(gridDesign4.gridContents[-4, -3], "1") class TestRZTGridBlueprint(unittest.TestCase): diff --git a/armi/reactor/geometry.py b/armi/reactor/geometry.py index 5fa9cc498..2b71659b8 100644 --- a/armi/reactor/geometry.py +++ b/armi/reactor/geometry.py @@ -379,7 +379,7 @@ def fromStr(cls, symmetryString: str) -> "SymmetryType": symmetryString, trimmedString ) errorMsg += ", ".join( - [f"{sym}" for sym in self.createValidSymmetryStrings()] + [f"{sym}" for sym in cls.createValidSymmetryStrings()] ) raise ValueError(errorMsg) return cls(domain, boundary, isThroughCenter) diff --git a/armi/utils/asciimaps.py b/armi/utils/asciimaps.py index 37e44839f..4f8315ab8 100644 --- a/armi/utils/asciimaps.py +++ b/armi/utils/asciimaps.py @@ -154,6 +154,8 @@ def _updateDimensionsFromData(self): _updateDimensionsFromAsciiLines : used when reading info from ascii lines """ self._ijMax = max(sum(key) for key in self.asciiLabelByIndices) + self._asciiMaxCol = max(key[0] for key in self.asciiLabelByIndices) + 1 + self._asciiMaxLine = max(key[1] for key in self.asciiLabelByIndices) + 1 @staticmethod def fromReactor(reactor): @@ -245,6 +247,9 @@ def _makeOffsets(self): def items(self): return self.asciiLabelByIndices.items() + def keys(self): + return self.asciiLabelByIndices.keys() + class AsciiMapCartesian(AsciiMap): """ @@ -263,6 +268,17 @@ def _asciiLinesToIndices(self): ij = self._getIJFromColRow(ci, li) self.asciiLabelByIndices[ij] = asciiLabel + def _updateDimensionsFromData(self): + AsciiMap._updateDimensionsFromData(self) + iMin = min(key[0] for key in self.asciiLabelByIndices) + jMin = min(key[1] for key in self.asciiLabelByIndices) + + if iMin > 0 or jMin > 0: + raise ValueError( + "Asciimaps only supports sets of indices that " + "start at less than or equal to zero, got {}, {}".format(iMin, jMin) + ) + def _getIJFromColRow(self, columNum, lineNum): return columNum, lineNum diff --git a/armi/utils/gridEditor.py b/armi/utils/gridEditor.py index a292bf0c5..388cb0c6c 100644 --- a/armi/utils/gridEditor.py +++ b/armi/utils/gridEditor.py @@ -1487,11 +1487,22 @@ def save(self, stream=None, full=False): aMap = asciimaps.asciiMapFromGeomAndSym( self.grid.geomType, self.grid.symmetry )() - aMap.asciiLabelByIndices = gridDesign.gridContents + # asciimaps can't handle negative indices, so we bump everything + # forward if needed + offset = ( + min(key[0] for key in gridDesign.gridContents.keys()), + min(key[1] for key in gridDesign.gridContents.keys()), + ) + aMap.asciiLabelByIndices = { + (key[0] - offset[0], key[1] - offset[1]): val + for key, val in gridDesign.gridContents.items() + } aMap.gridContentsToAscii() - except: + except Exception as e: runLog.warning( - "Cannot write geometry with asciimap. Defaulting to dict." + "Cannot write geometry with asciimap. Defaulting to dict. Issue: {}".format( + e + ) ) aMap = None diff --git a/armi/utils/tests/test_asciimaps.py b/armi/utils/tests/test_asciimaps.py index eb0e2cb04..894c7a6c5 100644 --- a/armi/utils/tests/test_asciimaps.py +++ b/armi/utils/tests/test_asciimaps.py @@ -18,6 +18,7 @@ CARTESIAN_MAP = """2 2 2 2 2 +2 2 2 2 2 2 1 1 1 2 2 1 3 1 2 2 3 1 1 2 @@ -178,12 +179,6 @@ def test_cartesian(self): stream.seek(0) asciimap.readAscii(stream.read()) - with io.StringIO() as stream: - asciimap.writeAscii(stream) - stream.seek(0) - output = stream.read() - self.assertEqual(output, CARTESIAN_MAP) - self.assertEqual(asciimap[0, 0], "2") self.assertEqual(asciimap[1, 1], "3") self.assertEqual(asciimap[2, 2], "3") @@ -191,6 +186,15 @@ def test_cartesian(self): with self.assertRaises(KeyError): asciimap[5, 2] # pylint: disable=pointless-statement + outMap = asciimaps.AsciiMapCartesian() + outMap.asciiLabelByIndices = asciimap.asciiLabelByIndices + outMap.gridContentsToAscii() + with io.StringIO() as stream: + outMap.writeAscii(stream) + stream.seek(0) + output = stream.read() + self.assertEqual(output, CARTESIAN_MAP) + def test_hexThird(self): """Read 1/3 core flats-up maps.""" asciimap = asciimaps.AsciiMapHexThirdFlatsUp()