Repository: mne-tools/mne-python

Issue:
BUG: problems with epochs._raw_times while cropping resampled epochs
While cropping epochs, we also change epochs._raw_times. However, as soon as epochs were resampled before cropping, epochs._raw_times is based on a different sfreq. Thats why we cannot apply the same tmask for epochs.times and epochs._raw_times. This leeds to wrong values in epochs._raw_times if sfreq_new < sfreq_old and an error if sfreq_new > sfreq_old. Changing epochs.crop(). We thus should use a different tmask for changing epochs._raw_times. As we do not store the old sampling frequency (AFAIK), we need to compute it as 1./ (epochs._raw_times[1]-epochs._raw_times[0]).

In mne.epochs.crop(), changing

```
tmask = _time_mask(self.times, tmin, tmax, sfreq=self.info['sfreq'])
self.times = self.times[tmask]
self._raw_times = self._raw_times[tmask]
self._data = self._data[:, :, tmask]
```

to 

```
tmask = _time_mask(self.times, tmin, tmax, sfreq=self.info['sfreq'])
self.times = self.times[tmask]
self._data = self._data[:, :, tmask]
tmask = _time_mask(self._raw_times, tmin, tmax,
    sfreq=(1. / (self._raw_times[1] - self._raw_times[0])))
self._raw_times = self._raw_times[tmask]
```

should solve the issue.



Discussion:

Resolving patch:
diff --git a/mne/epochs.py b/mne/epochs.py
--- a/mne/epochs.py
+++ b/mne/epochs.py
@@ -32,7 +32,7 @@
 from .io.proj import setup_proj, ProjMixin, _proj_equal
 from .io.base import _BaseRaw, ToDataFrameMixin, TimeMixin
 from .bem import _check_origin
-from .evoked import EvokedArray
+from .evoked import EvokedArray, _check_decim
 from .baseline import rescale, _log_rescale
 from .channels.channels import (ContainsMixin, UpdateChannelsMixin,
                                 SetChannelsMixin, InterpolationMixin)
@@ -340,6 +340,9 @@ def load_data(self):
     def decimate(self, decim, offset=0):
         """Decimate the epochs
 
+        .. note:: No filtering is performed. To avoid aliasing, ensure
+                  your data are properly lowpassed.
+
         Parameters
         ----------
         decim : int
@@ -356,6 +359,12 @@ def decimate(self, decim, offset=0):
         epochs : instance of Epochs
             The decimated Epochs object.
 
+        See Also
+        --------
+        Evoked.decimate
+        Epochs.resample
+        Raw.resample
+
         Notes
         -----
         Decimation can be done multiple times. For example,
@@ -364,36 +373,17 @@ def decimate(self, decim, offset=0):
 
         .. versionadded:: 0.10.0
         """
-        if decim < 1 or decim != int(decim):
-            raise ValueError('decim must be an integer > 0')
-        decim = int(decim)
-        new_sfreq = self.info['sfreq'] / float(decim)
-        lowpass = self.info['lowpass']
-        if decim > 1 and lowpass is None:
-            warn('The measurement information indicates data is not low-pass '
-                 'filtered. The decim=%i parameter will result in a sampling '
-                 'frequency of %g Hz, which can cause aliasing artifacts.'
-                 % (decim, new_sfreq))
-        elif decim > 1 and new_sfreq < 2.5 * lowpass:
-            warn('The measurement information indicates a low-pass frequency '
-                 'of %g Hz. The decim=%i parameter will result in a sampling '
-                 'frequency of %g Hz, which can cause aliasing artifacts.'
-                 % (lowpass, decim, new_sfreq))  # > 50% nyquist lim
-        offset = int(offset)
-        if not 0 <= offset < decim:
-            raise ValueError('decim must be at least 0 and less than %s, got '
-                             '%s' % (decim, offset))
+        decim, offset, new_sfreq = _check_decim(self.info, decim, offset)
+        start_idx = int(round(-self._raw_times[0] * (self.info['sfreq'] *
+                                                     self._decim)))
         self._decim *= decim
-        start_idx = int(round(self._raw_times[0] * (self.info['sfreq'] *
-                                                    self._decim)))
-        i_start = start_idx % self._decim
-        decim_slice = slice(i_start + offset, len(self._raw_times),
-                            self._decim)
+        i_start = start_idx % self._decim + offset
+        decim_slice = slice(i_start, None, self._decim)
         self.info['sfreq'] = new_sfreq
         if self.preload:
             self._data = self._data[:, :, decim_slice].copy()
             self._raw_times = self._raw_times[decim_slice].copy()
-            self._decim_slice = slice(None, None, None)
+            self._decim_slice = slice(None)
             self._decim = 1
             self.times = self._raw_times
         else:
@@ -1707,6 +1697,7 @@ def resample(self, sfreq, npad='auto', window='boxcar', n_jobs=1,
         self.info['sfreq'] = float(sfreq)
         self.times = (np.arange(self._data.shape[2], dtype=np.float) /
                       sfreq + self.times[0])
+        self._raw_times = self.times
         return self
 
     def copy(self):
@@ -2112,7 +2103,8 @@ def _get_epoch_from_raw(self, idx, verbose=None):
         event_samp = self.events[idx, 0]
         # Read a data segment
         first_samp = self._raw.first_samp
-        start = int(round(event_samp + self.tmin * sfreq)) - first_samp
+        start = int(round(event_samp + self._raw_times[0] * sfreq))
+        start -= first_samp
         stop = start + len(self._raw_times)
         data = self._raw._check_bad_segment(start, stop, self.picks,
                                             self.reject_by_annotation)
diff --git a/mne/evoked.py b/mne/evoked.py
--- a/mne/evoked.py
+++ b/mne/evoked.py
@@ -160,6 +160,49 @@ def crop(self, tmin=None, tmax=None):
         self.data = self.data[:, mask]
         return self
 
+    def decimate(self, decim, offset=0):
+        """Decimate the evoked data
+
+        .. note:: No filtering is performed. To avoid aliasing, ensure
+                  your data are properly lowpassed.
+
+        Parameters
+        ----------
+        decim : int
+            The amount to decimate data.
+        offset : int
+            Apply an offset to where the decimation starts relative to the
+            sample corresponding to t=0. The offset is in samples at the
+            current sampling rate.
+
+        Returns
+        -------
+        evoked : instance of Evoked
+            The decimated Evoked object.
+
+        See Also
+        --------
+        Epochs.decimate
+        Epochs.resample
+        Raw.resample
+
+        Notes
+        -----
+        Decimation can be done multiple times. For example,
+        ``evoked.decimate(2).decimate(2)`` will be the same as
+        ``evoked.decimate(4)``.
+
+        .. versionadded:: 0.13.0
+        """
+        decim, offset, new_sfreq = _check_decim(self.info, decim, offset)
+        start_idx = int(round(self.times[0] * (self.info['sfreq'] * decim)))
+        i_start = start_idx % decim + offset
+        decim_slice = slice(i_start, None, decim)
+        self.info['sfreq'] = new_sfreq
+        self.data = self.data[:, decim_slice].copy()
+        self.times = self.times[decim_slice].copy()
+        return self
+
     def shift_time(self, tshift, relative=True):
         """Shift time scale in evoked data
 
@@ -900,6 +943,30 @@ def get_peak(self, ch_type=None, tmin=None, tmax=None, mode='abs',
                 time_idx if time_as_index else self.times[time_idx])
 
 
+def _check_decim(info, decim, offset):
+    """Helper to check decimation parameters"""
+    if decim < 1 or decim != int(decim):
+        raise ValueError('decim must be an integer > 0')
+    decim = int(decim)
+    new_sfreq = info['sfreq'] / float(decim)
+    lowpass = info['lowpass']
+    if decim > 1 and lowpass is None:
+        warn('The measurement information indicates data is not low-pass '
+             'filtered. The decim=%i parameter will result in a sampling '
+             'frequency of %g Hz, which can cause aliasing artifacts.'
+             % (decim, new_sfreq))
+    elif decim > 1 and new_sfreq < 2.5 * lowpass:
+        warn('The measurement information indicates a low-pass frequency '
+             'of %g Hz. The decim=%i parameter will result in a sampling '
+             'frequency of %g Hz, which can cause aliasing artifacts.'
+             % (lowpass, decim, new_sfreq))  # > 50% nyquist lim
+    offset = int(offset)
+    if not 0 <= offset < decim:
+        raise ValueError('decim must be at least 0 and less than %s, got '
+                         '%s' % (decim, offset))
+    return decim, offset, new_sfreq
+
+
 class EvokedArray(Evoked):
     """Evoked object from numpy array
 


Test patch:
diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py
--- a/mne/tests/test_epochs.py
+++ b/mne/tests/test_epochs.py
@@ -278,9 +278,9 @@ def test_decim():
     """Test epochs decimation
     """
     # First with EpochsArray
-    n_epochs, n_channels, n_times = 5, 10, 20
     dec_1, dec_2 = 2, 3
     decim = dec_1 * dec_2
+    n_epochs, n_channels, n_times = 5, 10, 20
     sfreq = 1000.
     sfreq_new = sfreq / decim
     data = rng.randn(n_epochs, n_channels, n_times)
@@ -297,13 +297,33 @@ def test_decim():
 
     # Now let's do it with some real data
     raw, events, picks = _get_data()
+    events = events[events[:, 2] == 1][:2]
+    raw.load_data().pick_channels([raw.ch_names[pick] for pick in picks[::30]])
+    raw.info.normalize_proj()
+    del picks
     sfreq_new = raw.info['sfreq'] / decim
-    raw.info['lowpass'] = sfreq_new / 4.  # suppress aliasing warnings
-    epochs = Epochs(raw, events, event_id, tmin, tmax, picks=picks,
-                    preload=False)
+    raw.info['lowpass'] = sfreq_new / 12.  # suppress aliasing warnings
     assert_raises(ValueError, epochs.decimate, -1)
     assert_raises(ValueError, epochs.decimate, 2, offset=-1)
     assert_raises(ValueError, epochs.decimate, 2, offset=2)
+    for this_offset in range(decim):
+        epochs = Epochs(raw, events, event_id,
+                        tmin=-this_offset / raw.info['sfreq'],
+                        tmax=tmax, preload=False)
+        idx_offsets = np.arange(decim) + this_offset
+        for offset, idx_offset in zip(np.arange(decim), idx_offsets):
+            expected_times = epochs.times[idx_offset::decim]
+            expected_data = epochs.get_data()[:, :, idx_offset::decim]
+            must_have = offset / float(epochs.info['sfreq'])
+            assert_true(np.isclose(must_have, expected_times).any())
+            ep_decim = epochs.copy().decimate(decim, offset)
+            assert_true(np.isclose(must_have, ep_decim.times).any())
+            assert_allclose(ep_decim.times, expected_times)
+            assert_allclose(ep_decim.get_data(), expected_data)
+            assert_equal(ep_decim.info['sfreq'], sfreq_new)
+
+    # More complex cases
+    epochs = Epochs(raw, events, event_id, tmin, tmax, preload=False)
     expected_data = epochs.get_data()[:, :, ::decim]
     expected_times = epochs.times[::decim]
     for preload in (True, False):
@@ -1155,7 +1175,7 @@ def test_resample():
     assert_true(epochs_resampled is epochs)
 
     # test proper setting of times (#2645)
-    n_trial, n_chan, n_time, sfreq = 1, 1, 10, 1000
+    n_trial, n_chan, n_time, sfreq = 1, 1, 10, 1000.
     data = np.zeros((n_trial, n_chan, n_time))
     events = np.zeros((n_trial, 3), int)
     info = create_info(n_chan, sfreq, 'eeg')
@@ -1168,6 +1188,20 @@ def test_resample():
     for e in epochs1, epochs2, epochs:
         assert_equal(e.times[0], epochs.tmin)
         assert_equal(e.times[-1], epochs.tmax)
+    # test that cropping after resampling works (#3296)
+    this_tmin = -0.002
+    epochs = EpochsArray(data, deepcopy(info), events, tmin=this_tmin)
+    for times in (epochs.times, epochs._raw_times):
+        assert_allclose(times, np.arange(n_time) / sfreq + this_tmin)
+    epochs.resample(info['sfreq'] * 2.)
+    for times in (epochs.times, epochs._raw_times):
+        assert_allclose(times, np.arange(2 * n_time) / (sfreq * 2) + this_tmin)
+    epochs.crop(0, None)
+    for times in (epochs.times, epochs._raw_times):
+        assert_allclose(times, np.arange((n_time - 2) * 2) / (sfreq * 2))
+    epochs.resample(sfreq)
+    for times in (epochs.times, epochs._raw_times):
+        assert_allclose(times, np.arange(n_time - 2) / sfreq)
 
 
 def test_detrend():
diff --git a/mne/tests/test_evoked.py b/mne/tests/test_evoked.py
--- a/mne/tests/test_evoked.py
+++ b/mne/tests/test_evoked.py
@@ -16,9 +16,9 @@
 from nose.tools import assert_true, assert_raises, assert_not_equal
 
 from mne import (equalize_channels, pick_types, read_evokeds, write_evokeds,
-                 grand_average, combine_evoked, create_info)
+                 grand_average, combine_evoked, create_info, read_events, io,
+                 Epochs, EpochsArray)
 from mne.evoked import _get_peak, Evoked, EvokedArray
-from mne.epochs import EpochsArray
 from mne.tests.common import assert_naming
 from mne.utils import (_TempDir, requires_pandas, slow_test, requires_version,
                        run_tests_if_main)
@@ -26,10 +26,51 @@
 
 warnings.simplefilter('always')
 
-fname = op.join(op.dirname(__file__), '..', 'io', 'tests', 'data',
-                'test-ave.fif')
-fname_gz = op.join(op.dirname(__file__), '..', 'io', 'tests', 'data',
-                   'test-ave.fif.gz')
+base_dir = op.join(op.dirname(__file__), '..', 'io', 'tests', 'data')
+fname = op.join(base_dir, 'test-ave.fif')
+fname_gz = op.join(base_dir, 'test-ave.fif.gz')
+raw_fname = op.join(base_dir, 'test_raw.fif')
+event_name = op.join(base_dir, 'test-eve.fif')
+
+
+def test_decim():
+    """Test evoked decimation"""
+    rng = np.random.RandomState(0)
+    n_epochs, n_channels, n_times = 5, 10, 20
+    dec_1, dec_2 = 2, 3
+    decim = dec_1 * dec_2
+    sfreq = 1000.
+    sfreq_new = sfreq / decim
+    data = rng.randn(n_epochs, n_channels, n_times)
+    events = np.array([np.arange(n_epochs), [0] * n_epochs, [1] * n_epochs]).T
+    info = create_info(n_channels, sfreq, 'eeg')
+    info['lowpass'] = sfreq_new / float(decim)
+    epochs = EpochsArray(data, info, events)
+    data_epochs = epochs.copy().decimate(decim).get_data()
+    data_epochs_2 = epochs.copy().decimate(decim, offset=1).get_data()
+    data_epochs_3 = epochs.decimate(dec_1).decimate(dec_2).get_data()
+    assert_array_equal(data_epochs, data[:, :, ::decim])
+    assert_array_equal(data_epochs_2, data[:, :, 1::decim])
+    assert_array_equal(data_epochs, data_epochs_3)
+
+    # Now let's do it with some real data
+    raw = io.read_raw_fif(raw_fname)
+    events = read_events(event_name)
+    sfreq_new = raw.info['sfreq'] / decim
+    raw.info['lowpass'] = sfreq_new / 4.  # suppress aliasing warnings
+    picks = pick_types(raw.info, meg=True, eeg=True, exclude=())
+    epochs = Epochs(raw, events, 1, -0.2, 0.5, picks=picks, preload=True)
+    for offset in (0, 1):
+        ev_ep_decim = epochs.copy().decimate(decim, offset).average()
+        ev_decim = epochs.average().decimate(decim, offset)
+        expected_times = epochs.times[offset::decim]
+        assert_allclose(ev_decim.times, expected_times)
+        assert_allclose(ev_ep_decim.times, expected_times)
+        expected_data = epochs.get_data()[:, :, offset::decim].mean(axis=0)
+        assert_allclose(ev_decim.data, expected_data)
+        assert_allclose(ev_ep_decim.data, expected_data)
+        assert_equal(ev_decim.info['sfreq'], sfreq_new)
+        assert_array_equal(ev_decim.times, expected_times)
 
 
 @requires_version('scipy', '0.14')