Repository: mne-tools/mne-python

Issue:
Improve error message when using default baseline with tmin > 0
``` python
epochs = Epochs(raw, events, tmin=1.0, tmax=2.0)
```

In this case, the default baseline is computed on the interval [tmin, 0], which raises an error as this interval is empty. We could have a better error message in this case.

Current error:

```
...

/cal/homes/tdupre/work/src/mne-python/mne/epochs.pyc in _detrend_offset_decim(self, epoch, verbose)
    554                            emg=True, exclude=[])
    555         epoch[picks] = rescale(epoch[picks], self._raw_times, self.baseline,
--> 556                                copy=False, verbose=False)
    557 
    558         # handle offset

/cal/homes/tdupre/work/src/mne-python/mne/baseline.pyc in rescale(data, times, baseline, mode, copy, verbose)

/cal/homes/tdupre/work/src/mne-python/mne/utils.pyc in verbose(function, *args, **kwargs)
    600         # set it back if we get an exception
    601         with use_log_level(verbose_level):
--> 602             return function(*args, **kwargs)
    603     return function(*args, **kwargs)
    604 

/cal/homes/tdupre/work/src/mne-python/mne/baseline.pyc in rescale(data, times, baseline, mode, copy, verbose)
     74             imax = len(times)
     75         else:
---> 76             imax = int(np.where(times <= bmax)[0][-1]) + 1
     77 
     78         # avoid potential "empty slice" warning

IndexError: index -1 is out of bounds for axis 0 with size 0
```



Discussion:
As we discussed, you have my blessing and +1 :)


Resolving patch:
diff --git a/mne/baseline.py b/mne/baseline.py
--- a/mne/baseline.py
+++ b/mne/baseline.py
@@ -36,12 +36,12 @@ def rescale(data, times, baseline, mode='mean', copy=True, verbose=None):
         Time instants is seconds.
     baseline : tuple or list of length 2, or None
         The time interval to apply rescaling / baseline correction.
-        If None do not apply it. If baseline is (a, b)
-        the interval is between "a (s)" and "b (s)".
-        If a is None the beginning of the data is used
-        and if b is None then b is set to the end of the interval.
-        If baseline is equal ot (None, None) all the time
-        interval is used. If None, no correction is applied.
+        If None do not apply it. If baseline is ``(bmin, bmax)``
+        the interval is between ``bmin`` (s) and ``bmax`` (s).
+        If ``bmin is None`` the beginning of the data is used
+        and if ``bmax is None`` then ``bmax`` is set to the end of the
+        interval. If baseline is ``(None, None)`` the entire time
+        interval is used. If baseline is None, no correction is applied.
     mode : 'logratio' | 'ratio' | 'zscore' | 'mean' | 'percent' | 'zlogratio'
         Do baseline correction with ratio (power is divided by mean
         power during baseline) or zscore (power is divided by standard
@@ -68,11 +68,22 @@ def rescale(data, times, baseline, mode='mean', copy=True, verbose=None):
     if bmin is None:
         imin = 0
     else:
-        imin = int(np.where(times >= bmin)[0][0])
+        imin = np.where(times >= bmin)[0]
+        if len(imin) == 0:
+            raise ValueError('bmin is too large (%s), it exceeds the largest '
+                             'time value' % (bmin,))
+        imin = int(imin[0])
     if bmax is None:
         imax = len(times)
     else:
-        imax = int(np.where(times <= bmax)[0][-1]) + 1
+        imax = np.where(times <= bmax)[0]
+        if len(imax) == 0:
+            raise ValueError('bmax is too small (%s), it is smaller than the '
+                             'smallest time value' % (bmax,))
+        imax = int(imax[-1]) + 1
+    if imin >= imax:
+        raise ValueError('Bad rescaling slice (%s:%s) from time values %s, %s'
+                         % (imin, imax, bmin, bmax))
 
     # avoid potential "empty slice" warning
     if data.shape[-1] > 0:
diff --git a/mne/epochs.py b/mne/epochs.py
--- a/mne/epochs.py
+++ b/mne/epochs.py
@@ -241,21 +241,9 @@ def __init__(self, info, data, events, event_id=None, tmin=-0.2, tmax=0.5,
             raise ValueError('detrend must be None, 0, or 1')
 
         # check that baseline is in available data
-        if baseline is not None:
-            baseline_tmin, baseline_tmax = baseline
-            tstep = 1. / info['sfreq']
-            if baseline_tmin is not None:
-                if baseline_tmin < tmin - tstep:
-                    err = ("Baseline interval (tmin = %s) is outside of epoch "
-                           "data (tmin = %s)" % (baseline_tmin, tmin))
-                    raise ValueError(err)
-            if baseline_tmax is not None:
-                if baseline_tmax > tmax + tstep:
-                    err = ("Baseline interval (tmax = %s) is outside of epoch "
-                           "data (tmax = %s)" % (baseline_tmax, tmax))
-                    raise ValueError(err)
         if tmin > tmax:
             raise ValueError('tmin has to be less than or equal to tmax')
+        _check_baseline(baseline, tmin, tmax, info['sfreq'])
         _log_rescale(baseline)
         self.baseline = baseline
         self.reject_tmin = reject_tmin
@@ -451,9 +439,12 @@ def apply_baseline(self, baseline, copy=None, verbose=None):
 
         .. versionadded:: 0.10.0
         """
-        if not isinstance(baseline, tuple) or len(baseline) != 2:
-            raise ValueError('`baseline=%s` is an invalid argument.'
-                             % str(baseline))
+        if not self.preload:
+            # Eventually we can relax this restriction, but it will require
+            # more careful checking of baseline (e.g., refactor with the
+            # _BaseEpochs.__init__ checks)
+            raise RuntimeError('Data must be loaded to apply a new baseline')
+        _check_baseline(baseline, self.tmin, self.tmax, self.info['sfreq'])
 
         epochs = _check_copy_dep(self, copy)
         picks = _pick_data_channels(epochs.info, exclude=[], with_ref_meg=True)
@@ -1855,6 +1846,35 @@ def equalize_event_counts(self, event_ids, method='mintime', copy=None):
         return epochs, indices
 
 
+def _check_baseline(baseline, tmin, tmax, sfreq):
+    """Helper to check for a valid baseline"""
+    if baseline is not None:
+        if not isinstance(baseline, tuple) or len(baseline) != 2:
+            raise ValueError('`baseline=%s` is an invalid argument.'
+                             % str(baseline))
+        baseline_tmin, baseline_tmax = baseline
+        tstep = 1. / float(sfreq)
+        if baseline_tmin is None:
+            baseline_tmin = tmin
+        baseline_tmin = float(baseline_tmin)
+        if baseline_tmax is None:
+            baseline_tmax = tmax
+        baseline_tmax = float(baseline_tmax)
+        if baseline_tmin < tmin - tstep:
+            raise ValueError(
+                "Baseline interval (tmin = %s) is outside of epoch "
+                "data (tmin = %s)" % (baseline_tmin, tmin))
+        if baseline_tmax > tmax + tstep:
+            raise ValueError(
+                "Baseline interval (tmax = %s) is outside of epoch "
+                "data (tmax = %s)" % (baseline_tmax, tmax))
+        if baseline_tmin > baseline_tmax:
+            raise ValueError(
+                "Baseline min (%s) must be less than baseline max (%s)"
+                % (baseline_tmin, baseline_tmax))
+        del baseline_tmin, baseline_tmax
+
+
 def _drop_log_stats(drop_log, ignore=('IGNORED',)):
     """
     Parameters


Test patch:
diff --git a/mne/io/fiff/tests/test_raw_fiff.py b/mne/io/fiff/tests/test_raw_fiff.py
--- a/mne/io/fiff/tests/test_raw_fiff.py
+++ b/mne/io/fiff/tests/test_raw_fiff.py
@@ -1245,6 +1245,13 @@ def compensate_mne(fname, grad):
         raw_py = Raw(ctf_comp_fname, preload=True, compensation=grad)
         raw_c = compensate_mne(ctf_comp_fname, grad)
         assert_allclose(raw_py._data, raw_c._data, rtol=1e-6, atol=1e-17)
+        assert_equal(raw_py.info['nchan'], raw_c.info['nchan'])
+        for ch_py, ch_c in zip(raw_py.info['chs'], raw_c.info['chs']):
+            for key in ('ch_name', 'coil_type', 'scanno', 'logno', 'unit',
+                        'coord_frame', 'kind'):
+                assert_equal(ch_py[key], ch_c[key])
+            for key in ('loc', 'unit_mul', 'range', 'cal'):
+                assert_allclose(ch_py[key], ch_c[key])
 
 
 @testing.requires_testing_data
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
@@ -20,6 +20,7 @@
 from mne import (Epochs, Annotations, read_events, pick_events, read_epochs,
                  equalize_channels, pick_types, pick_channels, read_evokeds,
                  write_evokeds, create_info, make_fixed_length_events)
+from mne.baseline import rescale
 from mne.preprocessing import maxwell_filter
 from mne.epochs import (
     bootstrap, equalize_epoch_counts, combine_event_ids, add_channels_epochs,
@@ -443,6 +444,20 @@ def test_epochs_bad_baseline():
     raw, events = _get_data()[:2]
     assert_raises(ValueError, Epochs, raw, events, None, -0.1, 0.3, (-0.2, 0))
     assert_raises(ValueError, Epochs, raw, events, None, -0.1, 0.3, (0, 0.4))
+    assert_raises(ValueError, Epochs, raw, events, None, -0.1, 0.3, (0.1, 0))
+    assert_raises(ValueError, Epochs, raw, events, None, 0.1, 0.3, (None, 0))
+    assert_raises(ValueError, Epochs, raw, events, None, -0.3, -0.1, (0, None))
+    epochs = Epochs(raw, events, None, 0.1, 0.3, baseline=None)
+    assert_raises(RuntimeError, epochs.apply_baseline, (0.1, 0.2))
+    epochs.load_data()
+    assert_raises(ValueError, epochs.apply_baseline, (None, 0))
+    assert_raises(ValueError, epochs.apply_baseline, (0, None))
+    # put some rescale options here, too
+    data = np.arange(100, dtype=float)
+    assert_raises(ValueError, rescale, data, times=data, baseline=(-2, -1))
+    rescale(data.copy(), times=data, baseline=(2, 2))  # ok
+    assert_raises(ValueError, rescale, data, times=data, baseline=(2, 1))
+    assert_raises(ValueError, rescale, data, times=data, baseline=(100, 101))
 
 
 def test_epoch_combine_ids():