diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index 2be7c5824f3..c8307aece12 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -125,7 +125,7 @@ Changelog - BrainVision data format files are now parsed for EEG impedance values in :func:`mne.io.read_raw_brainvision` and provided as a ``.impedances`` attribute of ``raw`` by `Stefan Appelhoff`_ and `Jan Sedivy`_ -- Deletion of applied (active) projectors via `~mne.io.Raw.del_proj`, `~mne.Epochs.del_proj`, and `~mne.Evoked.del_proj` is now possible if the channels the to-be-removed projector applies to are not present in the data anymore by `Richard Höchenberger`_ +- When picking a subset of channels, or when dropping channels from `~mne.io.Raw`, `~mne.Epochs`, or `~mne.Evoked`, projectors that can only be applied to the removed channels will now be dropped automatically by `Richard Höchenberger`_ Bug ~~~ diff --git a/mne/beamformer/tests/test_lcmv.py b/mne/beamformer/tests/test_lcmv.py index bbd5c48c16f..d54ffb9f65e 100644 --- a/mne/beamformer/tests/test_lcmv.py +++ b/mne/beamformer/tests/test_lcmv.py @@ -293,7 +293,7 @@ def test_make_lcmv(tmpdir, reg, proj): # __repr__ assert len(evoked.ch_names) == 22 - assert len(evoked.info['projs']) == (4 if proj else 0) + assert len(evoked.info['projs']) == (3 if proj else 0) assert len(evoked.info['bads']) == 2 rank = 17 if proj else 20 assert 'LCMV' in repr(filters) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 552d3cce9c9..c5ed2481531 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -756,8 +756,8 @@ def pick_channels(self, ch_names, ordered=False): .. versionadded:: 0.9.0 """ - return self._pick_drop_channels( - pick_channels(self.info['ch_names'], ch_names, ordered=ordered)) + picks = pick_channels(self.info['ch_names'], ch_names, ordered=ordered) + return self._pick_drop_channels(picks) @fill_doc def pick(self, picks, exclude=()): @@ -888,6 +888,23 @@ def _pick_drop_channels(self, idx): self._data = self._data.take(idx, axis=axis) else: assert isinstance(self, BaseRaw) and not self.preload + + self._pick_projs() + return self + + def _pick_projs(self): + """Keep only projectors which apply to at least 1 data channel.""" + drop_idx = [] + for idx, proj in enumerate(self.info['projs']): + if not set(self.info['ch_names']) & set(proj['data']['col_names']): + drop_idx.append(idx) + + for idx in drop_idx: + logger.info(f"Removing projector {self.info['projs'][idx]}") + + if drop_idx and hasattr(self, 'del_proj'): + self.del_proj(drop_idx) + return self def add_channels(self, add_list, force_update_info=False): diff --git a/mne/io/fiff/tests/test_raw_fiff.py b/mne/io/fiff/tests/test_raw_fiff.py index 0ff941729bd..9a77f2cc91c 100644 --- a/mne/io/fiff/tests/test_raw_fiff.py +++ b/mne/io/fiff/tests/test_raw_fiff.py @@ -728,10 +728,25 @@ def test_proj(tmpdir): assert_allclose(data_proj_1, data_proj_2) assert_allclose(data_proj_2, np.dot(raw._projector, data_proj_2)) + # Test that picking removes projectors ... + raw = read_raw_fif(fif_fname).apply_proj() + n_projs = len(raw.info['projs']) + raw.pick_types(meg=False, eeg=True) + assert len(raw.info['projs']) == n_projs - 3 + + # ... but only if it doesn't apply to any channels in the dataset anymore. + raw = read_raw_fif(fif_fname).apply_proj() + n_projs = len(raw.info['projs']) + raw.pick_types(meg='mag', eeg=True) + assert len(raw.info['projs']) == n_projs + + # I/O roundtrip of an MEG projector with a Raw that only contains EEG + # data. out_fname = tmpdir.join('test_raw.fif') raw = read_raw_fif(test_fif_fname, preload=True).crop(0, 0.002) + proj = raw.info['projs'][-1] raw.pick_types(meg=False, eeg=True) - raw.info['projs'] = [raw.info['projs'][-1]] + raw.info['projs'] = [proj] # Restore, because picking removed it! raw._data.fill(0) raw._data[-1] = 1. raw.save(out_fname) @@ -739,18 +754,6 @@ def test_proj(tmpdir): raw.apply_proj() assert_allclose(raw[:, :][0][:1], raw[0, :][0]) - # Read file again, apply proj, pick all channels one proj did NOT apply to; - # then try to delete this proj, which now exclusively refers to channels - # which are not present in the data anymore. - raw = read_raw_fif(fif_fname).apply_proj() - del_proj_idx = 0 - picks = list(set(raw.ch_names) - - set(raw.info['projs'][del_proj_idx]['data']['col_names'])) - raw.pick(picks) - n_projs = len(raw.info['projs']) - raw.del_proj(del_proj_idx) - assert len(raw.info['projs']) == n_projs - 1 - @testing.requires_testing_data @pytest.mark.parametrize('preload', [False, True, 'memmap.dat']) @@ -1475,6 +1478,13 @@ def test_drop_channels_mixin(): assert len(ch_names) == len(raw._cals) assert len(ch_names) == raw._data.shape[0] + # Test that dropping all channels a projector applies to will lead to the + # removal of said projector. + raw = read_raw_fif(fif_fname).apply_proj() + n_projs = len(raw.info['projs']) + raw.drop_channels(raw.info['projs'][-1]['data']['col_names']) # EEG proj + assert len(raw.info['projs']) == n_projs - 1 + @testing.requires_testing_data @pytest.mark.parametrize('preload', (True, False)) diff --git a/mne/io/proj.py b/mne/io/proj.py index eadf918fc5e..ae549408dd9 100644 --- a/mne/io/proj.py +++ b/mne/io/proj.py @@ -222,8 +222,7 @@ def del_proj(self, idx='all'): """Remove SSP projection vector. .. note:: The projection vector can only be removed if it is inactive - (has not been applied to the data), unless the channels it - was applied to no longer exist in the data. + (has not been applied to the data). Parameters ----------