Repository: openmc-dev/openmc

Issue:
Unable to find merged tally when loading tally from statepoint
The addition of the `exact` option in the `StatePoint.get_tally()` method in PR #660 has resulted in the `MGXS.load_from_statepoint()` method producing the following error in `mgxs-part-iii.ipynb`:

```
# Initialize MGXS Library with OpenMC statepoint data
mgxs_lib.load_from_statepoint(sp)

---------------------------------------------------------------------------
LookupError                               Traceback (most recent call last)
<ipython-input-28-76d7abb36a81> in <module>()
      1 # Initialize MGXS Library with OpenMC statepoint data
----> 2 mgxs_lib.load_from_statepoint(sp)

/home/shaner/.local/lib/python2.7/site-packages/openmc-0.7.1-py2.7.egg/openmc/mgxs/library.pyc in load_from_statepoint(self, statepoint)
    446             for mgxs_type in self.mgxs_types:
    447                 mgxs = self.get_mgxs(domain, mgxs_type)
--> 448                 mgxs.load_from_statepoint(statepoint)
    449                 mgxs.sparse = self.sparse
    450 

/home/shaner/.local/lib/python2.7/site-packages/openmc-0.7.1-py2.7.egg/openmc/mgxs/mgxs.pyc in load_from_statepoint(self, statepoint)
    694             sp_tally = statepoint.get_tally(
    695                 tally.scores, tally.filters, tally.nuclides,
--> 696                 estimator=tally.estimator, exact=True)
    697             sp_tally = sp_tally.get_slice(
    698                 tally.scores, filters, filter_bins, tally.nuclides)

/home/shaner/.local/lib/python2.7/site-packages/openmc-0.7.1-py2.7.egg/openmc/statepoint.pyc in get_tally(self, scores, filters, nuclides, name, id, estimator, exact)
    620         # If we did not find the Tally, return an error message
    621         if tally is None:
--> 622             raise LookupError('Unable to get Tally')
    623 
    624         return tally

LookupError: Unable to get Tally
```

If the tallies are not merged, MGXS is able to load the tallies from statepoint just fine. This error seems to occur regardless of the cross sections used in:

```
mgxs_lib.mgxs_types = ['transport', 'nu-fission', 'fission', 'nu-scatter matrix', 'chi']
```

For instance, even if `mgxs_lib.mgxs_types` is set to `['total']`, the error is still thrown. I haven't done too much digging into the cause of the error, but it should be pretty easy to reproduce by running `mgxs-part-iii.ipynb`.



Discussion:
This issue is my fault for not having done due diligence for #660. If tallies are merged for `MGXS` during input generation, then the `exact` option will not work when retrieving those tallies from a `StatePoint`. For example, if multiple `MGXS` are created for different cells/materials, their cell/material filters will be merged with a single tally having a cell/material filter with multiple bins n the input. However, the `MGXS` will be unable to find their uniquely matching tallies in the `StatePoint` with a cell/material filter with a single bin. I'm not really sure how to address this without re-opening issue #657 and am open to ideas.

One idea would be to have separate `exact_filters`, `exact_nuclides` and `exact_scores` parameters (in the case of MGXS, we would only set `exact_nuclides` to `True` and `exact_filters` and `exact_scores` would be set to `False). Alternatively,`exact` could accept some iterable of items specifying which paramters should be met exactly (_i.e._, a dictionary with string keys "filters", "scores", etc. and boolean values).

With a more consideration, neither of the "solutions" proposed above would actually solve this issue.

So the bug in #657 was found because scatter-p1 from a scatter matrix tally was a successful match when actually looking for scatter-1 from within transportxs. Are all use cases which could 'excite' this error obviated if the search function doesn't consider scatter-p1 a stand in for scatter-1?

I'm not sure exactly how to answer your question @nelsonag, but it is worth noting that in the output "statepoint.h5" file there will actually not be _any_ "scatter-p1" scores - instead, "scatter-px" scores in the input are expanded into separate "scatter-0", "scatter-1", ... " scatter-x" scores in the output.


Resolving patch:
diff --git a/openmc/mgxs/mgxs.py b/openmc/mgxs/mgxs.py
--- a/openmc/mgxs/mgxs.py
+++ b/openmc/mgxs/mgxs.py
@@ -693,7 +693,7 @@ def load_from_statepoint(self, statepoint):
         for tally_type, tally in self.tallies.items():
             sp_tally = statepoint.get_tally(
                 tally.scores, tally.filters, tally.nuclides,
-                estimator=tally.estimator, exact=True)
+                estimator=tally.estimator, exact_filters=True)
             sp_tally = sp_tally.get_slice(
                 tally.scores, filters, filter_bins, tally.nuclides)
             sp_tally.sparse = self.sparse
diff --git a/openmc/statepoint.py b/openmc/statepoint.py
--- a/openmc/statepoint.py
+++ b/openmc/statepoint.py
@@ -497,17 +497,18 @@ def sparse(self, sparse):
                 self.tallies[tally_id].sparse = self.sparse
 
     def get_tally(self, scores=[], filters=[], nuclides=[],
-                  name=None, id=None, estimator=None, exact=False):
+                  name=None, id=None, estimator=None, exact_filters=False,
+                  exact_nuclides=False, exact_scores=False):
         """Finds and returns a Tally object with certain properties.
 
         This routine searches the list of Tallies and returns the first Tally
         found which satisfies all of the input parameters.
 
-        NOTE: If the "exact" parameter is False (default), the input parameters
-        do not need to match the complete Tally specification and may only
-        represent a subset of the Tally's properties. If the "exact" parameter
-        is True then the scores, filters, nuclides and estimator parameters
-        must precisely match those of any matching Tally.
+        NOTE: If any of the "exact" parameters are False (default), the input
+        parameters do not need to match the complete Tally specification and
+        may only represent a subset of the Tally's properties. If an "exact"
+        parameter is True then number of scores, filters, or nuclides in the
+        parameters must precisely match those of any matching Tally.
 
         Parameters
         ----------
@@ -523,9 +524,18 @@ def get_tally(self, scores=[], filters=[], nuclides=[],
             The id specified for the Tally (default is None).
         estimator: str, optional
             The type of estimator ('tracklength', 'analog'; default is None).
-        exact : bool
-            Whether to strictly enforce the match between the parameters and
-            the returned tally
+        exact_filters : bool
+            If True, the number of filters in the parameters must be identical
+            to those in the matching Tally. If False (default), the filters in
+            the parameters may be a subset of those in the matching Tally.
+        exact_nuclides : bool
+            If True, the number of nuclides in the parameters must be identical
+            to those in the matching Tally. If False (default), the nuclides in
+            the parameters may be a subset of those in the matching Tally.
+        exact_scores : bool
+            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.
 
         Returns
         -------
@@ -554,17 +564,16 @@ def get_tally(self, scores=[], filters=[], nuclides=[],
                 continue
 
             # Determine if Tally has queried estimator
-            if (estimator or exact) and estimator != test_tally.estimator:
+            if estimator and estimator != test_tally.estimator:
                 continue
 
             # The number of filters, nuclides and scores must exactly match
-            if exact:
-                if len(scores) != test_tally.num_scores:
-                    continue
-                if len(nuclides) != test_tally.num_nuclides:
-                    continue
-                if len(filters) != test_tally.num_filters:
-                    continue
+            if exact_scores and len(scores) != test_tally.num_scores:
+                continue
+            if exact_nuclides and len(nuclides) != test_tally.num_nuclides:
+                continue
+            if exact_filters and len(filters) != test_tally.num_filters:
+                continue
 
             # Determine if Tally has the queried score(s)
             if scores:


Test patch: