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

Adding methods to automatically apply results to existing Tally objects. #2671

Open
wants to merge 23 commits into
base: develop
Choose a base branch
from

Conversation

pshriwise
Copy link
Contributor

@pshriwise pshriwise commented Aug 28, 2023

Description

One thing I've always found a bit clunky when working with our simulation results is the process of extracting user-specified tally results from the statepoint file. It seems to me that finding a match is left to the user, but it should be possible to automate that in the majority of cases.

The idea here is that we can now apply results from our statepoint file to the original tally objects created as inputs to the simulation. This simplifies situations like:

tally = openmc.Tally()
model.tallies = [tally]
# further tally setup here

sp_file = model.run()
with openmc.StatePoint(sp_file) as sp:
    sp_tally = sp.get_tally(id=tally.id)

# do result postprocessing here with the new sp_tally object, two objects to keep track of

to

tally = openmc.Tally()
model.tallies = [tally]
# further tally setup here

sp_file = model.run()
tally.add_results(sp_file)

# only one tally object to worry about 
# do result postprocessing here with original tally object

or even

tally = openmc.Tally()
model.tallies = [tally]
# further tally setup here

sp_file = model.run(apply_tally_results=True)

# do result postprocessing here with original tally

Keeping this as a draft for now as I'm kind of workshopping it. But I'll take care of the following dev tasks soon if there aren't any objections to heading this direction.

  • I have performed a self-review of my own code
  • I have followed the style guidelines for Python source files (if applicable)
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works (if applicable)

@pshriwise pshriwise marked this pull request as draft August 28, 2023 19:15
@pshriwise pshriwise marked this pull request as ready for review April 10, 2024 09:22
Copy link
Contributor

@MicahGale MicahGale left a comment

Choose a reason for hiding this comment

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

I like this feature. My one concern is that right now this PR drops coverage.

openmc/statepoint.py Outdated Show resolved Hide resolved
openmc/statepoint.py Outdated Show resolved Hide resolved
@pshriwise
Copy link
Contributor Author

After a good amount of massaging related to the Tally.estimator and Tally object equivalence I came to the following conclusion:

When the Tally.estimator attribute is None this is effectively an indication that OpenMC should select an appropriate estimator for the filters and tallies applied. (This is often the case from user tally generation code I've seen). At runtime, OpenMC will select and apply an appropriate estimator with a preference for the tracklength estimator if allowed. This estimator is written to the state point file explicitly, as it should be, but it makes matching the original, "input" Tally object to the Tally object in the StatePoint object less straightforward.

I've updated the documentation about the Tally.estimator attribute to reflect this understanding and the Tally.__eq__ method allows a match between tallies if either of the Tally.estimator attributes are set to None.

With that, I think I'm pretty happy with the state of this pending further review or something I've missed (impossible); I'll be looking forward to updating tutorials/notebooks to use this feature as I think it will make setup and post-processing in a single script/notebook more intuitive.

@MicahGale @paulromano @jtramm I'd be curious to hear your thoughts in particular!

@pshriwise pshriwise self-assigned this Sep 20, 2024
…ifferent than an explicity specified estimator
…n Tally.__eq__.

The default value of None for the `Tally.estimator` attribute allows the C++ code to provide additional warnings/errors if a user explicitly specifies an estimator that is incompatible with the tally filters/scores. When this value is None, no entry appears in the XML inputs and OpenMC is free to choose an appropriate estimator without error.
@MicahGale
Copy link
Contributor

I still need to pull this branch and play around with it a bit. My first inclination for the equality of None is: why use implicit defaults? If an explicit default were you used where it just defaulted to track length, then all the __eq__ logic would be unnecessary, and would be overall more intuitive for the users.

@pshriwise
Copy link
Contributor Author

pshriwise commented Oct 16, 2024

I still need to pull this branch and play around with it a bit. My first inclination for the equality of None is: why use implicit defaults? If an explicit default were you used where it just defaulted to track length, then all the __eq__ logic would be unnecessary, and would be overall more intuitive for the users.

Yeah I agree that an explicit default might be more clear in terms of intention, but I think the equivalence logic would mainly remain the same. We can't really rely on 'tracklength' as the default value because there are restrictions for estimators on certain scores/filters, but the C++ will try to respect an explicitly set estimator on the Tally object currently.

// Check if user specified estimator

Explicitly setting tracklength is going to cause a lot of problems in the test suite and a lot of this implicit default is already baked into that in one way or another. I actually tried this out earlier in this PR but it was clear that it's sort of its own mess to untangle if we want to change that behavior.

Another option is on the C++ side, the calls to fatal_error could be reduced to warnings with an update to the applied estimator as needed, but I prefer the status quo tbh.

@MicahGale
Copy link
Contributor

Sorry for the delay, yes that makes sense for now. Long term it would be nice it the default were automatically changed as needed, but that's beyond this scope.

@@ -644,6 +645,11 @@ def run(self, particles=None, threads=None, geometry_debug=False,
to True.

.. versionadded:: 0.13.3
apply_tally_results : bool
Whether or not to apply results of the final statepoint file to the
Copy link
Contributor

Choose a reason for hiding this comment

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

@tjlaboss would say:

Suggested change
Whether or not to apply results of the final statepoint file to the
Whether to apply results of the final statepoint file to the
model's tally objects.

Comment on lines -410 to -412
group = tallies_group[f'tally {tally_id}']

# Check if tally is internal and therefore has no data
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems to have moved to _populate_tally, but it doesn't seem like that's always ran. Is that ok that this won't always be ran?

Comment on lines +559 to +569
sp_tally = self.get_tally(tally.scores,
tally.filters,
tally.nuclides,
tally.name,
tally.id,
tally.estimator,
exact_filters=True,
exact_nuclides=True,
exact_scores=True,
multiply_density=True,
derivative=tally.derivative)
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't feel PEP8. With such a long arguments list keyword arguments should be used to avoid off-by-1 errors.

@@ -564,6 +611,12 @@ def get_tally(self, scores=[], filters=[], nuclides=[],
If True, the number of scores in the parameters must be identical
to those in the matching Tally. If False (default), the scores
in the parameters may be a subset of those in the matching Tally.
Default is None (no check).
multiply_density : bool, optional
Copy link
Contributor

Choose a reason for hiding this comment

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

This feels more like an enforce_multipl_dens than a multiply_density

@@ -591,17 +644,25 @@ def get_tally(self, scores=[], filters=[], nuclides=[],
if id and id != test_tally.id:
continue

# Determine if Tally has queried estimator
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a bit out of scope, but why iterate over self.tallies.values()? This seems like its a dictionary, and doing it this makes it an O(N^2) operation to find all tallies.

@@ -33,7 +33,7 @@
_FILTER_CLASSES = (openmc.Filter, openmc.CrossFilter, openmc.AggregateFilter)

# Valid types of estimators
ESTIMATOR_TYPES = ['tracklength', 'collision', 'analog']
ESTIMATOR_TYPES = ('tracklength', 'collision', 'analog')
Copy link
Contributor

Choose a reason for hiding this comment

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

Might as well just do a set for slightly better performance.

other_nuclides.remove('total')
if 'total' in self_nuclides:
self_nuclides.remove('total')
if other_nuclides != self_nuclides:
Copy link
Contributor

Choose a reason for hiding this comment

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

What dataset is nuclides? Does this require proper sorting? Maybe sets?

self_nuclides.remove('total')
if other_nuclides != self_nuclides:
return False
if other.scores != self.scores:
Copy link
Contributor

Choose a reason for hiding this comment

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

A lot of this is repeated code.

Is

for attr_name in {"scores", "triggers"...}:
    if getattr(other, attr_name) != getattr(self, attr_name):
        return False

too clever?

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 this pull request may close these issues.

2 participants