@@ -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+
119140def 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+
313433def test_bad_proj ():
314434 """Test dealing with bad projection application."""
315435 raw = read_raw_fif (raw_fname , preload = True )
0 commit comments