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

Extending axial expansion classes #1920

Merged
merged 46 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
56b81fb
Flatten and rename determine axial linkage function
drewj-tp Sep 23, 2024
6316e67
Provide axialAssemblyLinkage.AxialLink for improved readability
drewj-tp Sep 23, 2024
727d04d
Only warn about missing linked components if no linked block
drewj-tp Sep 24, 2024
4b9f67d
Streamline check for component axial linkage a bit more
drewj-tp Sep 27, 2024
9e0550d
Add type hints for ExpansionData methods
drewj-tp Sep 27, 2024
bfbdc46
Combine check for zero and negative expansion factors
drewj-tp Sep 27, 2024
006b6fb
Refactor ExpansionData.computeThermalExpansionFactors
drewj-tp Sep 27, 2024
00ce21e
Provide axialExpansionChanger.iterSolidComponents
drewj-tp Sep 27, 2024
fa283cc
Update ExpansionData docstring
drewj-tp Sep 27, 2024
81d39c3
ExpansionData.determineTargetComponent returns target
drewj-tp Sep 27, 2024
9a90311
Add some type hints for AxialExpansionChanger
drewj-tp Sep 27, 2024
319e474
Remove type name from componentLst in favor of components
drewj-tp Sep 27, 2024
2ddda07
Provide and use AssemblyAxialLinkage.areAxiallyLinked
drewj-tp Sep 30, 2024
d93b537
Skip setting target component for dummy block axial expansion
drewj-tp Sep 30, 2024
11352a4
Update ExpansionData.determineTargetComponent docstring
drewj-tp Sep 30, 2024
3a785b6
Use _setExpansionTarget in _isFuelLocked
drewj-tp Sep 30, 2024
5d39876
Release notes
drewj-tp Oct 1, 2024
71f0cb7
Merge remote-tracking branch 'origin/main' into drewj/improve-assem-a…
drewj-tp Oct 1, 2024
d4080bc
Simplify type hints for AssemblyAxialLinkage block and component links
drewj-tp Oct 1, 2024
2dfd9bc
Provide AssemblyAxialLinkage.getLinkedBlocks
drewj-tp Oct 1, 2024
0ca94ce
Refactor AssemblyAxialLinkage._getLinkedComponents
drewj-tp Oct 1, 2024
d6b5d92
Update armi/reactor/converters/axialExpansionChanger/assemblyAxialLin…
drewj-tp Oct 4, 2024
6e8337d
Move type-check guarded import of assembly
drewj-tp Oct 4, 2024
920872e
Apply suggestions from code review
drewj-tp Oct 7, 2024
c082768
Additional call to setExpansionTarget if we've found the target before
drewj-tp Oct 7, 2024
5d9fb81
Update comment on dummy components not needing target comps
drewj-tp Oct 7, 2024
f89367e
Update ExpansionData.determineTargetComponent docstring
drewj-tp Oct 7, 2024
a8645a0
Use AxialLink API in axiallyExpandAssembly
drewj-tp Oct 7, 2024
46a3f55
Apply suggestions from code review
drewj-tp Oct 7, 2024
9b57c06
Merge remote-tracking branch 'origin/drewj/improve-assem-axial-linkag…
drewj-tp Oct 7, 2024
e1ccf0e
Point AssemblyAxialLinkage.areAxiallyLinked doc to the function for l…
drewj-tp Oct 7, 2024
74fcd30
Require axially linked components to have identical types
drewj-tp Oct 7, 2024
1cc156a
Remove getitem, setitem, iter on AxialLink
drewj-tp Oct 8, 2024
f02fca2
Merge remote-tracking branch 'origin/main' into drewj/improve-assem-a…
drewj-tp Oct 8, 2024
dbea10c
Merge branch 'main' into drewj/improve-assem-axial-linkage
john-science Oct 9, 2024
142d814
Merge remote-tracking branch 'origin/main' into drewj/improve-assem-a…
drewj-tp Oct 11, 2024
2228880
Merge remote-tracking branch 'origin/main' into drewj/improve-assem-a…
drewj-tp Oct 11, 2024
df8ad61
Update armi/reactor/converters/tests/test_axialExpansionChanger.py
drewj-tp Oct 11, 2024
7f4c8ff
Black
drewj-tp Oct 11, 2024
83ee423
Merge remote-tracking branch 'refs/remotes/origin/drewj/improve-assem…
drewj-tp Oct 11, 2024
d925b98
Merge remote-tracking branch 'origin/main' into drewj/improve-assem-a…
drewj-tp Oct 16, 2024
d9af963
Merge remote-tracking branch 'origin/main' into drewj/improve-assem-a…
drewj-tp Oct 23, 2024
ab9b61b
Merge branch 'main' into drewj/improve-assem-axial-linkage
drewj-tp Oct 23, 2024
e2dc917
Merge remote-tracking branch 'origin/main' into drewj/improve-assem-a…
drewj-tp Oct 24, 2024
71c7497
Merge remote-tracking branch 'origin/main' into drewj/improve-assem-a…
drewj-tp Oct 24, 2024
038b75e
Merge branch 'main' into drewj/improve-assem-axial-linkage
drewj-tp Oct 29, 2024
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
1 change: 1 addition & 0 deletions armi/reactor/converters/axialExpansionChanger/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@
from armi.reactor.converters.axialExpansionChanger.expansionData import ExpansionData
from armi.reactor.converters.axialExpansionChanger.expansionData import (
getSolidComponents,
iterSolidComponents,
)
282 changes: 171 additions & 111 deletions armi/reactor/converters/axialExpansionChanger/assemblyAxialLinkage.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,34 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import typing
import dataclasses
import functools
import itertools

from armi import runLog
from armi.reactor.components import UnshapedComponent
from armi.reactor.blocks import Block
from armi.reactor.components import Component, UnshapedComponent
from armi.reactor.converters.axialExpansionChanger.expansionData import (
getSolidComponents,
iterSolidComponents,
)


def _determineLinked(componentA, componentB):
if typing.TYPE_CHECKING:
from armi.reactor.assemblies import Assembly


def areAxiallyLinked(componentA: Component, componentB: Component) -> bool:
"""Determine axial component linkage for two components.

Components are considered linked if the following are found to be true:

1. Both contain solid materials.
2. They have identical types (e.g., ``Circle``).
3. Their multiplicities are the same.
4. The biggest inner bounding diameter of the two is less than the smallest outer
bounding diameter of the two.

Parameters
----------
componentA : :py:class:`Component <armi.reactor.components.component.Component>`
Expand All @@ -33,8 +51,6 @@ def _determineLinked(componentA, componentB):
-----
- 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.
Expand All @@ -46,7 +62,7 @@ def _determineLinked(componentA, componentB):
"""
if (
(componentA.containsSolidMaterial() and componentB.containsSolidMaterial())
and isinstance(componentA, type(componentB))
and type(componentA) is type(componentB)
and (componentA.getDimension("mult") == componentB.getDimension("mult"))
):
if isinstance(componentA, UnshapedComponent):
Expand All @@ -57,118 +73,151 @@ def _determineLinked(componentA, componentB):
"they are going to be assumed to not be linked.",
single=True,
)
linked = False
else:
idA, odA = (
componentA.getCircleInnerDiameter(cold=True),
componentA.getBoundingCircleOuterDiameter(cold=True),
)
idB, odB = (
componentB.getCircleInnerDiameter(cold=True),
componentB.getBoundingCircleOuterDiameter(cold=True),
)
return False
# Check if one component could fit within the other
idA = componentA.getCircleInnerDiameter(cold=True)
odA = componentA.getBoundingCircleOuterDiameter(cold=True)
idB = componentB.getCircleInnerDiameter(cold=True)
odB = componentB.getBoundingCircleOuterDiameter(cold=True)
biggerID = max(idA, idB)
smallerOD = min(odA, odB)
return biggerID < smallerOD
return False


# Make a generic type so we can "template" the axial link class based on what could be above/below a thing
Comp = typing.TypeVar("Comp", Block, Component)
albeanth marked this conversation as resolved.
Show resolved Hide resolved

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
@dataclasses.dataclass
class AxialLink(typing.Generic[Comp]):
"""Small class for named references to objects above and below a specific object.

return linked
Axial expansion in ARMI works by identifying what objects occupy the same axial space.
For components in blocks, identify which above and below axially align. This is used
to determine what, if any, mass needs to be re-assigned across blocks during expansion.
For blocks, the linking determines what blocks need to move as a result of a specific block's
axial expansion.

Attributes
----------
lower : Composite or None
Object below, if any.
upper : Composite or None
Object above, if any.

Notes
-----
This class is "templated" by the type of composite that could be assigned and fetched. A
block-to-block linkage could be type-hinted via ``AxialLink[Block]`` or ``AxialLink[Component]``
for component-to-component link.

See Also
--------
* :attr:`AxialAssemblyLinkage.linkedBlocks`
* :attr:`AxialAssemblyLinkage.linkedComponents`
"""

lower: typing.Optional[Comp] = dataclasses.field(default=None)
upper: typing.Optional[Comp] = dataclasses.field(default=None)


class AssemblyAxialLinkage:
"""Determines and stores the block- and component-wise axial linkage for an assembly.

Parameters
----------
assem : armi.reactor.assemblies.Assembly
Assembly to be linked

Attributes
----------
a : :py:class:`Assembly <armi.reactor.assemblies.Assembly>`
reference to original assembly; is directly modified/changed during expansion.
linkedBlocks : dict
- keys = :py:class:`Block <armi.reactor.blocks.Block>`
- values = list of axially linked blocks; index 0 = lower linked block; index 1: upper
linked block.
Keys are blocks in the assembly. Their values are :class:`AxialLink` with
``upper`` and ``lower`` attributes for the blocks potentially above and
below this block.
linkedComponents : dict
- keys = :py:class:`Component <armi.reactor.components.component.Component>`
- values = list of axially linked components; index 0 = lower linked component;
index 1: upper linked component.
Keys are solid components in the assembly. Their values are :class:`AxialLink` with
``upper`` and ``lower`` attributes for the solid components potentially above and
below this block.
"""

def __init__(self, StdAssem):
self.a = StdAssem
self.linkedBlocks = {}
linkedBlocks: dict[Block, AxialLink[Block]]
linkedComponents: dict[Component, AxialLink[Component]]

def __init__(self, assem: "Assembly"):
self.a = assem
self.linkedBlocks = self.getLinkedBlocks(assem)
self.linkedComponents = {}
self._determineAxialLinkage()

@classmethod
def getLinkedBlocks(
cls,
blocks: typing.Sequence[Block],
) -> dict[Block, AxialLink[Block]]:
"""Produce a mapping showing how blocks are linked.

Parameters
----------
blocks : sequence of armi.reactor.blocks.Block
Ordered sequence of blocks from bottom to top. Could just as easily be an
:class:`armi.reactor.assemblies.Assembly`.

Returns
-------
dict[Block, AxialLink[Block]]
Dictionary where keys are individual blocks and their corresponding values point
to blocks above and below.
"""
nBlocks = len(blocks)
if nBlocks:
return cls._getLinkedBlocks(blocks, nBlocks)
raise ValueError("No blocks passed. Cannot determine links")

@staticmethod
def _getLinkedBlocks(
blocks: typing.Sequence[Block], nBlocks: int
) -> dict[Block, AxialLink[Block]]:
# Use islice to avoid making intermediate lists of subsequences of blocks
lower = itertools.chain((None,), itertools.islice(blocks, 0, nBlocks - 1))
upper = itertools.chain(itertools.islice(blocks, 1, None), (None,))
albeanth marked this conversation as resolved.
Show resolved Hide resolved
links = {}
for low, mid, high in zip(lower, blocks, upper):
links[mid] = AxialLink(lower=low, upper=high)
albeanth marked this conversation as resolved.
Show resolved Hide resolved
return links

def _determineAxialLinkage(self):
"""Gets the block and component based linkage."""
for b in self.a:
self._getLinkedBlocks(b)
for c in getSolidComponents(b):
for c in iterSolidComponents(b):
self._getLinkedComponents(b, c)

def _getLinkedBlocks(self, b):
"""Retrieve the axial linkage for block b.

Parameters
----------
b : :py:class:`Block <armi.reactor.blocks.Block>`
block to determine axial linkage for

Notes
-----
- block linkage is determined by matching ztop/zbottom (see below)
- block linkage is stored in self.linkedBlocks[b]
_ _
| |
| 2 | Block 2 is linked to block 1.
|_ _|
| |
| 1 | Block 1 is linked to both block 0 and 1.
|_ _|
| |
| 0 | Block 0 is linked to block 1.
|_ _|
"""
lowerLinkedBlock = None
upperLinkedBlock = None
block_list = self.a.getChildren()
for otherBlk in block_list:
if b.name != otherBlk.name:
if b.p.zbottom == otherBlk.p.ztop:
lowerLinkedBlock = otherBlk
elif b.p.ztop == otherBlk.p.zbottom:
upperLinkedBlock = otherBlk

self.linkedBlocks[b] = [lowerLinkedBlock, upperLinkedBlock]

if lowerLinkedBlock is None:
runLog.debug(
"Assembly {0:22s} at location {1:22s}, Block {2:22s}"
"is not linked to a block below!".format(
str(self.a.getName()),
str(self.a.getLocation()),
str(b.p.flags),
),
single=True,
)
if upperLinkedBlock is None:
runLog.debug(
"Assembly {0:22s} at location {1:22s}, Block {2:22s}"
"is not linked to a block above!".format(
str(self.a.getName()),
str(self.a.getLocation()),
str(b.p.flags),
),
single=True,
)
def _findComponentLinkedTo(
self, c: Component, otherBlock: typing.Optional[Block]
) -> typing.Optional[Component]:
if otherBlock is None:
return None
candidate = None
# Iterate over all solid components in the other block that are linked to this one
areLinked = functools.partial(self.areAxiallyLinked, c)
albeanth marked this conversation as resolved.
Show resolved Hide resolved
for otherComp in filter(areLinked, iterSolidComponents(otherBlock)):
if candidate is None:
candidate = otherComp
else:
errMsg = (
"Multiple component axial linkages have been found for "
f"Component {c} in Block {c.parent} in Assembly {c.parent.parent}. "
"This is indicative of an error in the blueprints! Linked "
f"components found are {candidate} and {otherComp} in {otherBlock}"
)
runLog.error(msg=errMsg)
raise RuntimeError(errMsg)
return candidate

def _getLinkedComponents(self, b, c):
def _getLinkedComponents(self, b: Block, c: Component):
"""Retrieve the axial linkage for component c.

Parameters
Expand All @@ -183,31 +232,42 @@ def _getLinkedComponents(self, b, c):
RuntimeError
multiple candidate components are found to be axially linked to a component
"""
lstLinkedC = [None, None]
for ib, linkdBlk in enumerate(self.linkedBlocks[b]):
if linkdBlk is not None:
for otherC in getSolidComponents(linkdBlk.getChildren()):
if _determineLinked(c, otherC):
if lstLinkedC[ib] is not None:
errMsg = (
"Multiple component axial linkages have been found for "
f"Component {c}; Block {b}; Assembly {b.parent}."
" This is indicative of an error in the blueprints! Linked "
f"components found are {lstLinkedC[ib]} and {otherC}"
)
runLog.error(msg=errMsg)
raise RuntimeError(errMsg)
lstLinkedC[ib] = otherC

linkedBlocks = self.linkedBlocks[b]
lowerC = self._findComponentLinkedTo(c, linkedBlocks.lower)
upperC = self._findComponentLinkedTo(c, linkedBlocks.upper)
lstLinkedC = AxialLink(lowerC, upperC)
self.linkedComponents[c] = lstLinkedC

if lstLinkedC[0] is None:
if self.linkedBlocks[b].lower is None and lstLinkedC.lower is None:
runLog.debug(
f"Assembly {self.a}, Block {b}, Component {c} has nothing linked below it!",
single=True,
)
if lstLinkedC[1] is None:
if self.linkedBlocks[b].upper is None and lstLinkedC.upper is None:
runLog.debug(
f"Assembly {self.a}, Block {b}, Component {c} has nothing linked above it!",
single=True,
)

@staticmethod
def areAxiallyLinked(componentA: Component, componentB: Component) -> bool:
"""Check if two components are axially linked.
drewj-tp marked this conversation as resolved.
Show resolved Hide resolved

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

Returns
-------
bool
Status of linkage check

See Also
--------
:func:`areAxiallyLinked` for more details, including the criteria for considering components linked.
This method is provided to allow subclasses the ability to override the linkage check.
"""
return areAxiallyLinked(componentA, componentB)
Loading
Loading