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

Improving getOptimalAssemblyOrientation #2019

Merged
merged 50 commits into from
Dec 2, 2024

Conversation

drewj-tp
Copy link
Contributor

@drewj-tp drewj-tp commented Nov 22, 2024

What is the change?

The algorithm goes as follows

  1. Get all the pin powers and IndexLocations from the block at the previous location (previous timestep)
  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 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.
  3. Fuel pins in a have similar locations in aPrev. This is mostly a safe assumption in that most fuel assemblies have similar layouts so it's plausible that if a has a fuel pin at (1, 0, 0), so does aPrev.

Why is the change being made?

The getOptimalAssemblyOrientation used to rely on things that either don't exist or don't get updated anymore. Like Block.p.pinLocations. And it's testing was subpar: it just checked that the new rotation was different, not correct.

class TestFuelHandlerMgmtTools(FuelHandlerTestHelper):
def test_buReducingAssemblyRotation(self):
fh = fuelHandlers.FuelHandler(self.o)
hist = self.o.getInterface("history")
newSettings = {CONF_ASSEM_ROTATION_STATIONARY: True}
self.o.cs = self.o.cs.modified(newSettings=newSettings)
assem = self.o.r.core.getFirstAssembly(Flags.FUEL)
# apply dummy pin-level data to allow intelligent rotation
for b in assem.getBlocks(Flags.FUEL):
b.breakFuelComponentsIntoIndividuals()
b.initializePinLocations()
b.p.percentBuMaxPinLocation = 10
b.p.percentBuMax = 5
b.p.linPowByPin = list(reversed(range(b.getNumPins())))
addSomeDetailAssemblies(hist, [assem])
rotNum = b.getRotationNum()
rotAlgos.buReducingAssemblyRotation(fh)
self.assertNotEqual(b.getRotationNum(), rotNum)

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:

  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.

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 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.

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

  1. PinLocations enumeration to de-mystify some magic pin locations.
  2. Helper methods for getting burnup vector with some known max location and pin power vector with known min locaiton
  3. Comparison of the rotation called on an assembly with the integer number of rotations we know from the test

Personal TODO

  • Remove debug statements once I've re-tested against new changes
  • Release notes for new features and removed / renamed parameters

Checklist

  • The release notes have been updated if necessary.
  • The documentation is still up-to-date in the doc folder.
  • The dependencies are still up-to-date in pyproject.toml.

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.
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.
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`
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.
@drewj-tp
Copy link
Contributor Author

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.

…pin-dep

* origin/main:
  Improving calculation for total job memory (#2018)
  Cleaning out unnecessary uses of r.o (#1999)
@john-science
Copy link
Member

@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)
@drewj-tp
Copy link
Contributor Author

@drewj-tp Are you hoping this PR will make it into the (imminent) ARMI 0.5.0 release?

Yes. It is my understanding that this is a need for the release based on user needs

@drewj-tp drewj-tp marked this pull request as ready for review December 2, 2024 16:10
@drewj-tp drewj-tp requested a review from john-science December 2, 2024 16:10
Avoid people importing it into their code. Hopefully. IDK
Comment on lines +94 to +95
raise ValueError(msg)
maxBuPinLocation = maxBurnupLocator(maxBuBlock)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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)

Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Member

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.

doc/release/0.5.rst Outdated Show resolved Hide resolved
Copy link
Member

@john-science john-science left a 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.

@john-science john-science removed the request for review from HunterPSmith December 2, 2024 21:17
@john-science john-science merged commit 7ef5651 into main Dec 2, 2024
22 checks passed
@john-science john-science deleted the drewj/bu-rotate-with-pin-dep branch December 2, 2024 21:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Insufficient testing on buReducingAssemblyRotation
2 participants