Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing sub-block grids #1947

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions armi/reactor/assemblies.py
Original file line number Diff line number Diff line change
Expand Up @@ -1219,9 +1219,8 @@ def rotate(self, rad):

Parameters
----------
rad: float
rad : float
number (in radians) specifying the angle of counter clockwise rotation

"""
for b in self:
b.rotate(rad)
Expand All @@ -1230,6 +1229,15 @@ def isOnWhichSymmetryLine(self):
grid = self.parent.spatialGrid
return grid.overlapsWhichSymmetryLine(self.spatialLocator.getCompleteIndices())

def orientBlocks(self, cornersUp):
"""Add special grids to the blocks inside this Assembly, respecting their orientation."""
for b in self:
if b.spatialGrid is None:
try:
b.autoCreateSpatialGrids(cornersUp)
except (ValueError, NotImplementedError) as e:
runLog.warning(str(e), single=True)

Comment on lines +1232 to +1240
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this live on the HexAssembly because we have a cornerUp flag that is hex specific?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All assemblies have a .autoCreateSpatialGrids() method though, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocks yes, but the cornersUp switch is hex specific. What does this mean for a cartesian grid? I get that it's good OOP practice to have the same signatures for overridden methods, but this feels in the vein of "everything is hexagonal in ARMI"


class HexAssembly(Assembly):
"""An assembly that is hexagonal in cross-section."""
Expand Down
54 changes: 37 additions & 17 deletions armi/reactor/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ def getSmearDensity(self, cold=True):

return smearDensity

def autoCreateSpatialGrids(self):
def autoCreateSpatialGrids(self, cornersUp=True):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same reservations about putting cornersUp switch in the generic Block.autoCreateSpatialGrids

"""
Creates a spatialGrid for a Block.

Expand Down Expand Up @@ -2340,7 +2340,7 @@ def getSymmetryFactor(self):
return 2.0
return 1.0

def autoCreateSpatialGrids(self):
def autoCreateSpatialGrids(self, cornersUp=True):
"""
Given a block without a spatialGrid, create a spatialGrid and give its children the
corresponding spatialLocators (if it is a simple block).
Expand All @@ -2349,20 +2349,27 @@ def autoCreateSpatialGrids(self):
to 1 or N but no other multiplicities. Also, this should only happen when N fits exactly
into a given number of hex rings. Otherwise, do not create a grid for this block.

Parameters
----------
cornersUp: bool
Should the hexagons of this grid be corners up (as opposed to flats-up)?

Notes
-----
If the Block meets all the conditions, we gather all components to either be a
multiIndexLocation containing all of the pin positions, or the locator is the center (0,0).
When a hex grid has another hex grid nested inside it, the nested grid has the opposite
orientation (corners vs flats up). This method takes care of that.

Also, this only works on blocks that have 'flat side up'.
If components inside this block are multiplicity 1, they get a single locator at the center
of the grid cell. If the multiplicity is greater than 1, all the components are added to a
multiIndexLocation on the hex grid.

Raises
------
ValueError
If the multiplicities of the block are not only 1 or N or if generated ringNumber leads
to more positions than necessary.
"""
# Check multiplicities...
# Check multiplicities
mults = {c.getDimension("mult") for c in self.iterComponents()}

if len(mults) != 2 or 1 not in mults:
Expand All @@ -2372,29 +2379,42 @@ def autoCreateSpatialGrids(self):
)
)

ringNumber = hexagon.numRingsToHoldNumCells(self.getNumPins())
# For the below to work, there must not be multiple wire or multiple clad types.
# 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)
# build the grid, from pitch and orientation
grid = grids.HexGrid.fromPitch(
self.getPinPitch(cold=True), numRings=0, cornersUp=True
self.getPinPitch(cold=True),
numRings=0,
armiObject=self,
cornersUp=cornersUp,
)

# build the sub-grid, with opposite orientation to the block
subGrid = grids.HexGrid.fromPitch(
self.getPinPitch(cold=True),
numRings=0,
armiObject=self,
cornersUp=not cornersUp,
)
spatialLocators = grids.MultiIndexLocation(grid=self.spatialGrid)

ringNumber = hexagon.numRingsToHoldNumCells(self.getNumPins())
numLocations = 0
for ring in range(ringNumber):
numLocations = numLocations + hexagon.numPositionsInRing(ring + 1)

if numLocations != self.getNumPins():
raise ValueError(
"Cannot create spatialGrid, number of locations in rings{} not equal to pin number{}".format(
"Cannot create spatialGrid, number of locations in rings {} not equal to pin number {}".format(
numLocations, self.getNumPins()
)
)

i = 0
# set the spatial position of the sub-block components
spatialLocators = grids.MultiIndexLocation(grid=subGrid)
for ring in range(ringNumber):
for pos in range(grid.getPositionsInRing(ring + 1)):
i, j = grid.getIndicesFromRingAndPos(ring + 1, pos + 1)
spatialLocators.append(grid[i, j, 0])
for pos in range(subGrid.getPositionsInRing(ring + 1)):
i, j = subGrid.getIndicesFromRingAndPos(ring + 1, pos + 1)
spatialLocators.append(subGrid[i, j, 0])

# finally, fill the spatial grid, and put the sub-block components on it
if self.spatialGrid is None:
self.spatialGrid = grid
Comment on lines 2418 to 2419
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The caller for this method in Assembly.orientBlocks only calls this if HexBlock.spatialGrid is None. Do we need to have this check here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a public method. People can call it multiple times, any time they want, downstream in their plugins.

So, this method would cause havoc if we let people call it multiple times in a row.

(I am considering moving this check (if self.spatialGrid is None) up to the top of the method in a different PR, by the way. But we have like 6 unit tests designed specifically for this check to work this way that will break. So I didn't want to muddy the waters and make this PR less clear for you to read. So I'll do it as it's own stand-alone PR.)

for c in self:
Expand Down
10 changes: 2 additions & 8 deletions armi/reactor/blueprints/blockBlueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,12 @@
from armi import getPluginManagerOrFail, runLog
from armi.materials.material import Material
from armi.reactor import blocks
from armi.reactor.composites import Composite
from armi.reactor import parameters
from armi.reactor.flags import Flags
from armi.reactor.blueprints import componentBlueprint
from armi.reactor.components.component import Component
from armi.reactor.composites import Composite
from armi.reactor.converters import blockConverters
from armi.settings.fwSettings import globalSettings
from armi.reactor.flags import Flags


def _configureGeomOptions():
Expand Down Expand Up @@ -240,11 +239,6 @@ def construct(
b.verifyBlockDims()
b.spatialGrid = spatialGrid

if b.spatialGrid is None and cs[globalSettings.CONF_BLOCK_AUTO_GRID]:
drewj-tp marked this conversation as resolved.
Show resolved Hide resolved
try:
b.autoCreateSpatialGrids()
except (ValueError, NotImplementedError) as e:
runLog.warning(str(e), single=True)
return b

def _getBlockwiseMaterialModifierOptions(
Expand Down
4 changes: 4 additions & 0 deletions armi/reactor/cores.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,11 +563,15 @@ def add(self, a, spatialLocator=None):
"".format(aName, self.assembliesByName[aName], a, self.r.p.maxAssemNum)
)
raise RuntimeError("Core already contains an assembly with the same name.")

self.assembliesByName[aName] = a
for b in a:
self.blocksByName[b.getName()] = b

if self.geomType == geometry.GeomType.HEX:
cornerUp = self.spatialGrid.cornersUp
a.orientBlocks(cornersUp=cornerUp)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Block.spatialGrid should be the opposite orientation of the grid on which its parent assembly lies

Suggested change
a.orientBlocks(cornersUp=cornerUp)
a.orientBlocks(cornersUp=not cornerUp)


ring, _loc = self.spatialGrid.getRingPos(
a.spatialLocator.getCompleteIndices()
)
Expand Down
4 changes: 4 additions & 0 deletions armi/reactor/grids/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ def __setstate__(self, state: Dict):
def isAxialOnly(self) -> bool:
"""Indicate to parts of ARMI if this Grid handles only axial cells."""

@property
def cornersUp(self) -> bool:
return False

Comment on lines +161 to +164
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CartesianGrid.cornersUp doesn't make sense to me. This seems like putting hex specific logic onto the base class just to unify the API.

@abstractmethod
def __len__(self) -> int:
"""Number of items in the grid."""
Expand Down
2 changes: 1 addition & 1 deletion armi/reactor/grids/locations.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ def getLocalCoordinates(self, nativeCoords=False):
"""Return the coordinates of the center of the mesh cell here in cm."""
if self.grid is None:
raise ValueError(
"Cannot get local coordinates of {} because grid is None.".format(self)
f"Cannot get local coordinates of {self} because grid is None."
)
return self.grid.getCoordinates(self.indices, nativeCoords=nativeCoords)

Expand Down
4 changes: 2 additions & 2 deletions armi/reactor/grids/structuredGrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@

import numpy as np

from armi.reactor.grids.grid import Grid
from armi.reactor.grids.locations import (
IJKType,
LocationBase,
IndexLocation,
LocationBase,
MultiIndexLocation,
)
from armi.reactor.grids.grid import Grid

# data structure for database-serialization of grids
GridParameters = collections.namedtuple(
Expand Down
92 changes: 84 additions & 8 deletions armi/reactor/grids/tests/test_grids.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
"""Tests for grids."""
from io import BytesIO
import math
import unittest
import pickle
import unittest

import numpy as np
from numpy.testing import assert_allclose, assert_array_equal
Expand Down Expand Up @@ -238,8 +238,9 @@ def test_ringPosFromIndicesIncorrect(self):
class TestHexGrid(unittest.TestCase):
"""A set of tests for the Hexagonal Grid."""

def test_positions(self):
grid = grids.HexGrid.fromPitch(1.0)
def test_getCoordinatesFlatsUp(self):
"""Test getCoordinates() for flats up hex grids."""
grid = grids.HexGrid.fromPitch(1.0, cornersUp=False)
self.assertAlmostEqual(grid.pitch, 1.0)
side = 1.0 / math.sqrt(3)
assert_allclose(grid.getCoordinates((0, 0, 0)), (0.0, 0.0, 0.0))
Expand All @@ -261,6 +262,86 @@ def test_positions(self):
assert_allclose(grid.getCoordinates((1, 0, 0)), iDirection)
assert_allclose(grid.getCoordinates((0, 1, 0)), jDirection)

def test_getCoordinatesCornersUp(self):
"""Test getCoordinates() for corners up hex grids."""
grid = grids.HexGrid.fromPitch(1.0, cornersUp=True)
self.assertAlmostEqual(grid.pitch, 1.0)
side = 1.0 / math.sqrt(3)
assert_allclose(grid.getCoordinates((0, 0, 0)), (0.0, 0.0, 0.0))
assert_allclose(grid.getCoordinates((1, 0, 0)), (0.5, 1.5 * side, 0.0))
assert_allclose(grid.getCoordinates((-1, 0, 0)), (-0.5, -1.5 * side, 0.0))
assert_allclose(grid.getCoordinates((0, 1, 0)), (-0.5, 1.5 * side, 0.0))
assert_allclose(grid.getCoordinates((1, -1, 0)), (1, 0.0, 0.0))
john-science marked this conversation as resolved.
Show resolved Hide resolved

unitSteps = grid.reduce()[0]
iDirection = tuple(direction[0] for direction in unitSteps)
jDirection = tuple(direction[1] for direction in unitSteps)
for directionVector in (iDirection, jDirection):
self.assertAlmostEqual(
(sum(val**2 for val in directionVector)) ** 0.5,
1.0,
msg=f"Direction vector {directionVector} should have "
"magnitude 1 for pitch 1.",
)
assert_allclose(grid.getCoordinates((1, 0, 0)), iDirection)
assert_allclose(grid.getCoordinates((0, 1, 0)), jDirection)

def test_getLocalCoordinatesHex(self):
"""Test getLocalCoordinates() is different for corners up vs flats up hex grids."""
grid0 = grids.HexGrid.fromPitch(1.0, cornersUp=True)
grid1 = grids.HexGrid.fromPitch(1.0, cornersUp=False)
for i in range(3):
for j in range(3):
if i == 0 and j == 0:
continue
coords0 = grid0[i, j, 0].getLocalCoordinates()
coords1 = grid1[i, j, 0].getLocalCoordinates()
self.assertNotEqual(coords0[0], coords1[0], msg=f"X @ ({i}, {j})")
self.assertNotEqual(coords0[1], coords1[1], msg=f"Y @ ({i}, {j})")
self.assertEqual(coords0[2], coords1[2], msg=f"Z @ ({i}, {j})")

def test_getLocalCoordinatesCornersUp(self):
"""Test getLocalCoordinates() for corners up hex grids."""
# validate the first ring of a corners-up hex grid
grid = grids.HexGrid.fromPitch(1.0, cornersUp=True)
vals = []
for pos in range(grid.getPositionsInRing(2)):
i, j = grid.getIndicesFromRingAndPos(2, pos + 1)
vals.append(grid[i, j, 0].getLocalCoordinates())

# short in Y
maxY = max(v[1] for v in vals)
minY = min(v[1] for v in vals)
self.assertLess(maxY, 1)
self.assertGreater(minY, -1)
Comment on lines +315 to +316
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommend a stronger check for asserting max and min y locations are +/- pitch * cos(60)


# long in X
maxX = max(v[0] for v in vals)
minX = min(v[0] for v in vals)
self.assertAlmostEqual(maxX, 1)
self.assertAlmostEqual(minX, -1)

def test_getLocalCoordinatesFlatsUp(self):
"""Test getLocalCoordinates() for flats up hex grids."""
# validate the first ring of a flats-up hex grid
grid = grids.HexGrid.fromPitch(1.0, cornersUp=False)
vals = []
for pos in range(grid.getPositionsInRing(2)):
i, j = grid.getIndicesFromRingAndPos(2, pos + 1)
vals.append(grid[i, j, 0].getLocalCoordinates())

# long in Y
maxY = max(v[1] for v in vals)
minY = min(v[1] for v in vals)
self.assertAlmostEqual(maxY, 1)
self.assertAlmostEqual(minY, -1)

# short in X
maxX = max(v[0] for v in vals)
minX = min(v[0] for v in vals)
self.assertLess(maxX, 1)
self.assertGreater(minX, -1)
Comment on lines +342 to +343
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same suggestion to check max and min x positions are +/- pitch * cos(60)


def test_neighbors(self):
grid = grids.HexGrid.fromPitch(1.0)
neighbs = grid.getNeighboringCellIndices(0, 0, 0)
Expand Down Expand Up @@ -837,8 +918,3 @@ def test_getLocations(self):
self.assertEqual(x, 0.0)
self.assertEqual(y, 0.0)
self.assertEqual(z, count + 0.5)


if __name__ == "__main__":
# import sys;sys.argv = ["", "TestHexGrid.testPositions"]
unittest.main()
4 changes: 4 additions & 0 deletions armi/reactor/spentFuelPool.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ def add(self, assem, loc=None):
else:
loc = self._getNextLocation()

# orient the blocks to match this grid
cornerUp = self.spatialGrid.cornersUp
assem.orientBlocks(cornerUp)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assem.orientBlocks(cornerUp)
assem.orientBlocks(not cornerUp)


super().add(assem, loc)

def getAssembly(self, name):
Expand Down
Loading
Loading