Skip to content
2 changes: 1 addition & 1 deletion doc/changes/latest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~
Expand Down
2 changes: 1 addition & 1 deletion mne/beamformer/tests/test_lcmv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 19 additions & 2 deletions mne/channels/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=()):
Expand Down Expand Up @@ -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):
Expand Down
36 changes: 23 additions & 13 deletions mne/io/fiff/tests/test_raw_fiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,29 +728,32 @@ 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)
raw = read_raw_fif(out_fname, preload=False)
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'])
Expand Down Expand Up @@ -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))
Expand Down
3 changes: 1 addition & 2 deletions mne/io/proj.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------
Expand Down