Repository: mne-tools/mne-python

Issue:
BUG?: resample + concatenation breaks
I'm regularly confronted to an error when I resample a set of epochs, and try to concatenate them.

```
import numpy as np
from mne.epochs import concatenate_epochs  # BTW, why is concatenate_epochs not accessible from mne directly
nepoch, nchan, ntime = 100, 30, 20
# make dummy epochs1 and epoch2, sfreq = 250 Hz
epochs = concatenate_epochs([epochs1, epochs2])
# works

epochs1.resample(epochs1.info['sfreq'] // 2)
epochs2.resample(epochs2.info['sfreq'] // 2)
epochs = concatenate_epochs([epochs1, epochs2])
# error
```

```
RuntimeError                              Traceback (most recent call last)
<ipython-input-6-28041ffc5880> in <module>()
      4 epochs1.resample(125)
      5 epochs2.resample(125)
----> 6 epochs = concatenate_epochs([epochs1, epochs2])

/home/jrking/mne-python/mne/epochs.pyc in concatenate_epochs(epochs_list)
   2602     .. versionadded:: 0.9.0
   2603     """
-> 2604     return _finish_concat(*_concatenate_epochs(epochs_list))

/home/jrking/mne-python/mne/epochs.pyc in _finish_concat(info, data, events, event_id, tmin, tmax, baseline, selection, drop_log, verbose)
   2580                       baseline=baseline, add_eeg_ref=False,
   2581                       selection=selection, drop_log=drop_log,
-> 2582                       proj=False, on_missing='ignore', verbose=verbose)
   2583     out.drop_bad_epochs()
   2584     return out

/home/jrking/mne-python/mne/epochs.pyc in __init__(***failed resolving arguments***)
    280             if data.ndim != 3 or data.shape[2] != \
    281                     round((tmax - tmin) * self.info['sfreq']) + 1:
--> 282                 raise RuntimeError('bad data shape')
    283             self.preload = True
    284             self._data = data

RuntimeError: bad data shape
```

I think we already mentioned this error with @Eric89GXL , but I haven't found the issue.



Discussion:
Can you fill out your minimal example with e.g. EpochsArray? That way others can run a bit of code to confirm/test more easily.

Sure

```
import copy
import numpy as np
from mne.epochs import EpochsArray
from mne.io.meas_info import create_info
from mne.epochs import concatenate_epochs  # BTW, why is concatenate_epochs not accessible from mne directly

n_trial, n_chan, n_time, sfreq  = 100, 30, 20, 250
data = np.zeros((n_trial, n_chan, n_time ))
chan_names = ['meg_%02i' % chan for chan in range(n_chan)]
chan_types = ['mag'] * n_chan
events = np.zeros((n_trial, 3), int)
info = create_info(chan_names, sfreq, chan_types)
epochs1 = EpochsArray(copy.deepcopy(data), copy.deepcopy(info),
                      events=np.array(events, int), verbose=False)
epochs2 = EpochsArray(copy.deepcopy(data), copy.deepcopy(info),
                      events=np.array(events, int), verbose=False)

# make dummy epochs1 and epoch2, sfreq = 250 Hz
epochs = concatenate_epochs([epochs1, epochs2])
# works

epochs1.resample(epochs1.info['sfreq'] // 2)
epochs2.resample(epochs2.info['sfreq'] // 2)
epochs = concatenate_epochs([epochs1, epochs2])
# error

```


Resolving patch:
diff --git a/mne/epochs.py b/mne/epochs.py
--- a/mne/epochs.py
+++ b/mne/epochs.py
@@ -87,7 +87,7 @@ def _save_split(epochs, fname, part_idx, n_parts):
     end_block(fid, FIFF.FIFFB_MNE_EVENTS)
 
     # First and last sample
-    first = int(epochs.times[0] * info['sfreq'])
+    first = int(round(epochs.tmin * info['sfreq']))  # round just to be safe
     last = first + len(epochs.times) - 1
     write_int(fid, FIFF.FIFF_FIRST_SAMPLE, first)
     write_int(fid, FIFF.FIFF_LAST_SAMPLE, last)
@@ -254,8 +254,6 @@ def __init__(self, info, data, events, event_id, tmin, tmax,
         if tmin > tmax:
             raise ValueError('tmin has to be less than or equal to tmax')
 
-        self.tmin = tmin
-        self.tmax = tmax
         self.baseline = baseline
         self.reject_tmin = reject_tmin
         self.reject_tmax = reject_tmax
@@ -286,11 +284,11 @@ def __init__(self, info, data, events, event_id, tmin, tmax,
 
         # Handle times
         sfreq = float(self.info['sfreq'])
-        start_idx = int(round(self.tmin * sfreq))
+        start_idx = int(round(tmin * sfreq))
         self._raw_times = np.arange(start_idx,
-                                    int(round(self.tmax * sfreq)) + 1) / sfreq
+                                    int(round(tmax * sfreq)) + 1) / sfreq
+        self.times = self._raw_times.copy()
         self._decim = 1
-        # this method sets the self.times property
         self.decimate(decim)
 
         # setup epoch rejection
@@ -1285,6 +1283,14 @@ def next(self, return_event_id=False):
 
         return epoch if not return_event_id else epoch, self.event_id
 
+    @property
+    def tmin(self):
+        return self.times[0]
+
+    @property
+    def tmax(self):
+        return self.times[-1]
+
     def __repr__(self):
         """ Build string representation
         """
@@ -1395,11 +1401,7 @@ def crop(self, tmin=None, tmax=None, copy=False):
             tmax = self.tmax
 
         tmask = _time_mask(self.times, tmin, tmax)
-        tidx = np.where(tmask)[0]
-
         this_epochs = self if not copy else self.copy()
-        this_epochs.tmin = this_epochs.times[tidx[0]]
-        this_epochs.tmax = this_epochs.times[tidx[-1]]
         this_epochs.times = this_epochs.times[tmask]
         this_epochs._raw_times = this_epochs._raw_times[tmask]
         this_epochs._data = this_epochs._data[:, :, tmask]
@@ -1450,7 +1452,6 @@ def resample(self, sfreq, npad=100, window='boxcar', n_jobs=1,
         inst.info['sfreq'] = sfreq
         inst.times = (np.arange(inst._data.shape[2], dtype=np.float) /
                       sfreq + inst.times[0])
-
         return inst
 
     def copy(self):
@@ -2437,32 +2438,15 @@ def bootstrap(epochs, random_state=None):
 
 def _check_merge_epochs(epochs_list):
     """Aux function"""
-    event_ids = set(tuple(epochs.event_id.items()) for epochs in epochs_list)
-    if len(event_ids) == 1:
-        event_id = dict(event_ids.pop())
-    else:
+    if len(set(tuple(epochs.event_id.items()) for epochs in epochs_list)) != 1:
         raise NotImplementedError("Epochs with unequal values for event_id")
-
-    tmins = set(epochs.tmin for epochs in epochs_list)
-    if len(tmins) == 1:
-        tmin = tmins.pop()
-    else:
+    if len(set(epochs.tmin for epochs in epochs_list)) != 1:
         raise NotImplementedError("Epochs with unequal values for tmin")
-
-    tmaxs = set(epochs.tmax for epochs in epochs_list)
-    if len(tmaxs) == 1:
-        tmax = tmaxs.pop()
-    else:
+    if len(set(epochs.tmax for epochs in epochs_list)) != 1:
         raise NotImplementedError("Epochs with unequal values for tmax")
-
-    baselines = set(epochs.baseline for epochs in epochs_list)
-    if len(baselines) == 1:
-        baseline = baselines.pop()
-    else:
+    if len(set(epochs.baseline for epochs in epochs_list)) != 1:
         raise NotImplementedError("Epochs with unequal values for baseline")
 
-    return event_id, tmin, tmax, baseline
-
 
 @verbose
 def add_channels_epochs(epochs_list, name='Unknown', add_eeg_ref=True,
@@ -2492,8 +2476,7 @@ def add_channels_epochs(epochs_list, name='Unknown', add_eeg_ref=True,
 
     info = _merge_info([epochs.info for epochs in epochs_list])
     data = [epochs.get_data() for epochs in epochs_list]
-    event_id, tmin, tmax, baseline = _check_merge_epochs(epochs_list)
-
+    _check_merge_epochs(epochs_list)
     for d in data:
         if len(d) != len(data[0]):
             raise ValueError('all epochs must be of the same length')
@@ -2517,10 +2500,6 @@ def add_channels_epochs(epochs_list, name='Unknown', add_eeg_ref=True,
 
     epochs = epochs_list[0].copy()
     epochs.info = info
-    epochs.event_id = event_id
-    epochs.tmin = tmin
-    epochs.tmax = tmax
-    epochs.baseline = baseline
     epochs.picks = None
     epochs.name = name
     epochs.verbose = verbose


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
@@ -967,6 +967,21 @@ def test_resample():
     epochs_resampled = epochs.resample(sfreq_normal * 2, npad=0, copy=False)
     assert_true(epochs_resampled is epochs)
 
+    # test proper setting of times (#2645)
+    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')
+    epochs1 = EpochsArray(data, deepcopy(info), events)
+    epochs2 = EpochsArray(data, deepcopy(info), events)
+    epochs = concatenate_epochs([epochs1, epochs2])
+    epochs1.resample(epochs1.info['sfreq'] // 2)
+    epochs2.resample(epochs2.info['sfreq'] // 2)
+    epochs = concatenate_epochs([epochs1, epochs2])
+    for e in epochs1, epochs2, epochs:
+        assert_equal(e.times[0], epochs.tmin)
+        assert_equal(e.times[-1], epochs.tmax)
+
 
 def test_detrend():
     """Test detrending of epochs
@@ -1636,12 +1651,12 @@ def make_epochs(picks, proj):
                   [epochs_meg2, epochs_eeg])
 
     epochs_meg2 = epochs_meg.copy()
-    epochs_meg2.tmin += 0.4
+    epochs_meg2.times += 0.4
     assert_raises(NotImplementedError, add_channels_epochs,
                   [epochs_meg2, epochs_eeg])
 
     epochs_meg2 = epochs_meg.copy()
-    epochs_meg2.tmin += 0.5
+    epochs_meg2.times += 0.5
     assert_raises(NotImplementedError, add_channels_epochs,
                   [epochs_meg2, epochs_eeg])