Repository: mne-tools/mne-python

Issue:
"reject_tmin" and "reject_tmax" params in Epochs are not applied for "reject_by_annotation"
first reported [on gitter](https://gitter.im/mne-tools/mne-python?at=5efdb0046c06cd1bf469ce70)

#### Describe the bug
The `reject_tmin` and `reject_tmax` parameters from [`Epochs`](https://mne.tools/dev/generated/mne.Epochs.html?highlight=epochs#mne.Epochs) are not used for the `reject_by_annotation` parameter. But instead only for the `reject` and `flat` parameters.

Look at [this part in the code](https://github.com/mne-tools/mne-python/blob/517c49612da33491fe4f147588deb00072526874/mne/epochs.py#L2130-L2135).

#### Steps to reproduce

MWE:

```Python
# Create raw data
sfreq = 1000
times = np.arange(0, 10, 0.001)
sin = np.sin(times * 10)
cos = np.cos(times * 10)
data = np.array([sin, cos])
ch_types = ['mag', 'grad']
ch_names = ['sin', 'cos']
info = mne.create_info(ch_names=ch_names, sfreq=sfreq, ch_types=ch_types)
raw = mne.io.RawArray(data, info)

# We have an event at 2 seconds
events = np.array([[2000, 0, 1]])

# There is a bad segment from 1 to 1.5 seconds
onset=[1]
duration=[0.5]
description=['BAD']
annots = mne.Annotations(onset, duration, description)

raw.set_annotations(annots)

# Create epochs from 1s to 3s, centered on the event at 2s.
# this epoch will be rejected_by_annotation,  because there is an overlap
# with a bad segment (1s to 1.5s)
epochs = mne.Epochs(raw, events, tmin=-1, tmax=1, preload=True, reject_by_annotation=True)
assert len(epochs) == 0

# Now setting `reject_tmin` so that there is NO overlap ... should not reject
# any epochs ... because bad segment is from 1s to 1.5s ... but epochs "reject" window
# is from 1.8s to 3s
epochs = mne.Epochs(raw, events, tmin=-1, tmax=1, reject_tmin=-0.2, preload=True, reject_by_annotation=True)

# This errors
assert len(epochs) == 1, "oups!"
```


#### Expected results
`reject_tmin` and `reject_tmax` should have an effect on how `reject_by_annotation` is applied.

Example: When an epoch from -2s to 3s relative to 0 was formed with `reject_tmin=-0.2`, then that epoch MUST NOT be rejected if there is an overlap with a `BAD` annotation from, say -2s to -1.5s




Discussion:
Agreed, @sappelhoff do you have time to look into it?
yes, I'll look into it!

Resolving patch:
diff --git a/mne/epochs.py b/mne/epochs.py
--- a/mne/epochs.py
+++ b/mne/epochs.py
@@ -2115,23 +2115,40 @@ def _get_epoch_from_raw(self, idx, verbose=None):
         Returns
         -------
         data : array | str | None
-            If string it's details on rejection reason.
-            If None it means no data.
+            If string, it's details on rejection reason.
+            If array, it's the data in the desired range (good segment)
+            If None, it means no data is available.
         """
         if self._raw is None:
             # This should never happen, as raw=None only if preload=True
-            raise ValueError('An error has occurred, no valid raw file found.'
-                             ' Please report this to the mne-python '
+            raise ValueError('An error has occurred, no valid raw file found. '
+                             'Please report this to the mne-python '
                              'developers.')
         sfreq = self._raw.info['sfreq']
         event_samp = self.events[idx, 0]
-        # Read a data segment
+        # Read a data segment from "start" to "stop" in samples
         first_samp = self._raw.first_samp
         start = int(round(event_samp + self._raw_times[0] * sfreq))
         start -= first_samp
         stop = start + len(self._raw_times)
+
+        # reject_tmin, and reject_tmax need to be converted to samples to
+        # check the reject_by_annotation boundaries: reject_start, reject_stop
+        reject_tmin = self.reject_tmin
+        if reject_tmin is None:
+            reject_tmin = self._raw_times[0]
+        reject_start = int(round(event_samp + reject_tmin * sfreq))
+        reject_start -= first_samp
+
+        reject_tmax = self.reject_tmax
+        if reject_tmax is None:
+            reject_tmax = self._raw_times[-1]
+        diff = int(round((self._raw_times[-1] - reject_tmax) * sfreq))
+        reject_stop = stop - diff
+
         logger.debug('    Getting epoch for %d-%d' % (start, stop))
         data = self._raw._check_bad_segment(start, stop, self.picks,
+                                            reject_start, reject_stop,
                                             self.reject_by_annotation)
         return data
 
diff --git a/mne/io/base.py b/mne/io/base.py
--- a/mne/io/base.py
+++ b/mne/io/base.py
@@ -431,6 +431,7 @@ def _read_segment_file(self, data, idx, fi, start, stop, cals, mult):
         raise NotImplementedError
 
     def _check_bad_segment(self, start, stop, picks,
+                           reject_start, reject_stop,
                            reject_by_annotation=False):
         """Check if data segment is bad.
 
@@ -446,6 +447,10 @@ def _check_bad_segment(self, start, stop, picks,
             End of the slice.
         picks : array of int
             Channel picks.
+        reject_start : int
+            First sample to check for overlaps with bad annotations.
+        reject_stop : int
+            Last sample to check for overlaps with bad annotations.
         reject_by_annotation : bool
             Whether to perform rejection based on annotations.
             False by default.
@@ -462,9 +467,9 @@ def _check_bad_segment(self, start, stop, picks,
             annot = self.annotations
             sfreq = self.info['sfreq']
             onset = _sync_onset(self, annot.onset)
-            overlaps = np.where(onset < stop / sfreq)
+            overlaps = np.where(onset < reject_stop / sfreq)
             overlaps = np.where(onset[overlaps] + annot.duration[overlaps] >
-                                start / sfreq)
+                                reject_start / sfreq)
             for descr in annot.description[overlaps]:
                 if descr.lower().startswith('bad'):
                     return descr


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
@@ -400,6 +400,32 @@ def test_reject():
         raw.set_annotations(None)
 
 
+def test_reject_by_annotations_reject_tmin_reject_tmax():
+    """Test reject_by_annotations with reject_tmin and reject_tmax defined."""
+    # 10 seconds of data, event at 2s, bad segment from 1s to 1.5s
+    info = mne.create_info(ch_names=['test_a'], sfreq=1000, ch_types='eeg')
+    raw = mne.io.RawArray(np.atleast_2d(np.arange(0, 10, 1 / 1000)), info=info)
+    events = np.array([[2000, 0, 1]])
+    raw.set_annotations(mne.Annotations(1, 0.5, 'BAD'))
+
+    # Make the epoch based on the event at 2s, so from 1s to 3s ... assert it
+    # is rejected due to bad segment overlap from 1s to 1.5s
+    epochs = mne.Epochs(raw, events, tmin=-1, tmax=1,
+                        preload=True, reject_by_annotation=True)
+    assert len(epochs) == 0
+
+    # Setting `reject_tmin` to prevent rejection of epoch.
+    epochs = mne.Epochs(raw, events, tmin=-1, tmax=1, reject_tmin=-0.2,
+                        preload=True, reject_by_annotation=True)
+    assert len(epochs) == 1
+
+    # Same check but bad segment overlapping from 2.5s to 3s: use `reject_tmax`
+    raw.set_annotations(mne.Annotations(2.5, 0.5, 'BAD'))
+    epochs = mne.Epochs(raw, events, tmin=-1, tmax=1, reject_tmax=0.4,
+                        preload=True, reject_by_annotation=True)
+    assert len(epochs) == 1
+
+
 def test_own_data():
     """Test for epochs data ownership (gh-5346)."""
     raw, events = _get_data()[:2]