From cacafdb8ddd3da6120c3ea4f1828e12af2e6bb64 Mon Sep 17 00:00:00 2001 From: jstilley Date: Fri, 9 Feb 2024 10:00:24 -0800 Subject: [PATCH 01/15] Placeholder - tests working --- armi/utils/asciimaps.py | 76 ++++++++++++++++++++---------- armi/utils/tests/test_asciimaps.py | 69 +++++++++++++++++++++------ 2 files changed, 106 insertions(+), 39 deletions(-) diff --git a/armi/utils/asciimaps.py b/armi/utils/asciimaps.py index 127dc6afed..5cf507ad90 100644 --- a/armi/utils/asciimaps.py +++ b/armi/utils/asciimaps.py @@ -40,7 +40,6 @@ but in other geometries (like hex), it is a totally different coordinate system. - See Also -------- armi.reactor.grids : More powerful, nestable lattices with specific dimensions @@ -97,8 +96,13 @@ def __init__(self): def writeAscii(self, stream): """Write out the ascii representation.""" + stream.write(self.__str__()) + + def __str__(self): + """TODO JOHN""" if not self.asciiLines: raise ValueError("Cannot write ASCII map before ASCII lines are processed") + if len(self.asciiOffsets) != len(self.asciiLines): runLog.error(f"AsciiLines: {self.asciiLines}") runLog.error(f"Offsets: {self.asciiOffsets}") @@ -106,17 +110,26 @@ def writeAscii(self, stream): f"Inconsistent lines ({len(self.asciiLines)}) " f"and offsets ({len(self.asciiOffsets)})" ) + + txt = "" fmt = f"{{val:{len(self._placeholder)}s}}" for offset, line in zip(self.asciiOffsets, self.asciiLines): data = [fmt.format(val=v) for v in line] line = self._spacer * offset + self._spacer.join(data) + "\n" - stream.write(line) + txt += line + + return txt def readAscii(self, text): """ Read ascii representation from a stream. Update placeholder size according to largest thing read. + + Parameters + ---------- + text : str + Custom string that describes the ASCII map of the core. """ text = text.strip().splitlines() @@ -127,6 +140,7 @@ def readAscii(self, text): self.asciiLines.append(columns) if len(columns) > self._asciiMaxCol: self._asciiMaxCol = len(columns) + self._asciiMaxLine = li + 1 self._updateDimensionsFromAsciiLines() self._asciiLinesToIndices() @@ -185,34 +199,41 @@ def gridContentsToAscii(self): is universal. In some implementations, this operation is in a different method for efficiency. """ + print(f"TODO JOHN 1 _asciiMaxLine: {self._asciiMaxLine}") self._updateDimensionsFromData() + print(f"TODO JOHN 2 _asciiMaxLine: {self._asciiMaxLine}") self.asciiLines = [] + print("--------------------------------------------------") for lineNum in self._getLineNumsToWrite(): + print(lineNum) line = [] for colNum in range(self._asciiMaxCol): + print(f" {colNum}") ij = self._getIJFromColRow(colNum, lineNum) # convert to string and strip any whitespace in thing we're representing line.append( str(self.asciiLabelByIndices.get(ij, PLACEHOLDER)).replace(" ", "") ) self.asciiLines.append(line) + print("+++++++++++++++++++++++++++++++++++++++++++++++++++") # clean data noDataLinesYet = True # handle all-placeholder rows newLines = [] for line in self.asciiLines: + print(line) if re.search(f"^[{PLACEHOLDER}]+$", "".join(line)) and noDataLinesYet: continue noDataLinesYet = False newLine = self._removeTrailingPlaceholders(line) + print(f" {newLine}") if newLine: newLines.append(newLine) else: # if entire newline is wiped out, it's a full row of placeholders! # but oops this actually still won't work. Needs more work when # doing pure rows from data is made programmatically. - # newLines.append(line) raise ValueError( "Cannot write asciimaps with blank rows from pure data yet." ) @@ -308,9 +329,9 @@ class AsciiMapHexThirdFlatsUp(AsciiMap): """ Hex ascii map for 1/3 core flats-up map. - Indices start with (0,0) in the bottom left (origin). - i increments on the 30-degree ray - j increments on the 90-degree ray + - Indices start with (0,0) in the bottom left (origin). + - i increments on the 30-degree ray + - j increments on the 90-degree ray In all flats-up hex maps, i increments by 2*col for each col and j decrements by col from the base. @@ -319,7 +340,6 @@ class AsciiMapHexThirdFlatsUp(AsciiMap): there are 2 ascii lines for every j index (jaggedly). Lines are read from the bottom of the ascii map up in this case. - """ def _asciiLinesToIndices(self): @@ -396,7 +416,7 @@ def _makeOffsets(self): # renomalize the offsets to start at 0 minOffset = min(self.asciiOffsets) - for li, (_, offset) in enumerate(zip(self.asciiLines, self.asciiOffsets)): + for _li, (_, offset) in enumerate(zip(self.asciiLines, self.asciiOffsets)): newOffsets.append(offset - minOffset) self.asciiOffsets = newOffsets @@ -442,7 +462,14 @@ def _updateDimensionsFromData(self): # now that we understand how many corner positions are truncated, # we can fully determine the size of the ascii map self._asciiMaxCol = self._ijMax + 1 + print( + f"\n TODO JOHN 1/3 before: self._asciiMaxLine: {self._asciiMaxLine}" + ) + print( + f" self._ijMax: {self._ijMax}, self._asciiLinesOffCorner: {self._asciiLinesOffCorner}" + ) self._asciiMaxLine = self._ijMax * 2 + 1 - self._asciiLinesOffCorner + print(f" TODO JOHN 1/3 after: self._asciiMaxLine: {self._asciiMaxLine}\n") class AsciiMapHexFullFlatsUp(AsciiMapHexThirdFlatsUp): @@ -501,6 +528,7 @@ def _makeOffsets(self): """ # max lines required if corners were not cut off maxIJIndex = self._ijMax + print(f"TODO JOHN: maxIJIndex: {maxIJIndex}") self.asciiOffsets = [] # grab top left edge going down until corner where it lifts off edge. # Due to the placeholders these just oscillate @@ -508,7 +536,9 @@ def _makeOffsets(self): self.asciiOffsets.append((li - self._asciiLinesOffCorner) % 2) # going away from the left edge, the offsets increase linearly + print(f"TODO JOHN: len(self.asciiOffsets): {len(self.asciiOffsets)}") self.asciiOffsets.extend(range(maxIJIndex + 1)) + print(f"TODO JOHN: len(self.asciiOffsets): {len(self.asciiOffsets)}") # since we allow cut-off corners, we must truncate the offsets # number of items in last line indicates how many @@ -520,27 +550,28 @@ def _makeOffsets(self): def _updateDimensionsFromData(self): AsciiMapHexThirdFlatsUp._updateDimensionsFromData(self) self._asciiMaxCol = self._ijMax + 1 + print(f" TODO JOHN FULL before: self._asciiMaxLine: {self._asciiMaxLine}") + print( + f" self._ijMax: {self._ijMax}, self._asciiLinesOffCorner: {self._asciiLinesOffCorner}" + ) self._asciiMaxLine = self._ijMax * 4 + 1 - self._asciiLinesOffCorner * 2 + print(f" TODO JOHN FULL after: self._asciiMaxLine: {self._asciiMaxLine}") class AsciiMapHexFullTipsUp(AsciiMap): """ Full hex with tips up of the smaller cells. - I axis is pure horizontal here - J axis is 60 degrees up. (upper right corner) - - (0,0) is in the center of the hexagon. + - I axis is pure horizontal here + - J axis is 60 degrees up. (upper right corner) + - (0,0) is in the center of the hexagon. Frequently used for pins inside hex assemblies. - This does not currently support omitted positions on - the hexagonal corners. + This does not currently support omitted positions on the hexagonal corners. - In this geometry, the outline-defining _ijMax is equal - to I at the far right of the hex. Thus, ijMax represents - the number of positions from the center to the outer edge - towards any of the 6 corners. + In this geometry, the outline-defining _ijMax is equal to I at the far right of the hex. Thus, ijMax represents the + number of positions from the center to the outer edge towards any of the 6 corners. """ def _asciiLinesToIndices(self): @@ -568,8 +599,7 @@ def _getIJFromColRow(self, columnNum, lineNum): Notes ----- - Not used in reading from file b/c inefficient/repeated base calc - but required for writing from ij data + Not used in reading from file b/c inefficient/repeated base calc but required for writing from ij data. """ iBase, jBase = self._getIJBaseByAsciiLine(lineNum) return self._getIJFromColAndBase(columnNum, iBase, jBase) @@ -580,8 +610,7 @@ def _getIJBaseByAsciiLine(self, asciiLineNum): Upper left is shifted by (size-1)//2 - for a 19-line grid, we have the top left as (-18,9) - and then: (-17, 8), (-16, 7), ... + for a 19-line grid, we have the top left as (-18,9) and then: (-17, 8), (-16, 7), ... """ shift = self._ijMax iBase = -shift * 2 + asciiLineNum @@ -590,8 +619,7 @@ def _getIJBaseByAsciiLine(self, asciiLineNum): def _updateDimensionsFromAsciiLines(self): """Update dimension metadata when reading ascii.""" - # ijmax here can be inferred directly from the max number of columns - # in the asciimap text + # ijmax here can be inferred directly from the max number of columns in the asciimap text self._ijMax = (self._asciiMaxCol - 1) // 2 def _updateDimensionsFromData(self): diff --git a/armi/utils/tests/test_asciimaps.py b/armi/utils/tests/test_asciimaps.py index 17fbc366cf..80504f9f75 100644 --- a/armi/utils/tests/test_asciimaps.py +++ b/armi/utils/tests/test_asciimaps.py @@ -108,6 +108,7 @@ EX IC IC PC OC """ +# This is a "corners-up" hexagonal map. HEX_FULL_MAP = """- - - - - - - - - 1 1 1 1 1 1 1 1 1 4 - - - - - - - - 1 1 1 1 1 1 1 1 1 1 1 - - - - - - - 1 8 1 1 1 1 1 1 1 1 1 1 @@ -129,6 +130,7 @@ 1 1 1 1 1 1 1 1 1 1 """ +# This is a "flats-up" hexagonal map. HEX_FULL_MAP_FLAT = """- - - - ORS ORS ORS - - - ORS ORS ORS ORS - - - ORS IRS IRS IRS ORS @@ -289,15 +291,26 @@ def test_troublesomeHexThird(self): self.assertEqual(asciimap[5, 0], "TG") - def test_hexFull(self): - """Test sample full hex map against known answers.""" - # hex map is 19 rows tall, so it should go from -9 to 9 + def test_hexFullCornersUp(self): + """Test sample full hex map (with hex corners up) against known answers.""" + # hex map is 19 rows tall: from -9 to 9 asciimap = asciimaps.AsciiMapHexFullTipsUp() - with io.StringIO() as stream: - stream.write(HEX_FULL_MAP) - stream.seek(0) - asciimap.readAscii(stream.read()) + asciimap.readAscii(HEX_FULL_MAP) + # TODO: JOHN! Why does "print(asciimap)" fail here? + # ValueError: Inconsistent lines (19) and offsets (38) + # print(asciimap) + # self.assertIn("7 1 1 1 1 1 1 1 1 0", str(asciimap)) + # spot check some values in the map + self.assertEqual(asciimap[-8, 7], "8") + self.assertEqual(asciimap[-9, 0], "7") + self.assertEqual(asciimap[0, -1], "2") + self.assertEqual(asciimap[0, -8], "6") + self.assertEqual(asciimap[0, 0], "0") + self.assertEqual(asciimap[0, 9], "4") + self.assertEqual(asciimap[6, -6], "3") + + # also test writing from pure data (vs. reading) gives the exact same map asciimap2 = asciimaps.AsciiMapHexFullTipsUp() for ij, spec in asciimap.items(): asciimap2.asciiLabelByIndices[ij] = spec @@ -308,13 +321,39 @@ def test_hexFull(self): stream.seek(0) output = stream.read() self.assertEqual(output, HEX_FULL_MAP) - self.assertEqual(asciimap[0, 0], "0") - self.assertEqual(asciimap[0, -1], "2") - self.assertEqual(asciimap[0, -8], "6") - self.assertEqual(asciimap[0, 9], "4") - self.assertEqual(asciimap[-9, 0], "7") - self.assertEqual(asciimap[-8, 7], "8") - self.assertEqual(asciimap[6, -6], "3") + + self.assertIn("7 1 1 1 1 1 1 1 1 0", str(asciimap2)) + + def test_hexFullFlatsUp(self): + """Test sample full hex map (with hex flats up) against known answers.""" + # hex map is 21 rows tall: from -10 to 10 + asciimap = asciimaps.AsciiMapHexFullFlatsUp() + asciimap.readAscii(HEX_FULL_MAP_FLAT) + + # spot check some values in the map + self.assertIn("VOTA ICS IC IRT ICS OC", str(asciimap)) + self.assertEqual(asciimap[-3, 10], "ORS") + self.assertEqual(asciimap[0, -9], "ORS") + self.assertEqual(asciimap[0, 0], "IC") + self.assertEqual(asciimap[0, 9], "ORS") + self.assertEqual(asciimap[4, -6], "RR7") + self.assertEqual(asciimap[6, 0], "RR7") + self.assertEqual(asciimap[7, -1], "RR89") + + # also test writing from pure data (vs. reading) gives the exact same map + asciimap2 = asciimaps.AsciiMapHexFullFlatsUp() + for ij, spec in asciimap.items(): + asciimap2.asciiLabelByIndices[ij] = spec + + with io.StringIO() as stream: + asciimap2.gridContentsToAscii() + asciimap2.writeAscii(stream) + stream.seek(0) + output = stream.read() + self.assertEqual(output, HEX_FULL_MAP_FLAT) + + self.assertIn("VOTA ICS IC IRT ICS OC", str(asciimap)) + self.assertIn("VOTA ICS IC IRT ICS OC", str(asciimap2)) def test_hexFullFlat(self): """Test sample full hex map against known answers.""" @@ -335,7 +374,7 @@ def test_hexFullFlat(self): self.assertEqual(asciimap[-5, 2], "VOTA") self.assertEqual(asciimap[2, 3], "FS") - # also test writing from pure data (vs. reading) gives the exact same map :o + # also test writing from pure data (vs. reading) gives the exact same map with io.StringIO() as stream: asciimap2 = asciimaps.AsciiMapHexFullFlatsUp() asciimap2.asciiLabelByIndices = asciimap.asciiLabelByIndices From 467a613505a081d31da3f73548dcaca5e2d94f6a Mon Sep 17 00:00:00 2001 From: jstilley Date: Fri, 9 Feb 2024 10:36:01 -0800 Subject: [PATCH 02/15] FIXED A BUG - offsets were sometimes wrong in hex corners-up maps --- armi/utils/asciimaps.py | 42 +++++++++++------------------- armi/utils/tests/test_asciimaps.py | 6 ++--- 2 files changed, 17 insertions(+), 31 deletions(-) diff --git a/armi/utils/asciimaps.py b/armi/utils/asciimaps.py index 5cf507ad90..535225715e 100644 --- a/armi/utils/asciimaps.py +++ b/armi/utils/asciimaps.py @@ -99,9 +99,17 @@ def writeAscii(self, stream): stream.write(self.__str__()) def __str__(self): - """TODO JOHN""" + """Build the human-readable ASCII string representing the lattice map. + + This method is useful for quickly printing out a lattice map. + + Returns + ------- + str : The custom ARMI ASCII-art-style string representing the map. + """ + # Do some basic validation if not self.asciiLines: - raise ValueError("Cannot write ASCII map before ASCII lines are processed") + raise ValueError("Cannot write ASCII map before ASCII lines are processed.") if len(self.asciiOffsets) != len(self.asciiLines): runLog.error(f"AsciiLines: {self.asciiLines}") @@ -111,6 +119,7 @@ def __str__(self): f"and offsets ({len(self.asciiOffsets)})" ) + # Finally, build the string representation. txt = "" fmt = f"{{val:{len(self._placeholder)}s}}" for offset, line in zip(self.asciiOffsets, self.asciiLines): @@ -199,35 +208,27 @@ def gridContentsToAscii(self): is universal. In some implementations, this operation is in a different method for efficiency. """ - print(f"TODO JOHN 1 _asciiMaxLine: {self._asciiMaxLine}") self._updateDimensionsFromData() - print(f"TODO JOHN 2 _asciiMaxLine: {self._asciiMaxLine}") self.asciiLines = [] - print("--------------------------------------------------") for lineNum in self._getLineNumsToWrite(): - print(lineNum) line = [] for colNum in range(self._asciiMaxCol): - print(f" {colNum}") ij = self._getIJFromColRow(colNum, lineNum) # convert to string and strip any whitespace in thing we're representing line.append( str(self.asciiLabelByIndices.get(ij, PLACEHOLDER)).replace(" ", "") ) self.asciiLines.append(line) - print("+++++++++++++++++++++++++++++++++++++++++++++++++++") # clean data noDataLinesYet = True # handle all-placeholder rows newLines = [] for line in self.asciiLines: - print(line) if re.search(f"^[{PLACEHOLDER}]+$", "".join(line)) and noDataLinesYet: continue noDataLinesYet = False newLine = self._removeTrailingPlaceholders(line) - print(f" {newLine}") if newLine: newLines.append(newLine) else: @@ -237,6 +238,7 @@ def gridContentsToAscii(self): raise ValueError( "Cannot write asciimaps with blank rows from pure data yet." ) + if not newLines: raise ValueError("No data found") self.asciiLines = newLines @@ -272,6 +274,7 @@ def __setitem__(self, ijKey, item): def _makeOffsets(self): """Build offsets.""" + raise NotImplementedError def items(self): return self.asciiLabelByIndices.items() @@ -317,7 +320,7 @@ def _getIJFromColRow(self, columnNum, lineNum): def _makeOffsets(self): """Cartesian grids have 0 offset on all lines.""" - AsciiMap._makeOffsets(self) + self.asciiOffsets = [] for _line in self.asciiLines: self.asciiOffsets.append(0) @@ -462,14 +465,7 @@ def _updateDimensionsFromData(self): # now that we understand how many corner positions are truncated, # we can fully determine the size of the ascii map self._asciiMaxCol = self._ijMax + 1 - print( - f"\n TODO JOHN 1/3 before: self._asciiMaxLine: {self._asciiMaxLine}" - ) - print( - f" self._ijMax: {self._ijMax}, self._asciiLinesOffCorner: {self._asciiLinesOffCorner}" - ) self._asciiMaxLine = self._ijMax * 2 + 1 - self._asciiLinesOffCorner - print(f" TODO JOHN 1/3 after: self._asciiMaxLine: {self._asciiMaxLine}\n") class AsciiMapHexFullFlatsUp(AsciiMapHexThirdFlatsUp): @@ -528,7 +524,6 @@ def _makeOffsets(self): """ # max lines required if corners were not cut off maxIJIndex = self._ijMax - print(f"TODO JOHN: maxIJIndex: {maxIJIndex}") self.asciiOffsets = [] # grab top left edge going down until corner where it lifts off edge. # Due to the placeholders these just oscillate @@ -536,9 +531,7 @@ def _makeOffsets(self): self.asciiOffsets.append((li - self._asciiLinesOffCorner) % 2) # going away from the left edge, the offsets increase linearly - print(f"TODO JOHN: len(self.asciiOffsets): {len(self.asciiOffsets)}") self.asciiOffsets.extend(range(maxIJIndex + 1)) - print(f"TODO JOHN: len(self.asciiOffsets): {len(self.asciiOffsets)}") # since we allow cut-off corners, we must truncate the offsets # number of items in last line indicates how many @@ -550,12 +543,7 @@ def _makeOffsets(self): def _updateDimensionsFromData(self): AsciiMapHexThirdFlatsUp._updateDimensionsFromData(self) self._asciiMaxCol = self._ijMax + 1 - print(f" TODO JOHN FULL before: self._asciiMaxLine: {self._asciiMaxLine}") - print( - f" self._ijMax: {self._ijMax}, self._asciiLinesOffCorner: {self._asciiLinesOffCorner}" - ) self._asciiMaxLine = self._ijMax * 4 + 1 - self._asciiLinesOffCorner * 2 - print(f" TODO JOHN FULL after: self._asciiMaxLine: {self._asciiMaxLine}") class AsciiMapHexFullTipsUp(AsciiMap): @@ -638,7 +626,7 @@ def _getLineNumsToWrite(self): def _makeOffsets(self): """Full hex tips-up grids have linearly incrementing offset.""" - AsciiMap._makeOffsets(self) + self.asciiOffsets = [] for li, _line in enumerate(self.asciiLines): self.asciiOffsets.append(li) diff --git a/armi/utils/tests/test_asciimaps.py b/armi/utils/tests/test_asciimaps.py index 80504f9f75..272099604b 100644 --- a/armi/utils/tests/test_asciimaps.py +++ b/armi/utils/tests/test_asciimaps.py @@ -296,12 +296,9 @@ def test_hexFullCornersUp(self): # hex map is 19 rows tall: from -9 to 9 asciimap = asciimaps.AsciiMapHexFullTipsUp() asciimap.readAscii(HEX_FULL_MAP) - # TODO: JOHN! Why does "print(asciimap)" fail here? - # ValueError: Inconsistent lines (19) and offsets (38) - # print(asciimap) - # self.assertIn("7 1 1 1 1 1 1 1 1 0", str(asciimap)) # spot check some values in the map + self.assertIn("7 1 1 1 1 1 1 1 1 0", str(asciimap)) self.assertEqual(asciimap[-8, 7], "8") self.assertEqual(asciimap[-9, 0], "7") self.assertEqual(asciimap[0, -1], "2") @@ -322,6 +319,7 @@ def test_hexFullCornersUp(self): output = stream.read() self.assertEqual(output, HEX_FULL_MAP) + self.assertIn("7 1 1 1 1 1 1 1 1 0", str(asciimap)) self.assertIn("7 1 1 1 1 1 1 1 1 0", str(asciimap2)) def test_hexFullFlatsUp(self): From 401919767c9d183761fc7809221b7366281ce0c7 Mon Sep 17 00:00:00 2001 From: jstilley Date: Mon, 12 Feb 2024 11:14:21 -0800 Subject: [PATCH 03/15] Changning language from points-up to corners-up --- armi/reactor/blocks.py | 2 +- armi/reactor/blueprints/gridBlueprint.py | 2 +- armi/reactor/grids/hexagonal.py | 30 +++++++++++++++++++----- armi/reactor/grids/tests/test_grids.py | 8 +++---- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/armi/reactor/blocks.py b/armi/reactor/blocks.py index 97296c974a..74078e6928 100644 --- a/armi/reactor/blocks.py +++ b/armi/reactor/blocks.py @@ -2292,7 +2292,7 @@ def autoCreateSpatialGrids(self): # note that it's the pointed end of the cell hexes that are up (but the # macro shape of the pins forms a hex with a flat top fitting in the assembly) grid = grids.HexGrid.fromPitch( - self.getPinPitch(cold=True), numRings=0, pointedEndUp=True + self.getPinPitch(cold=True), numRings=0, cornersUp=True ) spatialLocators = grids.MultiIndexLocation(grid=self.spatialGrid) numLocations = 0 diff --git a/armi/reactor/blueprints/gridBlueprint.py b/armi/reactor/blueprints/gridBlueprint.py index 0fe0124542..798d703b62 100644 --- a/armi/reactor/blueprints/gridBlueprint.py +++ b/armi/reactor/blueprints/gridBlueprint.py @@ -318,7 +318,7 @@ def _constructSpatialGrid(self): spatialGrid = grids.HexGrid.fromPitch( pitch, numRings=maxIndex + 2, - pointedEndUp=geom == geometry.HEX_CORNERS_UP, + cornersUp=geom == geometry.HEX_CORNERS_UP, ) elif geom == geometry.CARTESIAN: # if full core or not cut-off, bump the first assembly from the center of diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py index 2365b10015..5149cbe5bc 100644 --- a/armi/reactor/grids/hexagonal.py +++ b/armi/reactor/grids/hexagonal.py @@ -63,7 +63,7 @@ class HexGrid(StructuredGrid): Notes ----- - In an axial plane (i, j) are as follows (second one is pointedEndUp):: + In an axial plane (i, j) are as follows (second one is cornersUp):: ( 0, 1) @@ -80,8 +80,24 @@ class HexGrid(StructuredGrid): (-1, 0) ( 0,-1) """ + def __init__( + self, + unitSteps=(0, 0, 0), + bounds=(None, None, None), + unitStepLimits=((0, 1), (0, 1), (0, 1)), + offset=None, + geomType="", + symmetry="", + armiObject=None, + cornersUp=False, + ): + super().__init__( + unitSteps, bounds, unitStepLimits, offset, geomType, symmetry, armiObject + ) + self.cornersUp = cornersUp + @staticmethod - def fromPitch(pitch, numRings=25, armiObject=None, pointedEndUp=False, symmetry=""): + def fromPitch(pitch, numRings=25, armiObject=None, cornersUp=False, symmetry=""): """ Build a finite step-based 2-D hex grid from a hex pitch in cm. @@ -116,7 +132,7 @@ def fromPitch(pitch, numRings=25, armiObject=None, pointedEndUp=False, symmetry= armiObject : ArmiObject, optional The object that this grid is anchored to (i.e. the reactor for a grid of assemblies) - pointedEndUp : bool, optional + cornersUp : bool, optional Rotate the hexagons 30 degrees so that the pointed end faces up instead of the flat. symmetry : string, optional @@ -128,7 +144,7 @@ def fromPitch(pitch, numRings=25, armiObject=None, pointedEndUp=False, symmetry= A functional hexagonal grid object. """ side = hexagon.side(pitch) - if pointedEndUp: + if cornersUp: # rotated 30 degrees CCW from normal # increases in i move you in x and y # increases in j also move you in x and y @@ -142,12 +158,14 @@ def fromPitch(pitch, numRings=25, armiObject=None, pointedEndUp=False, symmetry= # y direction is a function of both. unitSteps = ((1.5 * side, 0.0, 0.0), (pitch / 2.0, pitch, 0.0), (0, 0, 0)) - return HexGrid( + hex = HexGrid( unitSteps=unitSteps, unitStepLimits=((-numRings, numRings), (-numRings, numRings), (0, 1)), armiObject=armiObject, symmetry=symmetry, ) + hex.cornersUp = cornersUp + return hex @property def pitch(self) -> float: @@ -221,7 +239,7 @@ def getNeighboringCellIndices( Return the indices of the immediate neighbors of a mesh point in the plane. Note that these neighbors are ordered counter-clockwise beginning from the - 30 or 60 degree direction. Exact direction is dependent on pointedEndUp arg. + 30 or 60 degree direction. Exact direction is dependent on cornersUp arg. """ return [ (i + 1, j, k), diff --git a/armi/reactor/grids/tests/test_grids.py b/armi/reactor/grids/tests/test_grids.py index 4c1f2634ba..0ff7b11f2b 100644 --- a/armi/reactor/grids/tests/test_grids.py +++ b/armi/reactor/grids/tests/test_grids.py @@ -353,15 +353,15 @@ def test_thirdAndFullSymmetry(self): self.assertEqual(third.getPositionsInRing(3), 12) self.assertEqual(third.getSymmetricEquivalents((3, -2)), [(-1, 3), (-2, -1)]) - def test_pointsUpFlatsUp(self): - """Test the pointedEndUp attribute of the fromPitch method. + def test_cornersUpFlatsUp(self): + """Test the cornersUp attribute of the fromPitch method. .. test:: Build a points-up and a flats-up hexagonal grids. :id: T_ARMI_GRID_HEX_TYPE :tests: R_ARMI_GRID_HEX_TYPE """ - tipsUp = grids.HexGrid.fromPitch(1.0, pointedEndUp=True) - flatsUp = grids.HexGrid.fromPitch(1.0, pointedEndUp=False) + tipsUp = grids.HexGrid.fromPitch(1.0, cornersUp=True) + flatsUp = grids.HexGrid.fromPitch(1.0, cornersUp=False) self.assertEqual(tipsUp._unitSteps[0][0], 0.5) self.assertAlmostEqual(flatsUp._unitSteps[0][0], 0.8660254037844388) From 15be52bfacc54005b0ec8bab601951186af5dfa3 Mon Sep 17 00:00:00 2001 From: jstilley Date: Wed, 14 Feb 2024 09:24:17 -0800 Subject: [PATCH 04/15] FIXING HexGrid.changePitch by adding HexGrid.cornersUp attr --- armi/reactor/grids/hexagonal.py | 76 ++++++++++++++++---------- armi/reactor/grids/structuredgrid.py | 4 +- armi/reactor/grids/tests/test_grids.py | 48 ++++++++++++++-- 3 files changed, 94 insertions(+), 34 deletions(-) diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py index 5149cbe5bc..292a425b23 100644 --- a/armi/reactor/grids/hexagonal.py +++ b/armi/reactor/grids/hexagonal.py @@ -30,7 +30,7 @@ COS30 = sqrt(3) / 2.0 SIN30 = 1.0 / 2.0 -# going CCW from "position 1" (top right) +# going counter-clockwise from "position 1" (top right) TRIANGLES_IN_HEXAGON = numpy.array( [ (+COS30, SIN30), @@ -65,19 +65,23 @@ class HexGrid(StructuredGrid): ----- In an axial plane (i, j) are as follows (second one is cornersUp):: - ( 0, 1) (-1, 1) ( 1, 0) ( 0, 0) (-1, 0) ( 1,-1) ( 0,-1) - ( 0, 1) ( 1, 0) - (-1, 1) ( 0, 0) ( 1,-1) - (-1, 0) ( 0,-1) + + Basic hexagon geometry:: + + - pitch = sqrt(3) * side + - long diagonal = 2 * side + - Area = (sqrt(3) / 4) * side^2 + - perimeter = 6 * side + """ def __init__( @@ -133,8 +137,8 @@ def fromPitch(pitch, numRings=25, armiObject=None, cornersUp=False, symmetry="") The object that this grid is anchored to (i.e. the reactor for a grid of assemblies) cornersUp : bool, optional - Rotate the hexagons 30 degrees so that the pointed end faces up instead of - the flat. + Rotate the hexagons 30 degrees so that the corners point up instead of + the flat faces. symmetry : string, optional A string representation of the symmetry options for the grid. @@ -143,28 +147,15 @@ def fromPitch(pitch, numRings=25, armiObject=None, cornersUp=False, symmetry="") HexGrid A functional hexagonal grid object. """ - side = hexagon.side(pitch) - if cornersUp: - # rotated 30 degrees CCW from normal - # increases in i move you in x and y - # increases in j also move you in x and y - unitSteps = ( - (pitch / 2.0, -pitch / 2.0, 0), - (1.5 * side, 1.5 * side, 0), - (0, 0, 0), - ) - else: - # x direction is only a function of i because j-axis is vertical. - # y direction is a function of both. - unitSteps = ((1.5 * side, 0.0, 0.0), (pitch / 2.0, pitch, 0.0), (0, 0, 0)) + unitSteps = HexGrid.getRawUnitSteps(pitch, cornersUp) hex = HexGrid( unitSteps=unitSteps, unitStepLimits=((-numRings, numRings), (-numRings, numRings), (0, 1)), armiObject=armiObject, symmetry=symmetry, + cornersUp=cornersUp, ) - hex.cornersUp = cornersUp return hex @property @@ -408,12 +399,42 @@ def triangleCoords(self, indices: IJKType) -> numpy.ndarray: scale = self.pitch / 3.0 return xy + scale * TRIANGLES_IN_HEXAGON + @staticmethod + def getRawUnitSteps(pitch, cornersUp=False): + """Get the raw unit steps (ignore step dimensions), for a hex grid. + + Parameters + ---------- + pitch : float + The short diameter of the hexagons (from flat side to flat side). + cornersUp : bool, optional + If True, the hexagons have a corner pointing in the Y direction. Default: False + + Returns + ------- + tuple : The full 3D set of derivatives of X,Y,Z in terms of i,j,k. + """ + side = hexagon.side(pitch) + if cornersUp: + # rotated 30 degrees counter-clockwise from normal + # increases in i moves you in x and y + # increases in j also moves you in x and y + unitSteps = ( + (pitch / 2.0, -pitch / 2.0, 0), + (1.5 * side, 1.5 * side, 0), + (0, 0, 0), + ) + else: + # x direction is only a function of i because j-axis is vertical. + # y direction is a function of both. + unitSteps = ((1.5 * side, 0.0, 0.0), (pitch / 2.0, pitch, 0.0), (0, 0, 0)) + + return unitSteps + def changePitch(self, newPitchCm: float): """Change the hex pitch.""" - side = hexagon.side(newPitchCm) - self._unitSteps = numpy.array( - ((1.5 * side, 0.0, 0.0), (newPitchCm / 2.0, newPitchCm, 0.0), (0, 0, 0)) - )[self._stepDims] + unitSteps = numpy.array(HexGrid.getRawUnitSteps(newPitchCm, self.cornersUp)) + self._unitSteps = unitSteps[self._stepDims] def locatorInDomain(self, locator, symmetryOverlap: Optional[bool] = False) -> bool: # This will include the "top" 120-degree symmetry lines. This is to support @@ -488,8 +509,7 @@ def generateSortedHexLocationList(self, nLocs: int): return locList[: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 + # TODO: This is only used by the old GUI code, and should be moved there. def allPositionsInThird(self, ring, includeEdgeAssems=False): """ Returns a list of all the positions in a ring (in the first third). diff --git a/armi/reactor/grids/structuredgrid.py b/armi/reactor/grids/structuredgrid.py index 420a83f907..81d74add44 100644 --- a/armi/reactor/grids/structuredgrid.py +++ b/armi/reactor/grids/structuredgrid.py @@ -58,7 +58,7 @@ class StructuredGrid(Grid): variety of geometries, including hexagonal and Cartesian. The tuples are not vectors in the direction of the translation, but rather grouped by direction. If the bounds argument is described for a direction, the bounds will be used rather - than the unit step information. The default of (0, 0, 0) makes all dimensions + than the unit step information. The default of (0, 0, 0) makes all dimensions insensitive to indices since the coordinates are calculated by the dot product of this and the indices. With this default, any dimension that is desired to change with indices should be defined with bounds. RZtheta grids are created @@ -73,7 +73,7 @@ class StructuredGrid(Grid): grids to be finite so we can populate them with SpatialLocator objects. offset : 3-tuple, optional Offset in cm for each axis. By default the center of the (0,0,0)-th object is in - the center of the grid. Offsets can move it so that the (0,0,0)-th object can + the center of the grid. Offsets can move it so that the (0,0,0)-th object can be fully within a quadrant (i.e. in a Cartesian grid). armiObject : ArmiObject, optional The ArmiObject that this grid describes. For example if it's a 1-D assembly diff --git a/armi/reactor/grids/tests/test_grids.py b/armi/reactor/grids/tests/test_grids.py index 0ff7b11f2b..2c643dfcb2 100644 --- a/armi/reactor/grids/tests/test_grids.py +++ b/armi/reactor/grids/tests/test_grids.py @@ -433,11 +433,11 @@ def test_is_pickleable(self): newLoc = pickle.load(buf) assert_allclose(loc.indices, newLoc.indices) - def test_adjustPitch(self): - """Adjust the pich of a hexagonal lattice. + def test_adjustPitchFlatsUp(self): + """Adjust the pich of a hexagonal lattice, for a "flats up" grid. .. test:: Construct a hexagonal lattice with three rings. - :id: T_ARMI_GRID_HEX + :id: T_ARMI_GRID_HEX0 :tests: R_ARMI_GRID_HEX .. test:: Return the grid coordinates of different locations. @@ -451,14 +451,15 @@ def test_adjustPitch(self): unitSteps=((1.5 / math.sqrt(3), 0.0, 0.0), (0.5, 1, 0.0), (0, 0, 0)), unitStepLimits=((-3, 3), (-3, 3), (0, 1)), offset=numpy.array([offset, offset, offset]), + cornersUp=False, ) # test that we CAN change the pitch, and it scales the grid (but not the offset) v1 = grid.getCoordinates((1, 0, 0)) grid.changePitch(2.0) + self.assertEqual(grid.pitch, 2.0) v2 = grid.getCoordinates((1, 0, 0)) assert_allclose(2 * v1 - offset, v2) - self.assertEqual(grid.pitch, 2.0) # basic sanity: test number of rings has changed self.assertEqual(grid._unitStepLimits[0][1], 3) @@ -467,6 +468,45 @@ def test_adjustPitch(self): for i in range(3): self.assertEqual(grid.offset[i], offset) + def test_adjustPitchCornersUp(self): + """Adjust the pich of a hexagonal lattice, for a "corners up" grid. + + .. test:: Construct a hexagonal lattice with three rings. + :id: T_ARMI_GRID_HEX1 + :tests: R_ARMI_GRID_HEX + + .. test:: Return the grid coordinates of different locations. + :id: T_ARMI_GRID_GLOBAL_POS1 + :tests: R_ARMI_GRID_GLOBAL_POS + """ + # run this test for a grid with no offset, and then a few random offset values + for offset in [0, 1, 1.123, 3.14]: + offsets = [offset, 0, 0] + # build a hex grid with pitch=1, 3 rings, and the above offset + grid = grids.HexGrid( + unitSteps=((1.5 / math.sqrt(3), 0.0, 0.0), (0.5, 1, 0.0), (0, 0, 0)), + unitStepLimits=((-3, 3), (-3, 3), (0, 1)), + offset=numpy.array(offsets), + cornersUp=True, + ) + + # test that we CAN change the pitch, and it scales the grid (but not the offset) + v1 = grid.getCoordinates((1, 0, 0)) + grid.changePitch(2.0) + self.assertAlmostEqual(grid.pitch, math.sqrt(3), delta=1e-9) + v2 = grid.getCoordinates((1, 0, 0)) + b = math.sqrt(3) - 0.5 + a = math.sqrt(3) * b - 2 + correction = numpy.array([a, b, 0]) + assert_allclose(v1 + correction, v2) + + # basic sanity: test number of rings has changed + self.assertEqual(grid._unitStepLimits[0][1], 3) + + # basic sanity: check the offset exists and is correct + for i, off in enumerate(offsets): + self.assertEqual(grid.offset[i], off) + def test_badIndices(self): grid = grids.HexGrid.fromPitch(1.0, numRings=3) From 88ef2582961de020940784324493b3de69e62948 Mon Sep 17 00:00:00 2001 From: jstilley Date: Thu, 15 Feb 2024 13:38:28 -0800 Subject: [PATCH 05/15] Adding better docstrings --- armi/reactor/grids/hexagonal.py | 68 ++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py index 292a425b23..052891511d 100644 --- a/armi/reactor/grids/hexagonal.py +++ b/armi/reactor/grids/hexagonal.py @@ -257,20 +257,42 @@ def getLabel(self, indices): @staticmethod def _indicesAndEdgeFromRingAndPos(ring, position): + """Given the ring and position, return the (I,J) coordinates, and which edge the grid + cell is on. + + Parameters + ---------- + ring: int + Starting with 1 (not zero), the ring of the grid cell. + position: int + Starting with 1 (not zero), the position of the grid cell, in the ring. + + Returns + ------- + (int, int, int) : I coordinate, J coordinate, which edge of the hex ring + + Notes + ----- + - Edge indicates which edge of the ring in which the hexagon resides. + - Edge 0 is the NE edge, edge 1 is the N edge, etc. + - Offset is (0-based) index of the hexagon in that edge. For instance, + ring 3, pos 12 resides in edge 5 at index 1; it is the second hexagon + in ring 3, edge 5. + """ + # The inputs start counting at 1, but the grid starts counting at zero. ring = ring - 1 pos = position - 1 + # Handle the center grid cell. if ring == 0: if pos != 0: raise ValueError(f"Position in center ring must be 1, not {position}") return 0, 0, 0 - # Edge indicates which edge of the ring in which the hexagon resides. - # Edge 0 is the NE edge, edge 1 is the N edge, etc. - # Offset is (0-based) index of the hexagon in that edge. For instance, - # ring 3, pos 12 resides in edge 5 at index 1; it is the second hexagon - # in ring 3, edge 5. - edge, offset = divmod(pos, ring) # = pos//ring, pos%ring + # find the edge and offset (pos//ring or pos%ring) + edge, offset = divmod(pos, ring) + + # find (I,J) based on the ring, edge, and offset if edge == 0: i = ring - offset j = offset @@ -279,9 +301,9 @@ def _indicesAndEdgeFromRingAndPos(ring, position): j = ring elif edge == 2: i = -ring - j = -offset + ring + j = ring - offset elif edge == 3: - i = -ring + offset + i = offset - ring j = -offset elif edge == 4: i = offset @@ -290,13 +312,25 @@ def _indicesAndEdgeFromRingAndPos(ring, position): i = ring j = offset - ring else: - raise ValueError( - "Edge {} is invalid. From ring {}, pos {}".format(edge, ring, pos) - ) + raise ValueError(f"Edge {edge} is invalid. From ring {ring}, pos {pos}") + return i, j, edge @staticmethod def getIndicesFromRingAndPos(ring: int, pos: int) -> IJType: + """Given the ring and position, return the (I,J) coordinates in the hex grid. + + Parameters + ---------- + ring: int + Starting with 1 (not zero), the ring of the grid cell. + position: int + Starting with 1 (not zero), the position of the grid cell, in the ring. + + Returns + ------- + (int, int) : I coordinate, J coordinate + """ i, j, _edge = HexGrid._indicesAndEdgeFromRingAndPos(ring, pos) return i, j @@ -350,16 +384,16 @@ def overlapsWhichSymmetryLine(self, indices: IJType) -> Optional[int]: return symmetryLine def getSymmetricEquivalents(self, indices: IJKType) -> List[IJType]: - """Retrieve the equivalent indices; return them as-is if this is full core, but - return the symmetric equivalent if this is a 1/3-core grid. + """Retrieve the equivalent indices. If full core return nothing, if 1/3-core grid, + return the symmetric equivalents, if any other grid, raise an error. .. impl:: Equivalent contents in 1/3-core geometries are retrievable. :id: I_ARMI_GRID_EQUIVALENTS :implements: R_ARMI_GRID_EQUIVALENTS This method takes in (I,J,K) indices, and if this ``HexGrid`` is full core, - it returns them as-is. If this ``HexGrid`` is 1/3-core, this method will - return the 1/3-core symmetric equivalent. If this grid is any other kind, + it returns nothing. If this ``HexGrid`` is 1/3-core, this method will return + the 1/3-core symmetric equivalent of just (I,J). If this grid is any other kind, this method will just return an error; a hexagonal grid with any other symmetry is probably an error. """ @@ -379,9 +413,7 @@ def getSymmetricEquivalents(self, indices: IJKType) -> List[IJType]: @staticmethod def _getSymmetricIdenticalsThird(indices) -> List[IJType]: - """This works by rotating the indices by 120 degrees twice, - counterclockwise. - """ + """This works by rotating the indices by 120 degrees twice, counterclockwise.""" i, j = indices[:2] if i == 0 and j == 0: return [] From 98b59eaa0406e057520431b79a7976096dc14936 Mon Sep 17 00:00:00 2001 From: jstilley Date: Tue, 20 Feb 2024 09:29:46 -0800 Subject: [PATCH 06/15] Cleaning up new cornersUp property --- armi/reactor/grids/hexagonal.py | 15 ++++++++++++++- armi/reactor/grids/tests/test_grids.py | 10 ++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py index 052891511d..4205bb1f13 100644 --- a/armi/reactor/grids/hexagonal.py +++ b/armi/reactor/grids/hexagonal.py @@ -98,7 +98,16 @@ def __init__( super().__init__( unitSteps, bounds, unitStepLimits, offset, geomType, symmetry, armiObject ) - self.cornersUp = cornersUp + + @property + def cornersUp(self) -> bool: + """ + Check whether the hexagonal grid is "corners up" or "flats up". + + See the armi.reactor.grids.HexGrid class documentation for an + illustration of the two types of grid indexing. + """ + return self._unitSteps[0][1] != 0.0 @staticmethod def fromPitch(pitch, numRings=25, armiObject=None, cornersUp=False, symmetry=""): @@ -446,8 +455,10 @@ def getRawUnitSteps(pitch, cornersUp=False): ------- tuple : The full 3D set of derivatives of X,Y,Z in terms of i,j,k. """ + print("TODO JOHN getRawUnitSteps") side = hexagon.side(pitch) if cornersUp: + print("TODO JOHN getRawUnitSteps TRUE") # rotated 30 degrees counter-clockwise from normal # increases in i moves you in x and y # increases in j also moves you in x and y @@ -457,6 +468,7 @@ def getRawUnitSteps(pitch, cornersUp=False): (0, 0, 0), ) else: + print("TODO JOHN getRawUnitSteps FALSE") # x direction is only a function of i because j-axis is vertical. # y direction is a function of both. unitSteps = ((1.5 * side, 0.0, 0.0), (pitch / 2.0, pitch, 0.0), (0, 0, 0)) @@ -465,6 +477,7 @@ def getRawUnitSteps(pitch, cornersUp=False): def changePitch(self, newPitchCm: float): """Change the hex pitch.""" + print(f"TODO JOHN changePitch self.cornersUp: {self.cornersUp}") unitSteps = numpy.array(HexGrid.getRawUnitSteps(newPitchCm, self.cornersUp)) self._unitSteps = unitSteps[self._stepDims] diff --git a/armi/reactor/grids/tests/test_grids.py b/armi/reactor/grids/tests/test_grids.py index 2c643dfcb2..3493e862cc 100644 --- a/armi/reactor/grids/tests/test_grids.py +++ b/armi/reactor/grids/tests/test_grids.py @@ -484,7 +484,11 @@ def test_adjustPitchCornersUp(self): offsets = [offset, 0, 0] # build a hex grid with pitch=1, 3 rings, and the above offset grid = grids.HexGrid( - unitSteps=((1.5 / math.sqrt(3), 0.0, 0.0), (0.5, 1, 0.0), (0, 0, 0)), + unitSteps=( + (0.5, -0.5, 0), + (1.5 / math.sqrt(3), 1.5 / math.sqrt(3), 0), + (0, 0, 0), + ), unitStepLimits=((-3, 3), (-3, 3), (0, 1)), offset=numpy.array(offsets), cornersUp=True, @@ -495,9 +499,7 @@ def test_adjustPitchCornersUp(self): grid.changePitch(2.0) self.assertAlmostEqual(grid.pitch, math.sqrt(3), delta=1e-9) v2 = grid.getCoordinates((1, 0, 0)) - b = math.sqrt(3) - 0.5 - a = math.sqrt(3) * b - 2 - correction = numpy.array([a, b, 0]) + correction = numpy.array([0.5, math.sqrt(3) / 2, 0]) assert_allclose(v1 + correction, v2) # basic sanity: test number of rings has changed From ddeed04d417b5730772b1fa37caff69253b372e7 Mon Sep 17 00:00:00 2001 From: jstilley Date: Tue, 20 Feb 2024 09:51:34 -0800 Subject: [PATCH 07/15] Updating release notes --- doc/release/0.3.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/release/0.3.rst b/doc/release/0.3.rst index e27d98c6bc..8cc2cfa628 100644 --- a/doc/release/0.3.rst +++ b/doc/release/0.3.rst @@ -12,11 +12,11 @@ What's new in ARMI? Bug Fixes --------- -#. TBD +#. Fixed two bugs with "corners up" hex grids. (`PR#1649 `_) Changes that Affect Requirements -------------------------------- -#. TBD +#. (`PR#1649 `_) - Touched ``HexGrid`` by adding a "cornersUp" property and fixing two bugs. ARMI v0.3.0 From c1192c6e955d4bb72ac676d1d79e5f46f67be8cb Mon Sep 17 00:00:00 2001 From: jstilley Date: Tue, 20 Feb 2024 09:57:37 -0800 Subject: [PATCH 08/15] Removing debug print outs --- armi/reactor/grids/hexagonal.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py index 4205bb1f13..debd98c74f 100644 --- a/armi/reactor/grids/hexagonal.py +++ b/armi/reactor/grids/hexagonal.py @@ -455,10 +455,8 @@ def getRawUnitSteps(pitch, cornersUp=False): ------- tuple : The full 3D set of derivatives of X,Y,Z in terms of i,j,k. """ - print("TODO JOHN getRawUnitSteps") side = hexagon.side(pitch) if cornersUp: - print("TODO JOHN getRawUnitSteps TRUE") # rotated 30 degrees counter-clockwise from normal # increases in i moves you in x and y # increases in j also moves you in x and y @@ -468,7 +466,6 @@ def getRawUnitSteps(pitch, cornersUp=False): (0, 0, 0), ) else: - print("TODO JOHN getRawUnitSteps FALSE") # x direction is only a function of i because j-axis is vertical. # y direction is a function of both. unitSteps = ((1.5 * side, 0.0, 0.0), (pitch / 2.0, pitch, 0.0), (0, 0, 0)) @@ -477,7 +474,6 @@ def getRawUnitSteps(pitch, cornersUp=False): def changePitch(self, newPitchCm: float): """Change the hex pitch.""" - print(f"TODO JOHN changePitch self.cornersUp: {self.cornersUp}") unitSteps = numpy.array(HexGrid.getRawUnitSteps(newPitchCm, self.cornersUp)) self._unitSteps = unitSteps[self._stepDims] From ee49383ede75793ff73235e11a6178fa6cba289b Mon Sep 17 00:00:00 2001 From: John Stilley <1831479+john-science@users.noreply.github.com> Date: Tue, 20 Feb 2024 16:16:27 -0800 Subject: [PATCH 09/15] Apply suggestions from code review Co-authored-by: Chris Keckler --- armi/reactor/grids/hexagonal.py | 8 ++++---- armi/utils/asciimaps.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py index debd98c74f..f3cee93047 100644 --- a/armi/reactor/grids/hexagonal.py +++ b/armi/reactor/grids/hexagonal.py @@ -271,9 +271,9 @@ def _indicesAndEdgeFromRingAndPos(ring, position): Parameters ---------- - ring: int + ring : int Starting with 1 (not zero), the ring of the grid cell. - position: int + position : int Starting with 1 (not zero), the position of the grid cell, in the ring. Returns @@ -331,9 +331,9 @@ def getIndicesFromRingAndPos(ring: int, pos: int) -> IJType: Parameters ---------- - ring: int + ring : int Starting with 1 (not zero), the ring of the grid cell. - position: int + position : int Starting with 1 (not zero), the position of the grid cell, in the ring. Returns diff --git a/armi/utils/asciimaps.py b/armi/utils/asciimaps.py index 535225715e..c620861178 100644 --- a/armi/utils/asciimaps.py +++ b/armi/utils/asciimaps.py @@ -419,7 +419,7 @@ def _makeOffsets(self): # renomalize the offsets to start at 0 minOffset = min(self.asciiOffsets) - for _li, (_, offset) in enumerate(zip(self.asciiLines, self.asciiOffsets)): + for offset in self.asciiOffsets: newOffsets.append(offset - minOffset) self.asciiOffsets = newOffsets From 472fbee39e2dd7768055423557f27137bfcf18ed Mon Sep 17 00:00:00 2001 From: jstilley Date: Tue, 20 Feb 2024 16:19:40 -0800 Subject: [PATCH 10/15] Responding to comment: clarifying test --- armi/reactor/grids/tests/test_grids.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/armi/reactor/grids/tests/test_grids.py b/armi/reactor/grids/tests/test_grids.py index 3493e862cc..c4ba1daa65 100644 --- a/armi/reactor/grids/tests/test_grids.py +++ b/armi/reactor/grids/tests/test_grids.py @@ -454,6 +454,9 @@ def test_adjustPitchFlatsUp(self): cornersUp=False, ) + # test number of rings before converting pitch + self.assertEqual(grid._unitStepLimits[0][1], 3) + # test that we CAN change the pitch, and it scales the grid (but not the offset) v1 = grid.getCoordinates((1, 0, 0)) grid.changePitch(2.0) @@ -461,7 +464,7 @@ def test_adjustPitchFlatsUp(self): v2 = grid.getCoordinates((1, 0, 0)) assert_allclose(2 * v1 - offset, v2) - # basic sanity: test number of rings has changed + # basic sanity: test number of rings has not changed self.assertEqual(grid._unitStepLimits[0][1], 3) # basic sanity: check the offset exists and is correct @@ -494,6 +497,9 @@ def test_adjustPitchCornersUp(self): cornersUp=True, ) + # test number of rings before converting pitch + self.assertEqual(grid._unitStepLimits[0][1], 3) + # test that we CAN change the pitch, and it scales the grid (but not the offset) v1 = grid.getCoordinates((1, 0, 0)) grid.changePitch(2.0) @@ -502,7 +508,7 @@ def test_adjustPitchCornersUp(self): correction = numpy.array([0.5, math.sqrt(3) / 2, 0]) assert_allclose(v1 + correction, v2) - # basic sanity: test number of rings has changed + # basic sanity: test number of rings has not changed self.assertEqual(grid._unitStepLimits[0][1], 3) # basic sanity: check the offset exists and is correct From 9a56849adc7c4802889f7ef9de00b38cd3b6c5f3 Mon Sep 17 00:00:00 2001 From: jstilley Date: Tue, 20 Feb 2024 16:46:46 -0800 Subject: [PATCH 11/15] Removing HexGrid.__init__() --- armi/reactor/grids/hexagonal.py | 16 ---------------- armi/reactor/grids/tests/test_grids.py | 2 -- 2 files changed, 18 deletions(-) diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py index f3cee93047..5584164a09 100644 --- a/armi/reactor/grids/hexagonal.py +++ b/armi/reactor/grids/hexagonal.py @@ -84,21 +84,6 @@ class HexGrid(StructuredGrid): """ - def __init__( - self, - unitSteps=(0, 0, 0), - bounds=(None, None, None), - unitStepLimits=((0, 1), (0, 1), (0, 1)), - offset=None, - geomType="", - symmetry="", - armiObject=None, - cornersUp=False, - ): - super().__init__( - unitSteps, bounds, unitStepLimits, offset, geomType, symmetry, armiObject - ) - @property def cornersUp(self) -> bool: """ @@ -163,7 +148,6 @@ def fromPitch(pitch, numRings=25, armiObject=None, cornersUp=False, symmetry="") unitStepLimits=((-numRings, numRings), (-numRings, numRings), (0, 1)), armiObject=armiObject, symmetry=symmetry, - cornersUp=cornersUp, ) return hex diff --git a/armi/reactor/grids/tests/test_grids.py b/armi/reactor/grids/tests/test_grids.py index c4ba1daa65..e2090cc23e 100644 --- a/armi/reactor/grids/tests/test_grids.py +++ b/armi/reactor/grids/tests/test_grids.py @@ -451,7 +451,6 @@ def test_adjustPitchFlatsUp(self): unitSteps=((1.5 / math.sqrt(3), 0.0, 0.0), (0.5, 1, 0.0), (0, 0, 0)), unitStepLimits=((-3, 3), (-3, 3), (0, 1)), offset=numpy.array([offset, offset, offset]), - cornersUp=False, ) # test number of rings before converting pitch @@ -494,7 +493,6 @@ def test_adjustPitchCornersUp(self): ), unitStepLimits=((-3, 3), (-3, 3), (0, 1)), offset=numpy.array(offsets), - cornersUp=True, ) # test number of rings before converting pitch From 0017998b956793d0b59f6d2cc48a61d9aa4a812d Mon Sep 17 00:00:00 2001 From: jstilley Date: Tue, 20 Feb 2024 17:05:23 -0800 Subject: [PATCH 12/15] Verbiage changed! --- armi/reactor/grids/hexagonal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py index 5584164a09..4ec7fb8828 100644 --- a/armi/reactor/grids/hexagonal.py +++ b/armi/reactor/grids/hexagonal.py @@ -431,7 +431,7 @@ def getRawUnitSteps(pitch, cornersUp=False): Parameters ---------- pitch : float - The short diameter of the hexagons (from flat side to flat side). + The short diameter of the hexagons (flat to flat). cornersUp : bool, optional If True, the hexagons have a corner pointing in the Y direction. Default: False From 5c20601f3ec98ac9116711a158e752f475753d01 Mon Sep 17 00:00:00 2001 From: jstilley Date: Wed, 21 Feb 2024 09:03:38 -0800 Subject: [PATCH 13/15] Improving diagram of hex grids (ASCII art) --- armi/reactor/grids/hexagonal.py | 44 ++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py index 4ec7fb8828..81ee115fc8 100644 --- a/armi/reactor/grids/hexagonal.py +++ b/armi/reactor/grids/hexagonal.py @@ -44,7 +44,7 @@ class HexGrid(StructuredGrid): - """ + r""" Has 6 neighbors in plane. It is recommended to use :meth:`fromPitch` rather than calling the ``__init__`` @@ -63,17 +63,37 @@ class HexGrid(StructuredGrid): Notes ----- - In an axial plane (i, j) are as follows (second one is cornersUp):: - - ( 0, 1) - (-1, 1) ( 1, 0) - ( 0, 0) - (-1, 0) ( 1,-1) - ( 0,-1) - - ( 0, 1) ( 1, 0) - (-1, 1) ( 0, 0) ( 1,-1) - (-1, 0) ( 0,-1) + In an axial plane (i, j) are as follows (flats up):: + _____ + / \ + _____/ 0,1 \_____ + / \ / \ + / -1,1 \_____/ 1,0 \ + \ / \ / + \_____/ 0,0 \_____/ + / \ / \ + / -1,0 \_____/ 1,-1 \ + \ / \ / + \_____/ 0,-1 \_____/ + \ / + \_____/ + + In an axial plane (i, j) are as follows (corners up):: + + / \ / \ + / \ / \ + | 0,1 | 1,0 | + | | | + / \ / \ / \ + / \ / \ / \ + | -1,1 | 0,0 | 1,-1 | + | | | | + \ / \ / \ / + \ / \ / \ / + | -1,0 | 0,-1 | + | | | + \ / \ / + \ / \ / Basic hexagon geometry:: From 17c4d9772f82a1a9c626fc7c5beeb1d8f0d415b4 Mon Sep 17 00:00:00 2001 From: jstilley Date: Wed, 21 Feb 2024 09:05:34 -0800 Subject: [PATCH 14/15] Making helper method private --- armi/reactor/grids/hexagonal.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py index 81ee115fc8..45d3fffc25 100644 --- a/armi/reactor/grids/hexagonal.py +++ b/armi/reactor/grids/hexagonal.py @@ -161,7 +161,7 @@ def fromPitch(pitch, numRings=25, armiObject=None, cornersUp=False, symmetry="") HexGrid A functional hexagonal grid object. """ - unitSteps = HexGrid.getRawUnitSteps(pitch, cornersUp) + unitSteps = HexGrid._getRawUnitSteps(pitch, cornersUp) hex = HexGrid( unitSteps=unitSteps, @@ -445,7 +445,7 @@ def triangleCoords(self, indices: IJKType) -> numpy.ndarray: return xy + scale * TRIANGLES_IN_HEXAGON @staticmethod - def getRawUnitSteps(pitch, cornersUp=False): + def _getRawUnitSteps(pitch, cornersUp=False): """Get the raw unit steps (ignore step dimensions), for a hex grid. Parameters @@ -478,7 +478,7 @@ def getRawUnitSteps(pitch, cornersUp=False): def changePitch(self, newPitchCm: float): """Change the hex pitch.""" - unitSteps = numpy.array(HexGrid.getRawUnitSteps(newPitchCm, self.cornersUp)) + unitSteps = numpy.array(HexGrid._getRawUnitSteps(newPitchCm, self.cornersUp)) self._unitSteps = unitSteps[self._stepDims] def locatorInDomain(self, locator, symmetryOverlap: Optional[bool] = False) -> bool: From bac47f261b2c842cc3619b23cee253ea961e3d51 Mon Sep 17 00:00:00 2001 From: jstilley Date: Wed, 21 Feb 2024 13:56:08 -0800 Subject: [PATCH 15/15] Adding diagram for ring,pos --- armi/reactor/grids/hexagonal.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/armi/reactor/grids/hexagonal.py b/armi/reactor/grids/hexagonal.py index 985f3807ec..f479dddcd7 100644 --- a/armi/reactor/grids/hexagonal.py +++ b/armi/reactor/grids/hexagonal.py @@ -331,7 +331,7 @@ def _indicesAndEdgeFromRingAndPos(ring, position): @staticmethod def getIndicesFromRingAndPos(ring: int, pos: int) -> IJType: - """Given the ring and position, return the (I,J) coordinates in the hex grid. + r"""Given the ring and position, return the (I,J) coordinates in the hex grid. Parameters ---------- @@ -343,6 +343,28 @@ def getIndicesFromRingAndPos(ring: int, pos: int) -> IJType: Returns ------- (int, int) : I coordinate, J coordinate + + Notes + ----- + In an axial plane, the (ring, position) coordinates are as follows:: + + Flat-to-Flat Corners Up + _____ + / \ / \ / \ + _____/ 2,2 \_____ / \ / \ + / \ / \ | 2,2 | 2,1 | + / 2,3 \_____/ 2,1 \ | | | + \ / \ / / \ / \ / \ + \_____/ 1,1 \_____/ / \ / \ / \ + / \ / \ | 2,3 | 1,1 | 2,6 | + / 2,4 \_____/ 2,6 \ | | | | + \ / \ / \ / \ / \ / + \_____/ 2,5 \_____/ \ / \ / \ / + \ / | 2,4 | 2,5 | + \_____/ | | | + \ / \ / + \ / \ / + """ i, j, _edge = HexGrid._indicesAndEdgeFromRingAndPos(ring, pos) return i, j