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

Provide better composite iteration methods #2031

Open
wants to merge 27 commits into
base: main
Choose a base branch
from

Conversation

drewj-tp
Copy link
Contributor

@drewj-tp drewj-tp commented Dec 11, 2024

What is the change?

The high-level idea is to provide ways for users and developers to traverse the composite tree without using lists. For example, calling

for c in r.getChildren(deep=True):

first traverses the entire tree and populates a list of all composites in the reactor tree. Then, it returns that list to the caller for iteration.

This PR adds the following Composite methods:

  • iterChildren
  • iterChildrenWithFlags
  • iterChildrenOfType
  • iterChildrenWithMaterials
    • I hate this one because it's so specific I don't see end users using it often.
    • But it's vital for MPI distribution that we should have a robust mechanism for it.
    • But it's only used in a few places that I don't think it justifies being a Composite method, but I can't easily think of a better place for it to live
    • 🤷

This PR adds the following specific methods

  • Assembly.iterBlocks

The various get* methods still return lists of things for backwards compatibility, but rely on the iter* methods. Ideally Composite.getChildren would just be list(self.getChildren(...)) but we need to support Composite.getChildren(..., includeMaterials=True) so that's some additional complexity.

In a few places where we use the get* methods for once-through iteration, those are replaced with iter*. So our example above would instead look lik

for c in r.iterChildren(deep=True):

There are still some use cases for making a list, especially if you want to iterate through the thing twice. Something like

objs = r.iterChildren(deep=True)
for o in objs:
    # do something on the first pass
second = list(objs)

produces an empty list for second because the iterator objs is exhausted in the loop.

Why is the change being made?

Efficiency, both time (remove redundant traversal over tree) and space (remove list of children)

Closes #1915


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.

@drewj-tp drewj-tp added the low priority Style points and non-features label Dec 11, 2024
@drewj-tp drewj-tp self-assigned this Dec 11, 2024
@drewj-tp drewj-tp force-pushed the drewj/composite-iter/1915 branch from 02785b6 to ab3fdf2 Compare December 11, 2024 17:21
@john-science
Copy link
Member

This feature is definitely big enough for a release note. Since we are planning this PR for just post-release, it should go under 0.5.1 "API Changes":

ARMI v0.5.1
===========
Release Date: TBD
New Features
------------
#. Move instead of copy files from TemporaryDirectoryChanger. (`PR#2022 <https://github.com/terrapower/armi/pull/2022>`_)
API Changes
-----------
#. TBD

@john-science
Copy link
Member

Okay, on first pass I like this. It seems straight forward, and I don't see any problems.

I'm going to hold off until Monday to give it a "final review", and hopefully merge it. #fingers-crossed

Nice work!

Produces an iterable over all children that match the flag spec. Composite.getChildrenWithFlags
throws these into a list so the user API is the same.

Test added showing it does what we expect it to do.

Replaced one usage of iteration over the list flavor with the iterable variant.
Rather than generate a potentially large list just to use the first element,
try to advance the iterator. If it raises a StopIteration error, we don't have any blocks that match
the spec.
I cannot find any uses of this method outside of the one method where it is tested
Calling SFP.getChildren with a flags argument should really be
SFP.getChildrenWithFlags
Rather than checking just the length of things, compare we get the
objects we expect
Came up in some debugging that this isn't totally tested. Turns out a
couple places override this and that was causing my problem. But a test
is still good to add!
This should still pass on main. But I added it during some debugging
because we didn't have a test just on this method.

Tests are still good though
Produces an iterator of the children rather than a list of all children.
Does not support the `includeMaterials` option
Special handling for `includeMaterials=True`
Don't need `getChildren` either because `Composite.getChildren` just
needs `Composite.__iter__` to be implemented for the traversal.
A lot of places have the pattern
```python
for child in self.getChildren()
```
that isn't necessary. If you want to iterate over the children of a
composite, you can use `Composite.__iter__` for natural iteration.
We just need a list of assemblies. `list(SFP)` will do that for us
@drewj-tp drewj-tp force-pushed the drewj/composite-iter/1915 branch from 9249016 to 7252468 Compare December 13, 2024 22:50
@jakehader
Copy link
Member

jakehader commented Dec 16, 2024

@drewj-tp - are there any immediate performance gains from this change that you can share in memory and/or speed? I like the idea of iterators, but I'm curious from a macroscopic perspective of managing the entire reactor how this propagates.

Thanks!

@drewj-tp
Copy link
Contributor Author

drewj-tp commented Dec 16, 2024

@drewj-tp - are there any immediate performance gains from this change that you can share in memory and/or speed? I like the idea of iterators, but I'm curious from a macroscopic perspective of managing the entire reactor how this propagates.

Thanks!

Good questions. I ran some tests on the FFTF model and, when traversing the entire reactor tree:

  • 30x memory reduction
  • 35% speed up

We'll still see some benefit if users don't migrate to the new iterChildren and still have large loops over r.getChildren(). With the new implementation, where getChildren(...) is basically list(c.iterChildren(...)), we get 18x memory reduction. The speed up is negligible and/or in the noise.

Note To see the real changes, I reverted the changes to Composite.getChildren to not use Composite.iterChildren. If you just checkout this branch, you'll likely see closer to the 18x memory reduction and negligible speed up.

Memory
import pathlib
import pickle

import armi

import tracemalloc

armi.configure()

pickleJar = pathlib.Path("fftf.pkl")
if pickleJar.exists():
    REACTOR = pickle.loads(pickleJar.read_bytes())
else:
    REACTOR = armi.init(fName="fftf/FFTF.yaml").r
    pickleJar.write_bytes(pickle.dumps(REACTOR))

tracemalloc.start()

for _ in REACTOR.getChildren(deep=True):
    pass

firstSize, firstPeak = tracemalloc.get_traced_memory()

tracemalloc.reset_peak()

for _ in REACTOR.iterChildren(deep=True):
    pass

secondSize, secondPeak = tracemalloc.get_traced_memory()


print(f"{firstSize=} :: {firstPeak=}")
print(f"{secondSize=} :: {secondPeak=}")
print(f"{firstSize/secondSize=} :: {firstPeak/secondPeak=}")
Time
import pathlib
import pickle

import armi

import timeit

armi.configure()

pickleJar = pathlib.Path("fftf.pkl")
if pickleJar.exists():
    REACTOR = pickle.loads(pickleJar.read_bytes())
else:
    REACTOR = armi.init(fName="fftf/FFTF.yaml").r
    pickleJar.write_bytes(pickle.dumps(REACTOR))


def getChildren(r):
    list(r.getChildren(deep=True))

def iterChildren(r):
    list(r.iterChildren(deep=True))

N = 500

FAKE_GLOBALS = locals()

tGet = timeit.timeit("getChildren(REACTOR)", number=N, globals=FAKE_GLOBALS) / N

tIter = timeit.timeit("iterChildren(REACTOR)", number=N, globals=FAKE_GLOBALS) / N

print(f"{tGet=}")
print(f"{tIter=}")
print(f"{tIter/tGet=}")

@john-science
Copy link
Member

  • 30x memory reduction
  • 35% speed up

That's awesome!

I would always argue strongly that you will get more performance improvements if/when we elbow the downstream repositories to start using these new tools.

Hell, we could grep around and open the PRs ourselves.

Just sayin.

@drewj-tp
Copy link
Contributor Author

I would always argue strongly that you will get more performance improvements if/when we elbow the downstream repositories to start using these new tools.

Hell, we could grep around and open the PRs ourselves

I have a follow on PR here that will help and then I'll start pinging projects on improvements

@john-science
Copy link
Member

@drewj-tp Love it!

I have a few small questions for you, at your leisure.

But I love the feature. Assuming your downstream testing suite passes, I'm convinced.

Once we have `ArmiObject.iterChildren` specified on `ArmiObject`, we can
use that to build more base functionality:

- `ArmiObject.iterChildrenWithFlags`
- `ArmiObject.getChildrenWithFlags`
- `ArmiObject.iterChildrenOfType`
- `ArmiObject.getChildrenOfType`

Additionally, I noted that `ArmiObject.getChildrenWithFlags` had a
different default `exactMatch=True` than
`Composite.getChildrenWithFlags`. Both now default to `exactMatch=False`
for consistency.
@drewj-tp
Copy link
Contributor Author

Assuming your downstream testing suite passes, I'm convinced

Internal tests are passing!

@@ -1191,7 +1204,11 @@ def countBlocksWithFlags(self, blockTypeSpec=None):
blockCounter : int
number of blocks of this type
"""
return len(self.getBlocks(blockTypeSpec))
if blockTypeSpec is None:
Copy link
Member

Choose a reason for hiding this comment

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

Not your fault, but can we fix the docstring in this method? It says:

Returns the block at a specified axial dimension elevation (given in cm).

But it looks like it should say:

Returns the number of blocks at a specified axial dimension elevation (given in cm).

Also, we can just delete this text from the docstring:

Used as a way to determine what block the control rod will be modifying with a mergeBlocks.

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.

I love it!

Great change.

We'll merge when we've looked at downstream testing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
low priority Style points and non-features
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Provide more Composite.iter* methods
3 participants