@@ -261,6 +261,113 @@ def test_compute_proj_epochs(tmp_path):
261261 write_proj (fname , ["foo" ], overwrite = True )
262262
263263
264+ @pytest .mark .parametrize (
265+ ("ch_type" , "picks" , "proj_kwargs" ),
266+ [
267+ ("meg" , [0 , 1 , 2 , 3 , 4 , 6 , 7 ], dict (n_grad = 2 , n_mag = 2 , n_eeg = 0 )),
268+ ("eeg" , np .arange (340 , 360 ), dict (n_grad = 0 , n_mag = 0 , n_eeg = 2 )),
269+ ],
270+ )
271+ def test_reconstruct_proj_selection (ch_type , picks , proj_kwargs ):
272+ """Test selecting projectors for reconstruction."""
273+ raw = read_raw_fif (raw_fname )
274+ raw .add_proj ([], remove_existing = True )
275+ events = read_events (event_fname )
276+ evoked = Epochs (
277+ raw ,
278+ events [:5 ],
279+ 1 ,
280+ - 0.1 ,
281+ 0.1 ,
282+ picks = picks ,
283+ decim = 10 ,
284+ preload = True ,
285+ verbose = "error" ,
286+ ).average ()
287+ if ch_type == "eeg" :
288+ evoked .set_eeg_reference (projection = True )
289+ evoked_car_active = evoked .copy ().apply_proj ()
290+ evoked_proj = evoked .copy ().apply_proj ().crop (None , 0 )
291+ projs = compute_proj_evoked (evoked_proj , ** proj_kwargs )
292+ assert len (projs ) >= 2
293+ evoked .add_proj (projs )
294+ original_projs = cp .deepcopy (evoked .info ["projs" ])
295+
296+ # The default is unchanged, including activating all attached projectors.
297+ default = evoked .copy ()._reconstruct_proj ()
298+ default_none = evoked .copy ()._reconstruct_proj (projs = None )
299+ assert_allclose (default_none .data , default .data )
300+ assert all (proj ["active" ] for proj in default .info ["projs" ])
301+
302+ # A scalar projector and a list of projectors use exactly the requested
303+ # artifact directions, ignoring unrelated projectors attached to Info.
304+ selected = list (projs [:2 ])
305+ reconstructed = []
306+ for proj in selected :
307+ actual = evoked .copy ()._reconstruct_proj (projs = proj )
308+ expected = evoked .copy ().del_proj ().add_proj (proj )._reconstruct_proj ()
309+ assert_allclose (actual .data , expected .data )
310+ assert [p ["active" ] for p in actual .info ["projs" ]] == [
311+ p ["desc" ] == proj ["desc" ] for p in original_projs
312+ ]
313+ reconstructed .append (actual .data )
314+ assert np .linalg .norm (reconstructed [0 ] - reconstructed [1 ]) > 1e-3 * np .linalg .norm (
315+ reconstructed [0 ]
316+ )
317+
318+ actual = evoked .copy ()._reconstruct_proj (projs = selected )
319+ expected = evoked .copy ().del_proj ().add_proj (selected )._reconstruct_proj ()
320+ assert_allclose (actual .data , expected .data )
321+ selected_descs = {proj ["desc" ] for proj in selected }
322+ assert [p ["active" ] for p in actual .info ["projs" ]] == [
323+ p ["desc" ] in selected_descs for p in original_projs
324+ ]
325+ assert np .linalg .norm (actual .data - reconstructed [0 ]) > 1e-3 * np .linalg .norm (
326+ reconstructed [0 ]
327+ )
328+
329+ if ch_type == "eeg" :
330+ # An inactive average-reference projector is not part of the explicit
331+ # selection. An already-active one remains part of the mapping state.
332+ expected = evoked_car_active .copy ().add_proj (projs [0 ])._reconstruct_proj ()
333+ evoked_car_active .add_proj (projs )
334+ actual = evoked_car_active ._reconstruct_proj (projs = projs [0 ])
335+ assert_allclose (actual .data , expected .data )
336+ assert_allclose (actual .data .mean (axis = 0 ), 0.0 , atol = 1e-20 )
337+ assert [p ["active" ] for p in actual .info ["projs" ]] == [True , True , False ]
338+
339+
340+ def test_reconstruct_proj_selection_mixed ():
341+ """Test that explicit reconstruction leaves unaffected channel types alone."""
342+ raw = read_raw_fif (raw_fname )
343+ raw .add_proj ([], remove_existing = True )
344+ events = read_events (event_fname )
345+ picks = [0 , 1 , 2 , 3 , 4 , 6 , 7 , * range (340 , 360 )]
346+ evoked = Epochs (
347+ raw ,
348+ events [:5 ],
349+ 1 ,
350+ - 0.1 ,
351+ 0.1 ,
352+ picks = picks ,
353+ decim = 10 ,
354+ preload = True ,
355+ verbose = "error" ,
356+ ).average ()
357+ projs = compute_proj_evoked (
358+ evoked .copy ().pick ("meg" ).crop (None , 0 ), n_grad = 2 , n_mag = 2 , n_eeg = 0
359+ )
360+ evoked .add_proj (projs )
361+ eeg_picks = pick_types (evoked .info , meg = False , eeg = True )
362+ meg_picks = pick_types (evoked .info , meg = True , eeg = False )
363+ data_before = evoked .data .copy ()
364+ evoked ._reconstruct_proj (projs = projs [0 ])
365+ assert_allclose (evoked .data [eeg_picks ], data_before [eeg_picks ], rtol = 0 , atol = 0 )
366+ assert not np .array_equal (evoked .data [meg_picks ], data_before [meg_picks ])
367+ assert evoked .info ["projs" ][0 ]["active" ]
368+ assert not any (proj ["active" ] for proj in evoked .info ["projs" ][1 :])
369+
370+
264371@pytest .mark .slowtest
265372def test_compute_proj_raw (tmp_path ):
266373 """Test SSP computation on raw."""
0 commit comments