Repository: openmc-dev/openmc

Issue:
Use fewer `.id`s in the PythonAPI
Another tricky one to explain at workshops:  We sort of try to hide the concept of `id` numbers from the user by making them an optional keyword argument in most of our constructors, but you still need to use them when you create a `TallyFilter` or specify a `Plot.col_spec`.  I would like to make those objects accept e.g. `Cell` objects and grab their `id`s at export time.


Discussion:
Totally agree -- let's fix that. The `Plot` one was already taken care of in #826, so specifying IDs when using filters is the last remaining area where you have to manually say `.id`. I guess the reason we use IDs is that if you read back a `Tally` object from a statepoint, it won't be able to recreate the same object unless the summary file is linked. These days that's usually a good assumption since we write summary.h5 by default, but the question then is what to store for filter bins when no geometry information is available.
Maybe we can avoid that question by only ever storing `id`s on the `TallyFilter`s.  We can adjust the `bins.setter` method to simply strip the `id` numbers from e.g. `Cell`s passed to it.  In other words, make these two options have identical effects:
```python
openmc.CellFilter([c.id for c in cells])
openmc.CellFilter(cells)
```

In the long run, there are probably advantages to storing the `Cell`s themselves, but I think we can quickly implement the `id` stripping now and have something which is better than the status quo.
Yeah, I was thinking that too. Do you want to implement this or should I take it?
Or, we can have a newcomer try to attack this since it is relatively simple.
I'm assigning @giudgiud.  I don't see a way to assign him with the official GitHub thing.  Maybe he has to be a collaborator first?
To be a little more explicit about the proposed change:

Consider this example of creating a CellFilter:
```python
filter = CellFilter([1, 2, 3])
```

That always calls this method you see defined for the `Filter` class in filter.py
```python
    @bins.setter
    def bins(self, bins):
        # Format the bins as a 1D numpy array.
        bins = np.atleast_1d(bins)

        # Check the bin values.
        self.check_bins(bins)

        self._bins = bins
```

We want this method to be a little bit smarter so that we can do something like
```python
c1 = openmc.Cell(cell_id=1)
c2 = openmc.Cell(cell_id=2)
filt = openmc.CellFilter([c1, c2])
```
or
```python
m1 = openmc.Material(material_id=1)
m2 = openmc.Material(material_id=2)
filt = openmc.MaterialFilter([m1, m2])
```

The most obvious approach would be to overload the `bins.setter` method for each of the relevant filters.  For every filter where we add a `bins.setter`, it probably doesn't make sense to keep a separate `check_bins` method.  So for `CellFilter` you would define something like
```python
    @bins.setter
    def bins(self, bins):
        # Format the bins as a 1D numpy array.
        bins = np.atleast_1d(bins)

        # Make sure the bins have the right content.
        cv.check_iterable_type('filter bins', bins, (Integral, openmc.Cell))

        # Strip ids from Cells.
        if any([isinstance(b, openmc.Cell) for b in bins]):
            if isinstance(bins[0], openmc.Cell)
                bins = np.atleast_1d(
                    [b.id if isinstance(b, openmc.Cell) else b
                     for b in bins])

        self._bins = bins
```

You would need to modify `UniverseFilter`, `MaterialFilter`, `CellFilter`, `CellbornFilter`, and `DistribcellFilter`.  We should also probably modify test_tallies.py so that it tests this feature and the examples so that they demonstrate this feature.
Has there been any progress on this? I don't mean to rush it, but I would like to fix this before our next release and it is one of the last remaining issues tagged to v0.9.

Resolving patch:
diff --git a/examples/python/basic/build-xml.py b/examples/python/basic/build-xml.py
--- a/examples/python/basic/build-xml.py
+++ b/examples/python/basic/build-xml.py
@@ -95,7 +95,7 @@
 ###############################################################################
 
 # Instantiate some tally Filters
-cell_filter = openmc.CellFilter(100)
+cell_filter = openmc.CellFilter(cell2)
 energy_filter = openmc.EnergyFilter([0., 20.e6])
 energyout_filter = openmc.EnergyoutFilter([0., 20.e6])
 
diff --git a/examples/python/lattice/hexagonal/build-xml.py b/examples/python/lattice/hexagonal/build-xml.py
--- a/examples/python/lattice/hexagonal/build-xml.py
+++ b/examples/python/lattice/hexagonal/build-xml.py
@@ -153,7 +153,7 @@
 
 # Instantiate a distribcell Tally
 tally = openmc.Tally(tally_id=1)
-tally.filters = [openmc.DistribcellFilter(cell2.id)]
+tally.filters = [openmc.DistribcellFilter(cell2)]
 tally.scores = ['total']
 
 # Instantiate a Tallies collection and export to XML
diff --git a/openmc/filter.py b/openmc/filter.py
--- a/openmc/filter.py
+++ b/openmc/filter.py
@@ -434,54 +434,48 @@ def get_pandas_dataframe(self, data_size, **kwargs):
         return df
 
 
-class IntegralFilter(Filter):
-    """Tally modifier that describes phase-space and other characteristics.
-
-    Parameters
-    ----------
-    bins : Integral or Iterable of Integral
-        The bins for the filter. This takes on different meaning for different
-        filters. See the docstrings for sublcasses of this filter or the online
-        documentation for more details.
-
-    Attributes
-    ----------
-    bins : Integral or Iterable of Integral
-        The bins for the filter
-    num_bins : Integral
-        The number of filter bins
-    stride : Integral
-        The number of filter, nuclide and score bins within each of this
-        filter's bins.
-
-    """
+class WithIDFilter(Filter):
+    """Abstract parent for filters of types with ids (Cell, Material, etc.)."""
     @property
     def num_bins(self):
         return len(self.bins)
 
+    # Since num_bins property is declared, also need a num_bins.setter, but
+    # we don't want it to do anything since num_bins is completely determined
+    # by len(self.bins).  We also don't want to raise an error because that
+    # makes importing from HDF5 more complicated.
     @num_bins.setter
-    def num_bins(self, num_bins):
-        cv.check_type('filter num_bins', num_bins, Integral)
-        cv.check_greater_than('filter num_bins', num_bins, 0, equality=True)
-        self._num_bins = num_bins
+    def num_bins(self, num_bins): pass
 
-    def check_bins(self, bins):
-        cv.check_iterable_type('filter bins', bins, Integral)
+    def _smart_set_bins(self, bins, bin_type):
+        # Format the bins as a 1D numpy array.
+        bins = np.atleast_1d(bins)
+
+        # Check the bin values.
+        cv.check_iterable_type('filter bins', bins, (Integral, bin_type))
         for edge in bins:
-            cv.check_greater_than('filter bin', edge, 0, equality=True)
+            if isinstance(edge, Integral):
+                cv.check_greater_than('filter bin', edge, 0, equality=True)
 
+        # Extract id values.
+        bins = np.atleast_1d([b if isinstance(b, Integral) else b.id
+                              for b in bins])
+
+        self._bins = bins
 
-class UniverseFilter(IntegralFilter):
-    """Bins tally event locations based on the universe they occured in.
+
+class UniverseFilter(WithIDFilter):
+    """Bins tally event locations based on the Universe they occured in.
 
     Parameters
     ----------
-    bins : Integral or Iterable of Integral
-        openmc.Universe IDs.
+    bins : openmc.Universe, Integral, or iterable thereof
+        The Universes to tally. Either openmc.Universe objects or their
+        Integral ID numbers can be used.
 
     Attributes
     ----------
-    bins : Integral or Iterable of Integral
+    bins : Iterable of Integral
         openmc.Universe IDs.
     num_bins : Integral
         The number of filter bins
@@ -490,19 +484,27 @@ class UniverseFilter(IntegralFilter):
         filter's bins.
 
     """
+    @property
+    def bins(self):
+        return self._bins
+
+    @bins.setter
+    def bins(self, bins):
+        self._smart_set_bins(bins, openmc.Universe)
 
 
-class MaterialFilter(IntegralFilter):
-    """Bins tally events based on which material they occured in.
+class MaterialFilter(WithIDFilter):
+    """Bins tally event locations based on the Material they occured in.
 
     Parameters
     ----------
-    bins : Integral or Iterable of Integral
-        openmc.Material IDs.
+    bins : openmc.Material, Integral, or iterable thereof
+        The Materials to tally. Either openmc.Material objects or their
+        Integral ID numbers can be used.
 
     Attributes
     ----------
-    bins : Integral or Iterable of Integral
+    bins : Iterable of Integral
         openmc.Material IDs.
     num_bins : Integral
         The number of filter bins
@@ -511,19 +513,27 @@ class MaterialFilter(IntegralFilter):
         filter's bins.
 
     """
+    @property
+    def bins(self):
+        return self._bins
 
+    @bins.setter
+    def bins(self, bins):
+        self._smart_set_bins(bins, openmc.Material)
 
-class CellFilter(IntegralFilter):
-    """Bins tally event locations based on which cell they occured in.
+
+class CellFilter(WithIDFilter):
+    """Bins tally event locations based on the Cell they occured in.
 
     Parameters
     ----------
-    bins : Integral or Iterable of Integral
-        openmc.Cell IDs.
+    bins : openmc.Cell, Integral, or iterable thereof
+        The Cells to tally. Either openmc.Cell objects or their
+        Integral ID numbers can be used.
 
     Attributes
     ----------
-    bins : Integral or Iterable of Integral
+    bins : Iterable of Integral
         openmc.Cell IDs.
     num_bins : Integral
         The number of filter bins
@@ -532,19 +542,27 @@ class CellFilter(IntegralFilter):
         filter's bins.
 
     """
+    @property
+    def bins(self):
+        return self._bins
+
+    @bins.setter
+    def bins(self, bins):
+        self._smart_set_bins(bins, openmc.Cell)
 
 
-class CellbornFilter(IntegralFilter):
-    """Bins tally events based on the cell that the particle was born in.
+class CellbornFilter(WithIDFilter):
+    """Bins tally events based on which Cell the neutron was born in.
 
     Parameters
     ----------
-    bins : Integral or Iterable of Integral
-        openmc.Cell IDs.
+    bins : openmc.Cell, Integral, or iterable thereof
+        The birth Cells to tally. Either openmc.Cell objects or their
+        Integral ID numbers can be used.
 
     Attributes
     ----------
-    bins : Integral or Iterable of Integral
+    bins : Iterable of Integral
         openmc.Cell IDs.
     num_bins : Integral
         The number of filter bins
@@ -553,9 +571,16 @@ class CellbornFilter(IntegralFilter):
         filter's bins.
 
     """
+    @property
+    def bins(self):
+        return self._bins
 
+    @bins.setter
+    def bins(self, bins):
+        self._smart_set_bins(bins, openmc.Cell)
 
-class SurfaceFilter(IntegralFilter):
+
+class SurfaceFilter(Filter):
     """Bins particle currents on Mesh surfaces.
 
     Parameters
@@ -576,6 +601,28 @@ class SurfaceFilter(IntegralFilter):
         filter's bins.
 
     """
+    @property
+    def bins(self):
+        return self._bins
+
+    @property
+    def num_bins(self):
+        return len(self.bins)
+
+    @bins.setter
+    def bins(self, bins):
+        # Format the bins as a 1D numpy array.
+        bins = np.atleast_1d(bins)
+
+        # Check the bin values.
+        cv.check_iterable_type('filter bins', bins, Integral)
+        for edge in bins:
+            cv.check_greater_than('filter bin', edge, 0, equality=True)
+
+        self._bins = bins
+
+    @num_bins.setter
+    def num_bins(self, num_bins): pass
 
     def get_pandas_dataframe(self, data_size, **kwargs):
         """Builds a Pandas DataFrame for the Filter's bins.
@@ -1082,14 +1129,14 @@ class DistribcellFilter(Filter):
 
     Parameters
     ----------
-    bins : Integral or Iterable of Integral or Iterable of Real
-        The bins for the filter. This takes on different meaning for different
-        filters. See the OpenMC online documentation for more details.
+    cell : openmc.Cell or Integral
+        The distributed cell to tally. Either an openmc.Cell or an Integral
+        cell ID number can be used.
 
     Attributes
     ----------
-    bins : Integral or Iterable of Integral or Iterable of Real
-        The bins for the filter
+    bins : Iterable of Integral
+        An iterable with one element---the ID of the distributed Cell.
     num_bins : Integral
         The number of filter bins
     stride : Integral
@@ -1101,9 +1148,9 @@ class DistribcellFilter(Filter):
 
     """
 
-    def __init__(self, bins):
+    def __init__(self, cell):
         self._paths = None
-        super(DistribcellFilter, self).__init__(bins)
+        super(DistribcellFilter, self).__init__(cell)
 
     @classmethod
     def from_hdf5(cls, group, **kwargs):
@@ -1117,24 +1164,36 @@ def from_hdf5(cls, group, **kwargs):
 
         return out
 
+    @property
+    def bins(self):
+        return self._bins
+
     @property
     def paths(self):
         return self._paths
 
-    @paths.setter
-    def paths(self, paths):
-        cv.check_iterable_type('paths', paths, str)
-        self._paths = paths
+    @bins.setter
+    def bins(self, bins):
+        # Format the bins as a 1D numpy array.
+        bins = np.atleast_1d(bins)
 
-    def check_bins(self, bins):
+        # Make sure there is only 1 bin.
         if not len(bins) == 1:
             msg = 'Unable to add bins "{0}" to a DistribcellFilter since ' \
                   'only a single distribcell can be used per tally'.format(bins)
             raise ValueError(msg)
 
-        cv.check_iterable_type('filter bins', bins, Integral)
-        for edge in bins:
-            cv.check_greater_than('filter bin', edge, 0, equality=True)
+        # Check the type and extract the id, if necessary.
+        cv.check_type('distribcell bin', bins[0], (Integral, openmc.Cell))
+        if isinstance(bins[0], openmc.Cell):
+            bins = np.atleast_1d(bins[0].id)
+
+        self._bins = bins
+
+    @paths.setter
+    def paths(self, paths):
+        cv.check_iterable_type('paths', paths, str)
+        self._paths = paths
 
     def can_merge(self, other):
         # Distribcell filters cannot have more than one bin
@@ -1608,7 +1667,7 @@ def get_pandas_dataframe(self, data_size, paths=True):
         return df
 
 
-class DelayedGroupFilter(IntegralFilter):
+class DelayedGroupFilter(Filter):
     """Bins fission events based on the produced neutron precursor groups.
 
     Parameters
@@ -1631,6 +1690,28 @@ class DelayedGroupFilter(IntegralFilter):
         filter's bins.
 
     """
+    @property
+    def bins(self):
+        return self._bins
+
+    @property
+    def num_bins(self):
+        return len(self.bins)
+
+    @bins.setter
+    def bins(self, bins):
+        # Format the bins as a 1D numpy array.
+        bins = np.atleast_1d(bins)
+
+        # Check the bin values.
+        cv.check_iterable_type('filter bins', bins, Integral)
+        for edge in bins:
+            cv.check_greater_than('filter bin', edge, 0, equality=True)
+
+        self._bins = bins
+
+    @num_bins.setter
+    def num_bins(self, num_bins): pass
 
 
 class EnergyFunctionFilter(Filter):


Test patch:
diff --git a/tests/test_mg_tallies/test_mg_tallies.py b/tests/test_mg_tallies/test_mg_tallies.py
--- a/tests/test_mg_tallies/test_mg_tallies.py
+++ b/tests/test_mg_tallies/test_mg_tallies.py
@@ -26,8 +26,7 @@
     matching_eout_filter = openmc.EnergyoutFilter(energies)
     mesh_filter = openmc.MeshFilter(mesh)
 
-    mat_ids = [mat.id for mat in model.materials]
-    mat_filter = openmc.MaterialFilter(mat_ids)
+    mat_filter = openmc.MaterialFilter(model.materials)
 
     nuclides = [xs.name for xs in model.xs_data]
 
diff --git a/tests/test_tallies/test_tallies.py b/tests/test_tallies/test_tallies.py
--- a/tests/test_tallies/test_tallies.py
+++ b/tests/test_tallies/test_tallies.py
@@ -43,7 +43,10 @@
     azimuthal_tally3.estimator = 'tracklength'
 
     cellborn_tally = Tally()
-    cellborn_tally.filters = [CellbornFilter((10, 21, 22, 23))]
+    cellborn_tally.filters = [
+        CellbornFilter((model.geometry.get_all_cells()[10],
+                        model.geometry.get_all_cells()[21],
+                        22, 23))]  # Test both Cell objects and ids
     cellborn_tally.scores = ['total']
 
     dg_tally = Tally()
@@ -66,7 +69,10 @@
     transfer_tally.scores = ['scatter', 'nu-fission']
 
     material_tally = Tally()
-    material_tally.filters = [MaterialFilter((1, 2, 3, 4))]
+    material_tally.filters = [
+        MaterialFilter((model.geometry.get_materials_by_name('UOX fuel')[0],
+                        model.geometry.get_materials_by_name('Zircaloy')[0],
+                        3, 4))]  # Test both Material objects and ids
     material_tally.scores = ['total']
 
     mu_bins = (-1.0, -0.5, 0.0, 0.5, 1.0)
@@ -97,10 +103,15 @@
     polar_tally3.estimator = 'tracklength'
 
     universe_tally = Tally()
-    universe_tally.filters = [UniverseFilter((1, 2, 3, 4, 6, 8))]
+    universe_tally.filters = [
+        UniverseFilter((model.geometry.get_all_universes()[1],
+                        model.geometry.get_all_universes()[2],
+                        3, 4, 6, 8))]  # Test both Universe objects and ids
     universe_tally.scores = ['total']
 
-    cell_filter = CellFilter((10, 21, 22, 23, 60))
+    cell_filter = CellFilter((model.geometry.get_all_cells()[10],
+                              model.geometry.get_all_cells()[21],
+                              22, 23, 60))  # Test both Cell objects and ids
     score_tallies = [Tally(), Tally(), Tally()]
     for t in score_tallies:
         t.filters = [cell_filter]