-
Notifications
You must be signed in to change notification settings - Fork 92
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
Improving getOptimalAssemblyOrientation #2019
Conversation
The algorithm goes as follows 1. Get all the pin powers and `IndexLocation`s from the block at the previous location 2. Obtain the `IndexLocation` of the pin with the highest burnup 3. For each possible rotation, - Find the new location with `HexGrid.rotateIndex` - Find the index where that location occurs in previous locations - Find the previous power at that location 4. Return the rotation with the lowest previous power This algorithm assumes a few things. 1. `len(pinLocations) == len(pinPowers)` in both cases. This may make sense, but we've found some cases where this assumption breaks. Not even edge cases, like the C5G7 LWR benchmark. 2. Your assembly has at least 60 degree symmetry of fuel pins and powers. This means if we find a fuel pin with high burnup and rotate it 60 degrees, there should be another fuel pin at that lattice site. This is mostly a safe assumption since many hexagonal reactors have at least 60 degree symmetry of fuel pin layout. This assumption holds if you have a full hexagonal lattice of fuel pins as well.
…ferent assemblies
The current test just showed the orientations were different. Which maybe isn't what we want. Instead, use a patch to show that we call the getOptimalAssemblyOrientation with the assembly of interest, and delegate the testing on the accuracy of that function to more targeted unit tests.
The previous list approach, using the index method, got a little finicky depending on how `IndexLocation.__eq__` is implemented and on who's grid the location returned from `HexGrid.rotateIndex` belongs. So now just make a dictionary mapping `{(i, j): p}` for each location in the previous block. We could go a little more optimal and only store locations if they're in the same ring as the pin with the most burnup. But then we have to pass each location through `HexGrid.getRingPos`. And, at most we have maybe a few dozen entries per ring vs maybe 200 in a full hexagon. Splitting hairs but it may be useful to keep in mind.
Why is this even here?
The previous logic of ```python try: rotationMethod() except: rotationMethod(self) ``` would lead to confusing tracebacks if the second call also produced an error.
In some testing, I found that freshly created assemblies caused problems with the burnup reducing rotation algorithm. They didn't have a useful previous location, in that their `lastLocationLabel` was `LoadQueue` since they had yet to be placed within the core. This caused problems trying to find the assembly that now exists where they used to be because the logic for breaking an assembly locator label like `001-003` breaks when that label is `LoadQueue`. The fuel handler tests now work higher up on the `FuelHandler.outage` method that invokes the `buReducingAssemblyRotation` function. In the case of the fresh fuel, we fake a case where an assembly is created fresh and dropped into the reactor without moving another assembly. `FuelHandler.moved` only really makes sense for `N>1` entries if you're swapping or disharging assemblies. But, we want to show that we skip assemblies that are coming from outside the reactor.
It seems like checking if an assembly was in the detailed assembly prior to rotation was a means to check if it had pin power. But the workflow for adding detailed assemblies seems less widespread compared to setting pin powers. So, we'll first check any block in the assembly being rotated has pin power prior to the detailed assembly check. Check that the current assembly, `aNow`, has burnup defined on pins. Additionally check that the assembly previously located here, `aPrev`, has power defined on pins. These checks are facilitated by moving checks for assembly fuel pin powers and burnups into `fuelCycle/utils.py`
These will be removed.
Make sure we iterate over the fresh assembly in `fh.moved` or else this test really has nothing to do
…ined If we rotate an assembly that is then used to determine how a different assembly should be rotated, we've poisoned our check. We want to compare against the state of the assembly that was in the core when the power was produced. Consider: 1. Assembly A is shuffled to a new location and then rotated. 2. Assembly B is shuffled to the previous location of assembly A. The power profile that is used to determine the rotation of assembly B is based on a rotated assembly A. This is not ideal because we want to compare against the power profile assembly B is expected to experience. And by rotating assembly A before this check, we've introduced a bias into the check. Now, the `buReducingAssemblyRotation` will store the number of rotations for each assembly first, then rotate all assemblies at once. Additionally, any assembly that is found to have a zero rotation is not going to be rotated. Calling `Assembly.rotate(0)` is a no-op that we don't need to consider. These no-rotations will also be excluded from the counting of rotations.
Reverted the dropped and replaced pin burnup parameters because they were more entangled than I thought, and they are a want not a need right now. |
@drewj-tp Are you hoping this PR will make it into the (imminent) ARMI 0.5.0 release? |
…pin-dep * origin/main: Moving from shutil.copy to safeCopy (#2024) Move files from temporary directory changer (#2022) Handling checking for OSs better (#2023) Expose some skip inspection options for `armi.init` and `db.loadOperator` (#2005) Adding setting to control MCNP / ENDF library (#1989) Improving logging on ISOTXS compare (#2013)
Yes. It is my understanding that this is a need for the release based on user needs |
Avoid people importing it into their code. Hopefully. IDK
raise ValueError(msg) | ||
maxBuPinLocation = maxBurnupLocator(maxBuBlock) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
raise ValueError(msg) | |
maxBuPinLocation = maxBurnupLocator(maxBuBlock) | |
raise ValueError(msg) | |
# 2. Obtain the ``IndexLocation`` of the pin with the highest burnup in the current assembly. | |
maxBuPinLocation = maxBurnupLocator(maxBuBlock) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't have to do this exactly, but there is a lot of stuff happening in this function, so I am just trying to make it a touch easier to read.
Your call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's my call, I'm going to forgo this and similar requests.
I get readability through comments. I've also seen comments rot as the code changes while comments are not updated accordingly. So I'm inclined to have more verbose variable names and functions over comments. To hopefully help explain things.
maxBuPinLocation
feels to me like a thing that tells us where the pin with the highest burnup can be found
It's very much a personal stance of mine that, if I'm left to decide, I'm going to lean on fewer comments but more descriptive docstrings and verbose variables/functions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, that's fine.
That's not the M.O. for ARMI though. I don't particularly feel that docstrings are less likely to go out of date than comments. And without your docstring, this function's logic would be too complicated for someone to read at a glance. That's really my problem, I don't want some poor engineer to come to this function in 3 years and have to puzzle out this important logic on their own.
To that end, the docstring can stand as sufficient.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks good.
Thanks so much for all the hard work on the testing!
I have some thoughts for you, but none on the basic logic. That seems solid to me.
Co-authored-by: John Stilley <[email protected]>
What is the change?
The algorithm goes as follows
IndexLocation
s from the block at the previous location (previous timestep)IndexLocation
of the pin with the highest burnupHexGrid.rotateIndex
This algorithm assumes a few things.
len(pinLocations) == len(pinPowers)
in both cases. This may make sense, but we've found some cases where this assumption breaks. Not even edge cases, like the C5G7 LWR benchmark.a
have similar locations inaPrev
. This is mostly a safe assumption in that most fuel assemblies have similar layouts so it's plausible that ifa
has a fuel pin at(1, 0, 0)
, so doesaPrev
.Why is the change being made?
The
getOptimalAssemblyOrientation
used to rely on things that either don't exist or don't get updated anymore. LikeBlock.p.pinLocations
. And it's testing was subpar: it just checked that the new rotation was different, not correct.armi/armi/physics/fuelCycle/tests/test_assemblyRotationAlgorithms.py
Lines 30 to 50 in f1f3dbe
Additionally, we now have a pathway for plugins defining pin-level burnup as
Component.p.pinPercentBu
over block-level parameters.Closes #1921
Other noteworthy changes
Don't rotate assemblies until all assembly rotations have been determined
If we rotate an assembly that is then used to determine how a different assembly should be rotated, we've poisoned our check. We want to compare against the state of the assembly that was in the core when the power was produced.
Consider:
The power profile that is used to determine the rotation of assembly B is based on a rotated assembly A. This is not ideal because we want to compare against the power profile assembly B is expected to experience. And by rotating assembly A before this check, we've introduced a bias into the check.
Now, the
buReducingAssemblyRotation
will store the number of rotations for each assembly first, then rotate all assemblies at once.Additionally, any assembly that is found to have a zero rotation is not going to be rotated. Calling
Assembly.rotate(0)
is a no-op that we don't need to consider. These no-rotations will also be excluded from the counting of rotations.Improved handling of fresh assemblies
a0bb7ee
In some testing, I found that freshly created assemblies caused problems with the burnup reducing rotation algorithm. They didn't have a useful previous location, in that their
lastLocationLabel
wasLoadQueue
since they had yet to be placed within the core. This caused problems trying to find the assembly that now exists where they used to be because the logic for breaking an assembly locator label like001-003
breaks when that label isLoadQueue
.The fuel handler tests now work higher up on the
FuelHandler.outage
method that invokes thebuReducingAssemblyRotation
function.In the case of the fresh fuel, we fake a case where an assembly is created fresh and dropped into the reactor without moving another assembly.
FuelHandler.moved
only really makes sense forN>1
entries if you're swapping or disharging assemblies. But, we want to show that we skip assemblies that are coming from outside the reactor.Testing testing testing
Ensure that we get the correct rotation, not just a different rotation.
Adds a test where we involve three assemblies
A -> B -> C
mimicking a real outage event. This test is important to make sure we aren't rotating assemblies that we need later.Heavy refactor to make the tests a bit easier to read (hopefully) by adding a few things
Personal TODO
Checklist
doc
folder.pyproject.toml
.