Repository: mne-tools/mne-python

Issue:
BUG: ica.reject_ has no persistence
The `ica.reject_` param was added in https://github.com/mne-tools/mne-python/pull/5819 but it has no persistence. It also only shows up when fitting raw, despite the fact that fitting epochs you can have access to `epochs.reject` (which you should probably use for `ica.reject_` in that case). So I propose:

1. Actually save `ica.reject_`
2. Set `ica.reject_ = epochs.reject_` when fitting epochs

@agramfort does this make sense to you?


Discussion:
When you reject with ICA you don’t work to remove blinks for example. You want to remove really massive transient artifacts so it should not be the same reject as epochs. It’s not for the same purpose

I think for Epochs we should set it to None -- like you said, reject is only applied for Raw, while for epochs we have an entirely orthogonal reject mechanism in the epochs class 
Makes sense, PR coming

Resolving patch:
diff --git a/mne/preprocessing/ica.py b/mne/preprocessing/ica.py
--- a/mne/preprocessing/ica.py
+++ b/mne/preprocessing/ica.py
@@ -699,6 +699,8 @@ def _fit_raw(self, raw, picks, start, stop, decim, reject, flat, tstep,
             data, self.drop_inds_ = _reject_data_segments(data, reject, flat,
                                                           decim, self.info,
                                                           tstep)
+        else:
+            self.reject_ = None
 
         self.n_samples_ = data.shape[1]
         self._fit(data, 'raw')
@@ -724,6 +726,7 @@ def _fit_epochs(self, epochs, picks, decim, verbose):
         # more from _pre_whiten)
         data = np.hstack(data)
         self._fit(data, 'epochs')
+        self.reject_ = deepcopy(epochs.reject)
 
         return self
 
@@ -2558,6 +2561,11 @@ def _write_ica(fid, ica):
     #   Write bad components
     write_int(fid, FIFF.FIFF_MNE_ICA_BADS, list(ica.exclude))
 
+    #   Write reject_
+    if ica.reject_ is not None:
+        write_string(fid, FIFF.FIFF_MNE_EPOCHS_REJECT_FLAT,
+                     json.dumps(dict(reject=ica.reject_)))
+
     # Done!
     end_block(fid, FIFF.FIFFB_MNE_ICA)
 
@@ -2601,6 +2609,7 @@ def read_ica(fname, verbose=None):
             raise ValueError('Could not find ICA data')
 
     my_ica_data = ica_data[0]
+    ica_reject = None
     for d in my_ica_data['directory']:
         kind = d.kind
         pos = d.pos
@@ -2631,6 +2640,9 @@ def read_ica(fname, verbose=None):
         elif kind == FIFF.FIFF_MNE_ICA_MISC_PARAMS:
             tag = read_tag(fid, pos)
             ica_misc = tag.data
+        elif kind == FIFF.FIFF_MNE_EPOCHS_REJECT_FLAT:
+            tag = read_tag(fid, pos)
+            ica_reject = json.loads(tag.data)['reject']
 
     fid.close()
 
@@ -2682,6 +2694,7 @@ def f(x):
         ica.n_iter_ = ica_misc['n_iter_']
     if 'fit_params' in ica_misc:
         ica.fit_params = ica_misc['fit_params']
+    ica.reject_ = ica_reject
 
     logger.info('Ready.')
 
@@ -3006,4 +3019,5 @@ def read_ica_eeglab(fname, *, verbose=None):
     ica.info = info
     ica._update_mixing_matrix()
     ica._update_ica_names()
+    ica.reject_ = None
     return ica
diff --git a/mne/viz/ica.py b/mne/viz/ica.py
--- a/mne/viz/ica.py
+++ b/mne/viz/ica.py
@@ -455,6 +455,7 @@ def _fast_plot_ica_properties(ica, inst, picks=None, axes=None, dB=True,
     else:
         kind, dropped_indices, epochs_src, data = _prepare_data_ica_properties(
             inst, ica, reject_by_annotation, reject)
+    del reject
     ica_data = np.swapaxes(data[:, picks, :], 0, 1)
     dropped_src = ica_data
 
@@ -555,7 +556,7 @@ def _prepare_data_ica_properties(inst, ica, reject_by_annotation=True,
         # when auto, delegate reject to the ica
         from ..epochs import make_fixed_length_epochs
         if reject == 'auto':
-            reject = getattr(ica, 'reject_', None)
+            reject = ica.reject_
         if reject is None:
             drop_inds = None
             dropped_indices = []
@@ -569,7 +570,7 @@ def _prepare_data_ica_properties(inst, ica, reject_by_annotation=True,
                 verbose=False)
         else:
             data = inst.get_data()
-            data, drop_inds = _reject_data_segments(data, ica.reject_,
+            data, drop_inds = _reject_data_segments(data, reject,
                                                     flat=None, decim=None,
                                                     info=inst.info,
                                                     tstep=2.0)


Test patch:
diff --git a/mne/preprocessing/tests/test_ica.py b/mne/preprocessing/tests/test_ica.py
--- a/mne/preprocessing/tests/test_ica.py
+++ b/mne/preprocessing/tests/test_ica.py
@@ -244,10 +244,11 @@ def test_ica_noop(n_components, n_pca_components, tmp_path):
     # and with I/O
     fname = tmp_path / 'temp-ica.fif'
     ica.save(fname)
-    ica = read_ica(fname)
-    raw_new = ica.apply(raw.copy())
+    ica_new = read_ica(fname)
+    raw_new = ica_new.apply(raw.copy())
     assert_allclose(raw.get_data(), raw_new.get_data(), err_msg='I/O failure')
-    _assert_ica_attributes(ica)
+    _assert_ica_attributes(ica_new)
+    assert ica.reject_ == ica_new.reject_
 
 
 @requires_sklearn
@@ -1176,7 +1177,7 @@ def test_fit_methods(method, tmp_path):
         ('flat', dict(eeg=1e-6))
     )
 )
-def test_fit_params_epochs_vs_raw(param_name, param_val):
+def test_fit_params_epochs_vs_raw(param_name, param_val, tmp_path):
     """Check that we get a warning when passing parameters that get ignored."""
     method = 'infomax'
     n_components = 3
@@ -1184,12 +1185,20 @@ def test_fit_params_epochs_vs_raw(param_name, param_val):
 
     raw = read_raw_fif(raw_fname).pick_types(meg=False, eeg=True)
     events = read_events(event_name)
-    epochs = Epochs(raw, events=events)
+    reject = param_val if param_name == 'reject' else None
+    epochs = Epochs(raw, events=events, reject=reject)
     ica = ICA(n_components=n_components, max_iter=max_iter, method=method)
 
     fit_params = {param_name: param_val}
     with pytest.warns(RuntimeWarning, match='parameters.*will be ignored'):
         ica.fit(inst=epochs, **fit_params)
+    assert ica.reject_ == reject
+    _assert_ica_attributes(ica)
+    tmp_fname = tmp_path / 'test-ica.fif'
+    ica.save(tmp_fname)
+    ica = read_ica(tmp_fname)
+    assert ica.reject_ == reject
+    _assert_ica_attributes(ica)
 
 
 @requires_sklearn
@@ -1535,6 +1544,7 @@ def test_read_ica_eeglab_mismatch(tmp_path):
     assert 'unmixing_matrix_' in attrs
     assert ica.labels_ == ica_correct.labels_ == {}
     attrs.pop(attrs.index('labels_'))
+    attrs.pop(attrs.index('reject_'))
     for attr in attrs:
         a, b = getattr(ica, attr), getattr(ica_correct, attr)
         assert_allclose(a, b, rtol=1e-12, atol=1e-12, err_msg=attr)
@@ -1584,6 +1594,7 @@ def _assert_ica_attributes(ica, data=None, limits=(1.0, 70)):
         # at least close to normal
         assert norms.min() > limits[0], 'Not roughly unity'
         assert norms.max() < limits[1], 'Not roughly unity'
+    assert hasattr(ica, 'reject_')
 
 
 @pytest.mark.parametrize("ch_type", ["dbs", "seeg"])