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

Resolving axial component linkages for blocks with multiple identical component definitions #769

Open
keckler opened this issue Jul 14, 2022 · 3 comments · May be fixed by #1376
Open

Resolving axial component linkages for blocks with multiple identical component definitions #769

keckler opened this issue Jul 14, 2022 · 3 comments · May be fixed by #1376

Comments

@keckler
Copy link
Member

keckler commented Jul 14, 2022

In the current implementation of the axialExpansionChanger, linkages between axially-continuous components are inferred via a set of criteria:

def _determineLinked(componentA, componentB):
"""determine axial component linkage for two components
Parameters
----------
componentA : :py:class:`Component <armi.reactor.components.component.Component>`
component of interest
componentB : :py:class:`Component <armi.reactor.components.component.Component>`
component to compare and see if is linked to componentA
Notes
-----
- Requires that shapes have the getCircleInnerDiameter and getBoundingCircleOuterDiameter defined
- For axial linkage to be True, components MUST be solids, the same Component Class, multiplicity, and meet inner
and outer diameter requirements.
- When component dimensions are retrieved, cold=True to ensure that dimensions are evaluated
at cold/input temperatures. At temperature, solid-solid interfaces in ARMI may produce
slight overlaps due to thermal expansion. Handling these potential overlaps are out of scope.
Returns
-------
linked : bool
status is componentA and componentB are axially linked to one another
"""
if (
(componentA.containsSolidMaterial() and componentB.containsSolidMaterial())
and isinstance(componentA, type(componentB))
and (componentA.getDimension("mult") == componentB.getDimension("mult"))
):
idA, odA = (
componentA.getCircleInnerDiameter(cold=True),
componentA.getBoundingCircleOuterDiameter(cold=True),
)
idB, odB = (
componentB.getCircleInnerDiameter(cold=True),
componentB.getBoundingCircleOuterDiameter(cold=True),
)
biggerID = max(idA, idB)
smallerOD = min(odA, odB)
if biggerID >= smallerOD:
# one object fits inside the other
linked = False
else:
linked = True
else:
linked = False
return linked

However, at least one case slips through the cracks of this logic. If one defines a block with multiple independent pins that have the same exact geometry and material class, but that utilize different enrichments (or something else defined via the material modifications).

In such a case, the axialExpansionChanger will not be able to disambiguate the identical components, and when trying to link them to the components in the blocks above/below, it won't know which ones to link up and it will crash.

This might sound like an unlikely scenario, but I am trying to model more than one assembly that has these properties.

My first thought is that in such a complicated scenario, we might force the user to use an explicitly-defined grid for their block, as is demonstrated in the LWR blueprints tutorial:
https://terrapower.github.io/armi/tutorials/walkthrough_lwr_inputs.html

That would make the various pin positions completely unambiguous, and the linkage relationships should be completely obvious.

@albeanth @jakehader @onufer

@keckler
Copy link
Member Author

keckler commented May 23, 2023

@albeanth are there any thoughts on this? I am getting to a point where this is actively blocking, so I'm hoping we can make an effort to resolve this. More than willing to help here.

@keckler
Copy link
Member Author

keckler commented May 23, 2023

The nice thing about the option that I mentioned above (to base the linking off of a block's grid) is that blocks that look "normal" already get a grid auto-defined when the block is instantiated:

armi/armi/reactor/blocks.py

Lines 2054 to 2115 in 7603143

def autoCreateSpatialGrids(self):
"""
Given a block without a spatialGrid, create a spatialGrid and give its children
the corresponding spatialLocators (if it is a simple block).
In this case, a simple block would be one that has either multiplicity of
components equal 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.
Notes
-----
If the block meets all the conditions, we gather all components to either be a multiIndexLocation containing all
of the pin positions, otherwise, locator is the center (0,0).
Also, this only works on blocks that have 'flat side up'.
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...
mults = {c.getDimension("mult") for c in self.iterComponents()}
if len(mults) != 2 or 1 not in mults:
raise ValueError(
"Could not create a spatialGrid for block {}, multiplicities are not 1 or N they are {}".format(
self.p.type, mults
)
)
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)
grid = grids.HexGrid.fromPitch(
self.getPinPitch(cold=True), numRings=0, pointedEndUp=True
)
spatialLocators = grids.MultiIndexLocation(grid=self.spatialGrid)
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(
numLocations, self.getNumPins()
)
)
i = 0
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])
if self.spatialGrid is None:
self.spatialGrid = grid
for c in self:
if c.getDimension("mult") > 1:
c.spatialLocator = spatialLocators
elif c.getDimension("mult") == 1:
c.spatialLocator = grids.CoordinateLocation(0.0, 0.0, 0.0, grid)

@jakehader
Copy link
Member

Thanks for pinging on this again @keckler. @albeanth let's set up a discussion internally about how to handle this and maybe make a simple use case that can serve as a unit test. I feel like grids would be a natural way to connect components together but I have to admit that I haven't even studied the current implementation to know if this is a good fit or if this would be a significant change.

@albeanth albeanth linked a pull request Aug 7, 2023 that will close this issue
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants