Repository: gammapy/gammapy

Issue:
Add event list class for Fermi-LAT
The Gammapy event list class and tests needs some ❤️ , and I think we should support Fermi-LAT event lists via a separate class.

This issue is triggered by #1175 by @cosimoNigro where he complains that one of the event list methods doesn't work for Fermi-LAT event lists.

This is what we have now:

- https://github.com/gammapy/gammapy/blob/master/gammapy/data/event_list.py
- https://github.com/gammapy/gammapy/blob/master/gammapy/data/tests/test_event_list.py

There is a `TestEventListFermi` that loads up the Fermi-LAT event list, but there are no tests implemented like for `TestEventListHESS` that check which methods work or don't work for Fermi-LAT event lists. The reason is simply that so far we haven't really written code in Gammapy to analyse Fermi-LAT data. I think it's about time we start and the event list is a good place.

We could add if / else statements in the existing EventList class, trying to make it work for both cases (IACT and Fermi-LAT). But I think it would be better if we introduce different classes. The two cases are sufficiently different (one pointed observation, one slewing observation) that trying to handle both in one class will be painful. The question is how to avoid code duplication: quite a few methods like event energy or time or cone selection that access only info that's there in both event lists will just work for both and we don't want to duplicate the code. I'm generally sceptical for inheritance, but in this case I think inheritance is the best way I would know how to do this.

One question is concerning naming: the main options I see are:
* "IACT" or "CTA" or "Pointed" for the one case
* "Fermi" or "LAT" or "Slewing" for the other case

Probably CTA will also have slewing observations, and e.g. HAWC is another slewing instrument, so maybe naming by instrument isn't so great? On the other hand, "CTA" and "LAT" are nice and short to type, and in the end these are the two telescopes that will or did define the format, and then others like HESS or HAWC can simply use it as well. So my suggestion would be to use "EventListCTA" and "EventListLAT", and to call the base class "EventList". This has the problem that existing code using "EventList" will break, so as a practical matter, as an intermediate step,  we could add "EventListBase" for the base class and have `EventList(EventListBase)` and `EventListLAT(EventListBase)` as sub-classes. This would be a quick change to make in a PR now, because changes are limited to the class and not callers.

We have these two other classes already, in those cases we have "FermiLAT" in the name:
* http://docs.gammapy.org/en/latest/api/gammapy.datasets.FermiLATDataset.html
* http://docs.gammapy.org/en/latest/api/gammapy.image.FermiLATBasicImageEstimator.html
* http://docs.gammapy.org/en/latest/api/gammapy.image.IACTBasicImageEstimator.html

For these image estimators, I think we should refactor them completely: separate exposure computation from background estimation. And then the distinction between the two image estimators can go, they both make exposure cubes and background, and then integrate in energy. But that's a discussion for another issue, I think we should start here with event lists, and then do analysis code (much harder to structure) later once that's in place. We could just choose names here for event lists now for what we think is a good long-term solution (think next 10 years), without regard for names we currently have elsewhere, and adapt those later to match.

I would suggest to make `EventList` not an `abc.abstractclass`, but one that can be instantiated and used directly. E.g. this could / should work for both Fermi-LAT and IACT event lists:
```
from gammapy.data import EventList
events = EventList('events.fits')
events.time # gives a Time object
events.select_cone() # cone selection using RA / DEC columns
```

@woodmd @adonath @joleroi @cosimoNigro or anyone - Thoughts?

Does someone have time to make a pull request here? Maybe @cosimoNigro ?  I think the initial pull request would be quite limited in scope (only need to edit `event_list.py` and `test_event_list.py`), roughly one day of work:
- Add an `EventListBase` class
- Go through the methods in `EvenList` - move the ones that should work for both cases to `EventListBase`
- Add `EventListLAT(EventListBase)` - can be empty for now, or you implement LAT-specific things like `peek` as mentioned in #1175 that for LAT could e.g. make an all-sky plot showing where the events are. Another good method to have would be `__str__` that prints some summary info about the event list.
- Update the tests in `TestEventListFermi` and `TestEventListHESS` as you go, or even do "test-driven development" of always keeping the tests working as you refactor or adding a new test before adding a new method.


Discussion:
Hi @cdeil, I can give it a try, but I will bother you for support.

So, to recap
* we make a `EventListBase` that handles keywords both in IACT and in LAT tables.
* we make a `EventList(EventListBase)` collecting every function that deals with IACT, e.g. 
`altaz`, `pointing_radec`...
* we make an empty `EventListLAT(EventListBase)` and then we decide what to put inside.

ok?
> I can give it a try

Awesome!

> but I will bother you for support.

Please do! You can also just make a PR with comments that have questions and then I'll make suggestions there. Or ask eyestions here.

> So, to recap

Yes, that would be my suggestion how to proceed for now.

Hopefully @woodmd or others that might be interested in Fermi-LAT support will chime in here, too.

There's almost nothing you need on the `EventListLAT` class now, but I think it's good to introduce it, to have a place to put LAT-specific things in the future as convenience, e.g. a helper function for event type selection, or even just the one "peek" method to get a counts image with one line of code.

Discussed with @registerrier and @adonath just now. In the call we said that having a class hierarchy just for this one different method for the two cases isn't great, and we should remove it and instead put an if / else to handle the two cases there.

https://github.com/gammapy/gammapy/blob/912e64dee68754f767f8254949bd378e88e1cd3a/gammapy/data/event_list.py#L676-L706

After re-reading this issue description I again feel that sub-classing as-is might be better. Especially LAT has no pointing, so offering `pointing_radec` attributes on LAT event lists isn't great. And if we start to build out the LAT class to add specific things like e.g. event class and type handling, or DSS (dataset subset) header keyword support etc, we'll want to have a separate class for this, not put a ton of if / else.

So it's a close call and we could go either way - but I have a slight preference to keep the sub-classes as-is here.

@adonath @registerrier and anyone interested - please think about it and comment soon, so that we can decide this question for now and resolve this old issue.
I think going back to one single class would be best for now, and then if / when next year or later you really have different event lists, you could re-introduce the second class. Or the different classes for different cases could occur at the constituent level, e.g. we already have a Pointing and fixed Pointing class. If `EventList` had those as members or other classes that represent `events.table.meta` information, then the need for multiple classes at the event list class level might go away.

But I won't work on this - unassigning myself now.

Resolving patch:
diff --git a/gammapy/data/event_list.py b/gammapy/data/event_list.py
--- a/gammapy/data/event_list.py
+++ b/gammapy/data/event_list.py
@@ -2,30 +2,28 @@
 import collections
 import logging
 import numpy as np
+from regions import CircleSkyRegion
 from astropy.coordinates import AltAz, Angle, SkyCoord
 from astropy.coordinates.angle_utilities import angular_separation
 from astropy.table import Table
 from astropy.table import vstack as vstack_tables
 from astropy.units import Quantity, Unit
-from gammapy.maps import MapAxis, MapCoord, WcsNDMap
+from astropy import units as u
+from gammapy.maps import MapAxis, MapCoord, WcsNDMap, RegionGeom
 from gammapy.utils.fits import earth_location_from_dict
 from gammapy.utils.regions import make_region
 from gammapy.utils.scripts import make_path
 from gammapy.utils.testing import Checker
 from gammapy.utils.time import time_ref_from_dict
 
-__all__ = ["EventListBase", "EventList", "EventListLAT"]
+__all__ = ["EventList"]
 
 log = logging.getLogger(__name__)
 
 
-class EventListBase:
+class EventList:
     """Event list.
 
-    This class represents the base for two different event lists:
-    - EventList: targeted for IACT event lists
-    - EventListLAT: targeted for Fermi-LAT event lists
-
     Event list data is stored in ``table`` (`~astropy.table.Table`) data member.
 
     The most important reconstructed event parameters
@@ -106,25 +104,34 @@ def stack(self, other):
         self.table = vstack_tables([self.table, other.table])
 
     def __str__(self):
-        ss = (
-            "EventList info:\n"
-            f"- Number of events: {len(self.table)}\n"
-            f"- Median energy: {np.median(self.energy.value):.3g} {self.energy.unit}\n"
-        )
+        info = self.__class__.__name__ + "\n"
+        info += "-" * len(self.__class__.__name__) + "\n\n"
+
+        instrument = self.table.meta.get("INSTRUME")
+        info += f"\tInstrument       : {instrument}\n"
+
+        telescope = self.table.meta.get("TELESCOP")
+        info += f"\tTelescope        : {telescope}\n"
 
-        if "OBS_ID" in self.table.meta:
-            ss += "- OBS_ID = {}".format(self.table.meta["OBS_ID"])
+        obs_id = self.table.meta.get("OBS_ID", "")
+        info += f"\tObs. ID          : {obs_id}\n\n"
 
-        # TODO: add time, RA, DEC and if present GLON, GLAT info ...
+        info += f"\tNumber of events : {len(self.table)}\n"
 
-        if "AZ" in self.table.colnames:
-            # TODO: azimuth should be circular median
-            ss += "- Median azimuth: {}\n".format(np.median(self.table["AZ"]))
+        rate = len(self.table) / self.observation_time_duration
+        info += f"\tEvent rate       : {rate:.3f}\n\n"
 
-        if "ALT" in self.table.colnames:
-            ss += "- Median altitude: {}\n".format(np.median(self.table["ALT"]))
+        info += f"\tTime start       : {self.observation_time_start}\n"
+        info += f"\tTime stop        : {self.observation_time_stop}\n\n"
 
-        return ss
+        info += f"\tMin. energy      : {np.min(self.energy):.2e}\n"
+        info += f"\tMax. energy      : {np.max(self.energy):.2e}\n"
+        info += f"\tMedian energy    : {np.median(self.energy):.2e}\n\n"
+
+        if self.is_pointed_observation:
+            offset_max = np.max(self.offset)
+            info += f"\tMax. offset      : {offset_max:.1f}\n"
+        return info.expandtabs(tabsize=2)
 
     @property
     def time_ref(self):
@@ -150,7 +157,7 @@ def observation_time_start(self):
         return self.time_ref + Quantity(self.table.meta["TSTART"], "second")
 
     @property
-    def observation_time_end(self):
+    def observation_time_stop(self):
         """Observation stop time (`~astropy.time.Time`)."""
         return self.time_ref + Quantity(self.table.meta["TSTOP"], "second")
 
@@ -173,6 +180,14 @@ def energy(self):
         """Event energies (`~astropy.units.Quantity`)."""
         return self.table["ENERGY"].quantity
 
+    @property
+    def galactic_median(self):
+        """Median position in radec"""
+        galactic = self.galactic
+        median_lon = np.median(galactic.l.wrap_at("180d"))
+        median_lat = np.median(galactic.b)
+        return SkyCoord(median_lon, median_lat, frame="galactic")
+
     def select_row_subset(self, row_specifier):
         """Select table row subset.
 
@@ -261,8 +276,8 @@ def select_region(self, region, wcs=None):
         event_list : `EventList`
             Copy of event list with selection applied.
         """
-        region = make_region(region)
-        mask = region.contains(self.radec, wcs)
+        geom = RegionGeom.create(region, wcs=wcs)
+        mask = geom.contains(self.radec)
         return self.select_row_subset(mask)
 
     def select_parameter(self, parameter, band):
@@ -339,8 +354,8 @@ def plot_time(self, ax=None):
 
         ax.set_xlabel("Time (sec)")
         ax.set_ylabel("Counts")
-        y, x_edges = np.histogram(time, bins=30)
-        # x = (x_edges[1:] + x_edges[:-1]) / 2
+        y, x_edges = np.histogram(time, bins=20)
+
         xerr = np.diff(x_edges) / 2
         x = x_edges[:-1] + xerr
         yerr = np.sqrt(y)
@@ -410,37 +425,44 @@ def plot_offset2_distribution(self, ax=None, center=None, **kwargs):
         ax = plt.gca() if ax is None else ax
 
         if center is None:
-            center = self.pointing_radec
+            center = self._plot_center
 
         offset2 = center.separation(self.radec).deg ** 2
 
+        kwargs.setdefault("histtype", "step")
+        kwargs.setdefault("bins", 30)
+
         ax.hist(offset2, **kwargs)
         ax.set_xlabel("Offset^2 (deg^2)")
         ax.set_ylabel("Counts")
 
         return ax
 
-    def plot_energy_offset(self, ax=None):
+    def plot_energy_offset(self, ax=None, center=None):
         """Plot counts histogram with energy and offset axes."""
         import matplotlib.pyplot as plt
         from matplotlib.colors import LogNorm
 
         ax = plt.gca() if ax is None else ax
 
-        energy_bounds = self._default_plot_ebounds().to_value("TeV")
-        offsetangle = self.pointing_radec.separation(self.radec).max().to("deg").value
-        offset_bounds = np.linspace(0, offsetangle, 30)
+        if center is None:
+            center = self._plot_center
+
+        energy_bounds = self._default_plot_ebounds().to_value(self.energy.unit)
+        offset = center.separation(self.radec)
+        offset_max = offset.max()
+        offset_bounds = np.linspace(0, offset_max.deg, 30)
 
         counts = np.histogram2d(
             x=self.energy.value,
-            y=self.offset.value,
+            y=offset.deg,
             bins=(energy_bounds, offset_bounds),
         )[0]
 
         ax.pcolormesh(energy_bounds, offset_bounds, counts.T, norm=LogNorm())
         ax.set_xscale("log")
         ax.set_xlabel(f"Energy ({self.energy.unit})")
-        ax.set_ylabel(f"Offset ({self.offset.unit})")
+        ax.set_ylabel(f"Offset ({offset.unit})")
 
     def check(self, checks="all"):
         """Run checks.
@@ -489,28 +511,6 @@ def select_map_mask(self, mask):
         valid = values > 0
         return self.select_row_subset(valid)
 
-
-class EventList(EventListBase):
-    """Event list for IACT dataset.
-
-    Data format specification: :ref:`gadf:iact-events`
-
-    For further information, see the base class: `~gammapy.data.EventListBase`.
-
-    Parameters
-    ----------
-    table : `~astropy.table.Table`
-        Event list table
-
-    Examples
-    --------
-    To load an example H.E.S.S. event list:
-
-    >>> from gammapy.data import EventList
-    >>> filename = '$GAMMAPY_DATA/hess-dl3-dr1/data/hess_dl3_dr1_obs_id_023523.fits.gz'
-    >>> events = EventList.read(filename)
-    """
-
     @property
     def observatory_earth_location(self):
         """Observatory location (`~astropy.coordinates.EarthLocation`)."""
@@ -523,7 +523,8 @@ def observation_time_duration(self):
         This is a keyword related to IACTs
         The wall time, including dead-time.
         """
-        return Quantity(self.table.meta["ONTIME"], "second")
+        time_delta = (self.observation_time_stop - self.observation_time_start).sec
+        return Quantity(time_delta, "s")
 
     @property
     def observation_live_time_duration(self):
@@ -587,6 +588,14 @@ def offset(self):
         offset = center.separation(position)
         return Angle(offset, unit="deg")
 
+    @property
+    def offset_from_median(self):
+        """Event offset from the median position (`~astropy.coordinates.Angle`)."""
+        position = self.radec
+        center = self.galactic_median
+        offset = center.separation(position)
+        return Angle(offset, unit="deg")
+
     def select_offset(self, offset_band):
         """Select events in offset band.
 
@@ -605,84 +614,94 @@ def select_offset(self, offset_band):
         mask &= offset < offset_band[1]
         return self.select_row_subset(mask)
 
-    def peek(self):
-        """Quick look plots."""
-        import matplotlib.pyplot as plt
-
-        fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(12, 8))
-        self.plot_energy(ax=axes[0, 0])
-
-        off_max = self.pointing_radec.separation(self.radec).max().deg
-        bins = np.linspace(start=0, stop=off_max ** 2, num=30)
-        self.plot_offset2_distribution(ax=axes[0, 1], bins=bins)
-
-        self.plot_time(ax=axes[0, 2])
+    @property
+    def is_pointed_observation(self):
+        """Whether observation is pointed"""
+        return "RA_PNT" in self.table.meta
 
-        axes[1, 0].axis("off")
-        m = self._counts_image()
-        ax = plt.subplot(2, 3, 4, projection=m.geom.wcs)
-        m.plot(ax=ax, stretch="linear")
+    def peek(self, allsky=False):
+        """Quick look plots.
 
-        self.plot_energy_offset(ax=axes[1, 1])
+        Parameters
+        ----------
+        allsky : bool
+            Wheter to look at the events allsky
+        """
+        import matplotlib.pyplot as plt
+        import matplotlib.gridspec as gridspec
+
+        if allsky:
+            gs = gridspec.GridSpec(nrows=2, ncols=2)
+            fig = plt.figure(figsize=(8, 8))
+        else:
+            gs = gridspec.GridSpec(nrows=2, ncols=3)
+            fig = plt.figure(figsize=(12, 8))
+
+        # energy plot
+        ax_energy = fig.add_subplot(gs[1, 0])
+        self.plot_energy(ax=ax_energy)
+
+        # offset plots
+        if not allsky:
+            ax_offset = fig.add_subplot(gs[0, 1])
+            self.plot_offset2_distribution(ax=ax_offset)
+            ax_energy_offset = fig.add_subplot(gs[0, 2])
+            self.plot_energy_offset(ax=ax_energy_offset)
+
+        # time plot
+        ax_time = fig.add_subplot(gs[1, 1])
+        self.plot_time(ax=ax_time)
+
+        # image plot
+        m = self._counts_image(allsky=allsky)
+        if allsky:
+            ax_image = fig.add_subplot(gs[0, :], projection=m.geom.wcs)
+        else:
+            ax_image = fig.add_subplot(gs[0, 0], projection=m.geom.wcs)
+        m.plot(ax=ax_image, stretch="sqrt", vmin=0)
+        plt.subplots_adjust(wspace=0.3)
 
-        self._plot_text_summary(ax=axes[1, 2])
-        plt.subplots_adjust(hspace=0.3, wspace=0.2)
+    @property
+    def _plot_center(self):
+        if self.is_pointed_observation:
+            return self.pointing_radec
+        else:
+            return self.galactic_median
 
-    def _plot_text_summary(self, ax):
-        ax.axis("off")
-        txt = str(self)
-        ax.text(0, 1, txt, fontsize=12, verticalalignment="top")
+    @property
+    def _plot_width(self):
+        if self.is_pointed_observation:
+            offset = self.offset
+        else:
+            offset = self.offset_from_median
+
+        return 2 * offset.max()
+
+    def _counts_image(self, allsky):
+        if allsky:
+            opts = {
+                "npix": (360, 180),
+                "binsz": 1.0,
+                "proj": "AIT",
+                "frame": "galactic"
+            }
+        else:
+            opts = {
+                "width": self._plot_width,
+                "binsz": 0.05,
+                "proj": "TAN",
+                "frame": "galactic",
+                "skydir": self._plot_center,
+            }
 
-    def _counts_image(self):
-        width = self.galactic.b.max().deg - self.galactic.b.min().deg
-        opts = {
-            "width": (width, width),
-            "binsz": 0.05,
-            "proj": "TAN",
-            "frame": "galactic",
-            "skydir": self.pointing_radec,
-        }
         m = WcsNDMap.create(**opts)
         m.fill_by_coord(self.radec)
         m = m.smooth(width=0.5)
         return m
 
-    def plot_image(self):
-        """Quick look counts map sky plot."""
-        m = self._counts_image()
-        m.plot(stretch="sqrt")
-
-
-class EventListLAT(EventListBase):
-    """Event list for Fermi-LAT dataset.
-
-    Fermi-LAT data products
-    https://fermi.gsfc.nasa.gov/ssc/data/analysis/documentation/Cicerone/Cicerone_Data/LAT_DP.html
-    Data format specification (columns)
-    https://fermi.gsfc.nasa.gov/ssc/data/analysis/documentation/Cicerone/Cicerone_Data/LAT_Data_Columns.html
-
-    For further information, see the base class: `~gammapy.data.EventListBase`.
-
-    Parameters
-    ----------
-    table : `~astropy.table.Table`
-        Event list table
-
-    Examples
-    --------
-    To load an example Fermi-LAT event list:
-
-    >>> from gammapy.data import EventListLAT
-    >>> filename = "$GAMMAPY_DATA/fermi-3fhl-gc/fermi-3fhl-gc-events.fits.gz"
-    >>> events = EventListLAT.read(filename)
-    """
-
-    def plot_image(self):
+    def plot_image(self, allsky=False):
         """Quick look counts map sky plot."""
-        from gammapy.maps import WcsNDMap
-
-        m = WcsNDMap.create(npix=(360, 180), binsz=1.0, proj="AIT", frame="galactic")
-        m.fill_by_coord(self.radec)
+        m = self._counts_image(allsky=allsky)
         m.plot(stretch="sqrt")
 
 
@@ -786,7 +805,7 @@ def check_times(self):
         if dt.min() < self.accuracy["time"].to_value("s"):
             yield self._record(level="error", msg="Event times before obs start time")
 
-        dt = (self.event_list.time - self.event_list.observation_time_end).sec
+        dt = (self.event_list.time - self.event_list.observation_time_stop).sec
         if dt.max() > self.accuracy["time"].to_value("s"):
             yield self._record(level="error", msg="Event times after the obs end time")
 


Test patch:
diff --git a/gammapy/data/tests/test_event_list.py b/gammapy/data/tests/test_event_list.py
--- a/gammapy/data/tests/test_event_list.py
+++ b/gammapy/data/tests/test_event_list.py
@@ -4,7 +4,7 @@
 from astropy.coordinates import SkyCoord
 from astropy.table import Table
 from regions import CircleSkyRegion, RectangleSkyRegion
-from gammapy.data import EventList, EventListBase, EventListLAT
+from gammapy.data import EventList
 from gammapy.maps import Map, MapAxis, WcsGeom
 from gammapy.utils.testing import mpl_plot_check, requires_data, requires_dependency
 
@@ -12,7 +12,7 @@
 @requires_data()
 class TestEventListBase:
     def setup_class(self):
-        self.events = EventListBase.read(
+        self.events = EventList.read(
             "$GAMMAPY_DATA/hess-dl3-dr1/data/hess_dl3_dr1_obs_id_020136.fits.gz"
         )
 
@@ -31,6 +31,8 @@ def setup_class(self):
     def test_basics(self):
         assert "EventList" in str(self.events)
 
+        assert self.events.is_pointed_observation
+
         assert len(self.events.table) == 11243
         assert self.events.time[0].iso == "2004-03-26 02:57:47.004"
         assert self.events.radec[0].to_string() == "229.239 -58.3417"
@@ -70,6 +72,15 @@ def test_altaz(self):
         assert_allclose(altaz[0].alt.deg, 53.258024, atol=1e-3)
         # TODO: add asserts for frame properties
 
+    def test_median_position(self):
+        coord = self.events.galactic_median
+        assert_allclose(coord.l.deg, 320.539346, atol=1e-3)
+        assert_allclose(coord.b.deg, -0.882515, atol=1e-3)
+
+    def test_median_offset(self):
+        offset_max = self.events.offset_from_median.max()
+        assert_allclose(offset_max.to_value("deg"), 36.346379, atol=1e-3)
+
     def test_from_stack(self):
         event_lists = [self.events] * 2
         stacked_list = EventList.from_stack(event_lists)
@@ -119,18 +130,19 @@ def test_peek(self):
 @requires_data()
 class TestEventListFermi:
     def setup_class(self):
-        self.events = EventListLAT.read(
+        self.events = EventList.read(
             "$GAMMAPY_DATA/fermi-3fhl-gc/fermi-3fhl-gc-events.fits.gz"
         )
 
     def test_basics(self):
         assert "EventList" in str(self.events)
         assert len(self.events.table) == 32843
+        assert not self.events.is_pointed_observation
 
     @requires_dependency("matplotlib")
-    def test_plot_image(self):
+    def test_peek(self):
         with mpl_plot_check():
-            self.events.plot_image()
+            self.events.peek(allsky=True)
 
 
 @requires_data()
@@ -153,7 +165,7 @@ def setup_class(self):
         table["ENERGY"] = [1.0, 1.5, 1.5, 10.0] * u.TeV
         table["OFFSET"] = [0.1, 0.5, 1.0, 1.5] * u.deg
 
-        self.events = EventListBase(table)
+        self.events = EventList(table)
 
         center1 = SkyCoord(0.0, 0.0, frame="icrs", unit="deg")
         on_region1 = CircleSkyRegion(center1, radius=1.0 * u.deg)
diff --git a/gammapy/data/tests/test_filters.py b/gammapy/data/tests/test_filters.py
--- a/gammapy/data/tests/test_filters.py
+++ b/gammapy/data/tests/test_filters.py
@@ -5,14 +5,14 @@
 from astropy.coordinates import Angle, SkyCoord
 from astropy.time import Time
 from astropy.units import Quantity
-from gammapy.data import GTI, DataStore, EventListBase, ObservationFilter
+from gammapy.data import GTI, DataStore, EventList, ObservationFilter
 from gammapy.utils.regions import SphericalCircleSkyRegion
 from gammapy.utils.testing import assert_time_allclose, requires_data
 
 
 def test_event_filter_types():
     for method_str in ObservationFilter.EVENT_FILTER_TYPES.values():
-        assert hasattr(EventListBase, method_str)
+        assert hasattr(EventList, method_str)
 
 
 @pytest.fixture(scope="session")