Repository: mne-tools/mne-python

Issue:
Trying to apply rejection thresholds repeatedly to Epochs crashes if only using a subset of initial reject channels
MWE:
```python
from pathlib import Path
import mne


sample_dir = Path(mne.datasets.sample.data_path())
sample_fname = sample_dir / 'MEG' / 'sample' / 'sample_audvis_raw.fif'

raw = mne.io.read_raw_fif(sample_fname)
raw.crop(tmax=60)

events = mne.find_events(raw, stim_channel='STI 014')
event_id = {'auditory/left': 1, 'auditory/right': 2, 'visual/left': 3,
            'visual/right': 4, 'face': 5, 'buttonpress': 32}

epochs = mne.Epochs(raw, events=events, event_id=event_id,
                    tmin=-0.2, tmax=0.5, baseline=(None, 0),
                    preload=True)

# This works as expected
reject_a = dict(grad=8e-10, mag=10e-11)
epochs.drop_bad(reject=reject_a)

# This raises an exception, but I'd expect it to work:
# ValueError: reject["mag"] == inf > 1e-10 (old value), new
# reject values must be at least as stringent as previous ones
reject_b = dict(grad=1e-10)
epochs.drop_bad(reject=reject_b)

# This works as expected
reject_c = dict(grad=1e-10, mag=9e-11)
epochs.drop_bad(reject=reject_c)
```
Trying to apply rejection thresholds repeatedly to Epochs crashes if only using a subset of initial reject channels
MWE:
```python
from pathlib import Path
import mne


sample_dir = Path(mne.datasets.sample.data_path())
sample_fname = sample_dir / 'MEG' / 'sample' / 'sample_audvis_raw.fif'

raw = mne.io.read_raw_fif(sample_fname)
raw.crop(tmax=60)

events = mne.find_events(raw, stim_channel='STI 014')
event_id = {'auditory/left': 1, 'auditory/right': 2, 'visual/left': 3,
            'visual/right': 4, 'face': 5, 'buttonpress': 32}

epochs = mne.Epochs(raw, events=events, event_id=event_id,
                    tmin=-0.2, tmax=0.5, baseline=(None, 0),
                    preload=True)

# This works as expected
reject_a = dict(grad=8e-10, mag=10e-11)
epochs.drop_bad(reject=reject_a)

# This raises an exception, but I'd expect it to work:
# ValueError: reject["mag"] == inf > 1e-10 (old value), new
# reject values must be at least as stringent as previous ones
reject_b = dict(grad=1e-10)
epochs.drop_bad(reject=reject_b)

# This works as expected
reject_c = dict(grad=1e-10, mag=9e-11)
epochs.drop_bad(reject=reject_c)
```


Discussion:
So just to be clear, the bug is that `reject_c` works when it shouldn't.  Right?
Sorry, obviously I wasn't clear enough.

The issue is that `reject_b` doesn't work when it should; `reject_c` works as expected.

The issue is that when calling `drop_bad()` with `reject_a`, we pass a dict that contains PTP thresholds for mag and grad. If I subsequently want to do another round of PTP rejection, but only for one of the two initially used channel types, `drop_bad()` raises an error message, claiming that the PTP threshold for the one channel type I'm omitting now is `inf` and hence "not as stringent as the previous one"

The expected behavior would be that no checks are being done on the PTP thresholds for the omitted channel type.
sorry, I was mixed up about thresholds being "more extreme" versus "more stringent".  Yes, seems like a bug in handling of `reject_b`.
PR welcome

>

So just to be clear, the bug is that `reject_c` works when it shouldn't.  Right?
Sorry, obviously I wasn't clear enough.

The issue is that `reject_b` doesn't work when it should; `reject_c` works as expected.

The issue is that when calling `drop_bad()` with `reject_a`, we pass a dict that contains PTP thresholds for mag and grad. If I subsequently want to do another round of PTP rejection, but only for one of the two initially used channel types, `drop_bad()` raises an error message, claiming that the PTP threshold for the one channel type I'm omitting now is `inf` and hence "not as stringent as the previous one"

The expected behavior would be that no checks are being done on the PTP thresholds for the omitted channel type.
sorry, I was mixed up about thresholds being "more extreme" versus "more stringent".  Yes, seems like a bug in handling of `reject_b`.
PR welcome

>


Resolving patch:
diff --git a/mne/epochs.py b/mne/epochs.py
--- a/mne/epochs.py
+++ b/mne/epochs.py
@@ -744,18 +744,26 @@ def _reject_setup(self, reject, flat):
         bad_msg = ('{kind}["{key}"] == {new} {op} {old} (old value), new '
                    '{kind} values must be at least as stringent as '
                    'previous ones')
-        for key in set(reject.keys()).union(old_reject.keys()):
-            old = old_reject.get(key, np.inf)
-            new = reject.get(key, np.inf)
-            if new > old:
-                raise ValueError(bad_msg.format(kind='reject', key=key,
-                                                new=new, old=old, op='>'))
-        for key in set(flat.keys()).union(old_flat.keys()):
-            old = old_flat.get(key, -np.inf)
-            new = flat.get(key, -np.inf)
-            if new < old:
-                raise ValueError(bad_msg.format(kind='flat', key=key,
-                                                new=new, old=old, op='<'))
+
+        # copy thresholds for channel types that were used previously, but not
+        # passed this time
+        for key in set(old_reject) - set(reject):
+            reject[key] = old_reject[key]
+        # make sure new thresholds are at least as stringent as the old ones
+        for key in reject:
+            if key in old_reject and reject[key] > old_reject[key]:
+                raise ValueError(
+                    bad_msg.format(kind='reject', key=key, new=reject[key],
+                                   old=old_reject[key], op='>'))
+
+        # same for flat thresholds
+        for key in set(old_flat) - set(flat):
+            flat[key] = old_flat[key]
+        for key in flat:
+            if key in old_flat and flat[key] < old_flat[key]:
+                raise ValueError(
+                    bad_msg.format(kind='flat', key=key, new=flat[key],
+                                   old=old_flat[key], op='<'))
 
         # after validation, set parameters
         self._bad_dropped = False


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
@@ -1459,6 +1459,33 @@ def test_reject_epochs(tmpdir):
     epochs.save(temp_fname, overwrite=True)
     read_epochs(temp_fname)
 
+    # Ensure repeated rejection works, even if applied to only a subset of the
+    # previously-used channel types
+    epochs = Epochs(raw, events1, event_id, tmin, tmax,
+                    reject=reject, flat=flat)
+
+    new_reject = reject.copy()
+    new_flat = flat.copy()
+    del new_reject['grad'], new_reject['eeg'], new_reject['eog']
+    del new_flat['mag']
+
+    # No changes expected
+    epochs_cleaned = epochs.copy().drop_bad(reject=new_reject, flat=new_flat)
+    assert epochs_cleaned.reject == epochs.reject
+    assert epochs_cleaned.flat == epochs.flat
+
+    new_reject['mag'] /= 2
+    new_flat['grad'] *= 2
+    # Only the newly-provided thresholds should be updated, the existing ones
+    # should be kept
+    epochs_cleaned = epochs.copy().drop_bad(reject=new_reject, flat=new_flat)
+    assert epochs_cleaned.reject == dict(mag=new_reject['mag'],
+                                         grad=reject['grad'],
+                                         eeg=reject['eeg'],
+                                         eog=reject['eog'])
+    assert epochs_cleaned.flat == dict(grad=new_flat['grad'],
+                                       mag=flat['mag'])
+
 
 def test_preload_epochs():
     """Test preload of epochs."""