Repository: mne-tools/mne-python

Issue:
Epochs.reject_tmin and reject_tmax aren't being adjusted for deviations due to sampling rate
MWE:

```python
import os
import mne

sample_data_folder = mne.datasets.sample.data_path()
sample_data_raw_file = os.path.join(sample_data_folder, 'MEG', 'sample',
                                    'sample_audvis_raw.fif')

raw = mne.io.read_raw_fif(sample_data_raw_file, verbose=False).crop(tmax=60)
events = mne.find_events(raw, stim_channel='STI 014')

tmin, tmax = -0.2, 0.8
epochs = mne.Epochs(raw, events,
                    tmin=tmin, tmax=tmax,
                    reject_tmin=tmin, reject_tmax=tmax)

print(epochs.tmin)
print(epochs.reject_tmin)
print(epochs.tmax)
print(epochs.reject_tmax)
```

Output:
```python
-0.19979521315838786
-0.2
0.7991808526335514
0.8
```

Saving above Epochs to disk and trying to load them will fail:
```python
epochs_fname = '/tmp/epochs-epo.fif'

epochs.save(epochs_fname)
epochs_loaded = mne.read_epochs(epochs_fname)
```

Output:
```python
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Untitled-1 in 
     18 epochs.save(epochs_fname, overwrite=True)
     19 
---> 20 epochs_read = mne.read_epochs(epochs_fname)
     21 

<decorator-gen-212> in read_epochs(fname, proj, preload, verbose)

~/Development/mne-python/mne/epochs.py in read_epochs(fname, proj, preload, verbose)
   2704         The epochs.
   2705     """
-> 2706     return EpochsFIF(fname, proj, preload, verbose)
   2707 
   2708 

<decorator-gen-213> in __init__(self, fname, proj, preload, verbose)

~/Development/mne-python/mne/epochs.py in __init__(self, fname, proj, preload, verbose)
   2819         # again, ensure we're retaining the baseline period originally loaded
   2820         # from disk without trying to re-apply baseline correction
-> 2821         super(EpochsFIF, self).__init__(
   2822             info, data, events, event_id, tmin, tmax, baseline=None, raw=raw,
   2823             proj=proj, preload_at_end=False, on_missing='ignore',

<decorator-gen-198> in __init__(self, info, data, events, event_id, tmin, tmax, baseline, raw, picks, reject, flat, decim, reject_tmin, reject_tmax, detrend, proj, on_missing, preload_at_end, selection, drop_log, filename, metadata, event_repeated, verbose)

~/Development/mne-python/mne/epochs.py in __init__(***failed resolving arguments***)
    481         # check reject_tmin and reject_tmax
    482         if (reject_tmin is not None) and (reject_tmin < tmin):
--> 483             raise ValueError("reject_tmin needs to be None or >= tmin")
    484         if (reject_tmax is not None) and (reject_tmax > tmax):
    485             raise ValueError("reject_tmax needs to be None or <= tmax")

ValueError: reject_tmin needs to be None or >= tmin
```




Discussion:
can you have a look?

>

> can you have a look?


Yes I can work on this tomorrow.

Resolving patch:
diff --git a/mne/epochs.py b/mne/epochs.py
--- a/mne/epochs.py
+++ b/mne/epochs.py
@@ -478,20 +478,10 @@ def __init__(self, info, data, events, event_id=None, tmin=-0.2, tmax=0.5,
             self.metadata = metadata
             # do not set self.events here, let subclass do it
 
-        # check reject_tmin and reject_tmax
-        if (reject_tmin is not None) and (reject_tmin < tmin):
-            raise ValueError("reject_tmin needs to be None or >= tmin")
-        if (reject_tmax is not None) and (reject_tmax > tmax):
-            raise ValueError("reject_tmax needs to be None or <= tmax")
-        if (reject_tmin is not None) and (reject_tmax is not None):
-            if reject_tmin >= reject_tmax:
-                raise ValueError('reject_tmin needs to be < reject_tmax')
         if (detrend not in [None, 0, 1]) or isinstance(detrend, bool):
             raise ValueError('detrend must be None, 0, or 1')
-
-        self.reject_tmin = reject_tmin
-        self.reject_tmax = reject_tmax
         self.detrend = detrend
+
         self._raw = raw
         info._check_consistency()
         self.picks = _picks_to_idx(info, picks, none='all', exclude=(),
@@ -526,6 +516,33 @@ def __init__(self, info, data, events, event_id=None, tmin=-0.2, tmax=0.5,
         self._raw_times = np.arange(start_idx,
                                     int(round(tmax * sfreq)) + 1) / sfreq
         self._set_times(self._raw_times)
+
+        # check reject_tmin and reject_tmax
+        if reject_tmin is not None:
+            if (np.isclose(reject_tmin, tmin)):
+                # adjust for potential small deviations due to sampling freq
+                reject_tmin = self.tmin
+            elif reject_tmin < tmin:
+                raise ValueError(f'reject_tmin needs to be None or >= tmin '
+                                 f'(got {reject_tmin})')
+
+        if reject_tmax is not None:
+            if (np.isclose(reject_tmax, tmax)):
+                # adjust for potential small deviations due to sampling freq
+                reject_tmax = self.tmax
+            elif reject_tmax > tmax:
+                raise ValueError(f'reject_tmax needs to be None or <= tmax '
+                                 f'(got {reject_tmax})')
+
+        if (reject_tmin is not None) and (reject_tmax is not None):
+            if reject_tmin >= reject_tmax:
+                raise ValueError(f'reject_tmin ({reject_tmin}) needs to be '
+                                 f' < reject_tmax ({reject_tmax})')
+
+        self.reject_tmin = reject_tmin
+        self.reject_tmax = reject_tmax
+
+        # decimation
         self._decim = 1
         self.decimate(decim)
 
@@ -1560,6 +1577,18 @@ def crop(self, tmin=None, tmax=None, include_tmax=True, verbose=None):
         self._set_times(self.times[tmask])
         self._raw_times = self._raw_times[tmask]
         self._data = self._data[:, :, tmask]
+
+        # Adjust rejection period
+        if self.reject_tmin is not None and self.reject_tmin < self.tmin:
+            logger.info(
+                f'reject_tmin is not in epochs time interval. '
+                f'Setting reject_tmin to epochs.tmin ({self.tmin} sec)')
+            self.reject_tmin = self.tmin
+        if self.reject_tmax is not None and self.reject_tmax > self.tmax:
+            logger.info(
+                f'reject_tmax is not in epochs time interval. '
+                f'Setting reject_tmax to epochs.tmax ({self.tmax} sec)')
+            self.reject_tmax = self.tmax
         return self
 
     def copy(self):


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
@@ -1384,8 +1384,11 @@ def test_evoked_standard_error(tmpdir):
             assert ave.first == ave2.first
 
 
-def test_reject_epochs():
+def test_reject_epochs(tmpdir):
     """Test of epochs rejection."""
+    tempdir = str(tmpdir)
+    temp_fname = op.join(tempdir, 'test-epo.fif')
+
     raw, events, picks = _get_data()
     events1 = events[events[:, 2] == event_id]
     epochs = Epochs(raw, events1, event_id, tmin, tmax,
@@ -1437,6 +1440,17 @@ def test_reject_epochs():
     data = epochs[0].get_data()[0]
     assert epochs._is_good_epoch(data) == (True, None)
 
+    # Check that reject_tmin and reject_tmax are being adjusted for small time
+    # inaccuracies due to sfreq
+    epochs = Epochs(raw=raw, events=events1, event_id=event_id,
+                    tmin=tmin, tmax=tmax, reject_tmin=tmin, reject_tmax=tmax)
+    assert epochs.tmin != tmin
+    assert epochs.tmax != tmax
+    assert np.isclose(epochs.tmin, epochs.reject_tmin)
+    assert np.isclose(epochs.tmax, epochs.reject_tmax)
+    epochs.save(temp_fname, overwrite=True)
+    read_epochs(temp_fname)
+
 
 def test_preload_epochs():
     """Test preload of epochs."""
@@ -1525,8 +1539,11 @@ def test_comparision_with_c():
     assert_array_almost_equal(evoked.times, c_evoked.times, 12)
 
 
-def test_crop():
+def test_crop(tmpdir):
     """Test of crop of epochs."""
+    tempdir = str(tmpdir)
+    temp_fname = op.join(tempdir, 'test-epo.fif')
+
     raw, events, picks = _get_data()
     epochs = Epochs(raw, events[:5], event_id, tmin, tmax, picks=picks,
                     preload=False, reject=reject, flat=flat)
@@ -1584,6 +1601,24 @@ def test_crop():
         pytest.raises(ValueError, epochs.crop, 1000, 2000)
         pytest.raises(ValueError, epochs.crop, 0.1, 0)
 
+    # Test that cropping adjusts reject_tmin and reject_tmax if need be.
+    epochs = Epochs(raw=raw, events=events[:5], event_id=event_id,
+                    tmin=tmin, tmax=tmax, reject_tmin=tmin, reject_tmax=tmax)
+    epochs.load_data()
+    epochs_cropped = epochs.copy().crop(0, None)
+    assert np.isclose(epochs_cropped.tmin, epochs_cropped.reject_tmin)
+
+    epochs_cropped = epochs.copy().crop(None, 0.1)
+    assert np.isclose(epochs_cropped.tmax, epochs_cropped.reject_tmax)
+    del epochs_cropped
+
+    # Cropping & I/O roundtrip
+    epochs.crop(0, 0.1)
+    epochs.save(temp_fname)
+    epochs_read = mne.read_epochs(temp_fname)
+    assert np.isclose(epochs_read.tmin, epochs_read.reject_tmin)
+    assert np.isclose(epochs_read.tmax, epochs_read.reject_tmax)
+
 
 def test_resample():
     """Test of resample of epochs."""