Skip to content

Commit eaa44f4

Browse files
committed
ENH: Allow selecting projectors for reconstruction [ci skip]
1 parent 6f33a43 commit eaa44f4

2 files changed

Lines changed: 146 additions & 5 deletions

File tree

mne/_fiff/proj.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -541,19 +541,40 @@ def plot_projs_topomap(
541541
)
542542
return fig
543543

544-
def _reconstruct_proj(self, mode="accurate", origin="auto"):
544+
def _reconstruct_proj(self, *, projs=None, mode="accurate", origin="auto"):
545545
from ..forward import _map_meg_or_eeg_channels
546546

547-
if len(self.info["projs"]) == 0:
548-
return self
549-
self.apply_proj()
547+
if projs is None:
548+
if len(self.info["projs"]) == 0:
549+
return self
550+
self.apply_proj()
551+
mapping_info = self.info
552+
selected_projs = None
553+
else:
554+
self.apply_proj(projs=projs)
555+
selected_projs = [projs] if isinstance(projs, Projection) else projs
556+
if len(selected_projs) == 0:
557+
return self
558+
mapping_info = deepcopy(self.info)
559+
with mapping_info._unlock():
560+
mapping_info["projs"] = [
561+
proj for proj in mapping_info["projs"] if proj["active"]
562+
]
550563
for kind in ("meg", "eeg"):
551564
kwargs = dict(meg=False)
552565
kwargs[kind] = True
553566
picks = pick_types(self.info, **kwargs)
554567
if len(picks) == 0:
555568
continue
556-
info_from = pick_info(self.info, picks)
569+
info_from = pick_info(mapping_info, picks)
570+
if selected_projs is not None:
571+
with warnings.catch_warnings():
572+
warnings.simplefilter("ignore", RuntimeWarning)
573+
_, nproj, _ = make_projector(
574+
selected_projs, info_from["ch_names"], info_from["bads"]
575+
)
576+
if nproj == 0:
577+
continue
557578
info_to = info_from.copy()
558579
with info_to._unlock():
559580
info_to["projs"] = []

mne/tests/test_proj.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,27 @@ def _make_selection_epochs(raw, *, preload=False, proj=False, reject=None, event
116116
)
117117

118118

119+
def _make_reconstruct_evoked():
120+
"""Make small mixed MEG/EEG data with reconstruction projectors."""
121+
raw = read_raw_fif(raw_fname, preload=True, verbose=False).crop(0, 0.02)
122+
mag = pick_types(raw.info, meg="mag", exclude="bads")[:8]
123+
eeg = pick_types(raw.info, eeg=True, exclude="bads")[:8]
124+
raw.pick([raw.ch_names[pick] for pick in np.concatenate([mag, eeg])]).del_proj()
125+
mag_names, eeg_names = raw.ch_names[:8], raw.ch_names[8:]
126+
vector_a = np.zeros(8)
127+
vector_a[:2] = 1.0
128+
vector_b = np.zeros(8)
129+
vector_b[1:3] = 1.0
130+
projs = [
131+
_make_test_proj(mag_names, vector_a, "A"),
132+
_make_test_proj(mag_names, vector_b, "B"),
133+
_make_test_proj(eeg_names, vector_a, "EEG"),
134+
]
135+
evoked = EvokedArray(raw.get_data(), raw.info, tmin=0.0)
136+
evoked.add_proj(projs, verbose=False)
137+
return evoked, projs, np.arange(8), np.arange(8, 16)
138+
139+
119140
def test_apply_proj_selection():
120141
"""Test selecting attached projectors when applying projections."""
121142
raw, projs = _make_selection_raw()
@@ -310,6 +331,105 @@ def test_apply_proj_selection_raw_lazy(tmp_path):
310331
assert _active_projs(lazy) == [True, False, False]
311332

312333

334+
def test_reconstruct_proj_selection():
335+
"""Test selecting projectors for projection reconstruction."""
336+
evoked, projs, meg, eeg = _make_reconstruct_evoked()
337+
data = evoked.data.copy()
338+
339+
default = evoked.copy()._reconstruct_proj(mode="fast")
340+
explicit_none = evoked.copy()._reconstruct_proj(projs=None, mode="fast")
341+
assert_allclose(explicit_none.data, default.data)
342+
assert _active_projs(explicit_none) == [True, True, True]
343+
with pytest.raises(TypeError):
344+
evoked.copy()._reconstruct_proj(projs[0])
345+
346+
reconstructed = []
347+
for selection, active in (
348+
(projs[0], [True, False, False]),
349+
(projs[1], [False, True, False]),
350+
(projs[:2], [True, True, False]),
351+
):
352+
got = evoked.copy()._reconstruct_proj(projs=selection, mode="fast")
353+
reference = evoked.copy().del_proj().add_proj(selection, verbose=False)
354+
reference._reconstruct_proj(mode="fast")
355+
assert_allclose(got.data[meg], reference.data[meg])
356+
assert np.array_equal(got.data[eeg], data[eeg])
357+
assert _active_projs(got) == active
358+
reconstructed.append(got.data[meg].copy())
359+
assert not np.array_equal(reconstructed[0], reconstructed[1])
360+
361+
got = evoked.copy()._reconstruct_proj(projs=projs[2], mode="fast")
362+
reference = evoked.copy().del_proj().add_proj(projs[2], verbose=False)
363+
reference._reconstruct_proj(mode="fast")
364+
assert_allclose(got.data[eeg], reference.data[eeg])
365+
assert np.array_equal(got.data[meg], data[meg])
366+
assert _active_projs(got) == [False, False, True]
367+
368+
369+
def test_reconstruct_proj_state():
370+
"""Test active projector and EEG reference reconstruction state."""
371+
evoked, projs, meg, eeg = _make_reconstruct_evoked()
372+
data = evoked.data.copy()
373+
374+
active = evoked.copy().apply_proj(projs=projs[0], verbose=False)
375+
got = active.copy()._reconstruct_proj(projs=projs[1], mode="fast")
376+
reference = active.copy().del_proj(2)._reconstruct_proj(mode="fast")
377+
assert_allclose(got.data[meg], reference.data[meg])
378+
assert np.array_equal(got.data[eeg], data[eeg])
379+
assert _active_projs(got) == [True, True, False]
380+
381+
evoked.del_proj()
382+
car = make_eeg_average_ref_proj(evoked.info)
383+
evoked.add_proj([projs[2], car], verbose=False)
384+
inactive_car = evoked.copy()._reconstruct_proj(projs=projs[2], mode="fast")
385+
reference = evoked.copy().del_proj().add_proj(projs[2], verbose=False)
386+
reference._reconstruct_proj(mode="fast")
387+
assert_allclose(inactive_car.data[eeg], reference.data[eeg])
388+
assert np.array_equal(inactive_car.data[meg], data[meg])
389+
assert not np.allclose(inactive_car.data[eeg].mean(axis=0), 0.0, atol=1e-12)
390+
assert _active_projs(inactive_car) == [True, False]
391+
392+
active_car = evoked.copy().apply_proj(projs=car, verbose=False)
393+
got = active_car.copy()._reconstruct_proj(projs=projs[2], mode="fast")
394+
reference = active_car.copy()._reconstruct_proj(mode="fast")
395+
assert_allclose(got.data[eeg], reference.data[eeg])
396+
assert_allclose(got.data[meg], active_car.data[meg], atol=1e-20)
397+
assert_allclose(got.data[eeg].mean(axis=0), 0.0, atol=1e-12)
398+
assert _active_projs(got) == [True, True]
399+
400+
401+
def test_reconstruct_proj_noop():
402+
"""Test empty and unsupported reconstruction selections."""
403+
evoked, projs, _, _ = _make_reconstruct_evoked()
404+
data = evoked.data.copy()
405+
406+
evoked._reconstruct_proj(projs=[])
407+
assert np.array_equal(evoked.data, data)
408+
assert not any(_active_projs(evoked))
409+
410+
unsupported = _make_test_proj(["missing"], [1.0], "unsupported")
411+
evoked.add_proj(unsupported, verbose=False)
412+
evoked._reconstruct_proj(projs=unsupported)
413+
assert np.array_equal(evoked.data, data)
414+
assert not any(_active_projs(evoked))
415+
416+
active = evoked.copy().apply_proj(projs=projs[0], verbose=False)
417+
data = active.data.copy()
418+
active._reconstruct_proj(projs=unsupported)
419+
assert np.array_equal(active.data, data)
420+
assert _active_projs(active) == [True, False, False, False]
421+
422+
partial, _, meg, _ = _make_reconstruct_evoked()
423+
partial_names = [partial.ch_names[pick] for pick in meg[:3]]
424+
partial_proj = _make_test_proj(
425+
[*partial_names, "missing"], np.full(4, 0.5), "partial"
426+
)
427+
partial.add_proj(partial_proj, verbose=False)
428+
with pytest.warns(RuntimeWarning, match="reduced") as records:
429+
partial._reconstruct_proj(projs=partial_proj, mode="fast")
430+
assert len(records) == 2 # apply_proj and actual mapping, but not the probe
431+
432+
313433
def test_bad_proj():
314434
"""Test dealing with bad projection application."""
315435
raw = read_raw_fif(raw_fname, preload=True)

0 commit comments

Comments
 (0)