33# Copyright the MNE-Python contributors.
44
55import re
6+ import warnings
67from copy import deepcopy
78from itertools import count
89
@@ -274,11 +275,14 @@ def add_proj(self, projs, remove_existing=False, verbose=None):
274275 return self
275276
276277 @verbose
277- def apply_proj (self , verbose = None ):
278+ def apply_proj (self , * , projs = None , verbose = None ):
278279 """Apply the signal space projection (SSP) operators to the data.
279280
280281 Parameters
281282 ----------
283+ projs : Projection | list of Projection | None
284+ The projectors to apply. All projectors must already be present in
285+ ``self.info["projs"]``. If ``None``, all projectors are applied.
282286 %(verbose)s
283287
284288 Returns
@@ -288,9 +292,9 @@ def apply_proj(self, verbose=None):
288292
289293 Notes
290294 -----
291- Once the projectors have been applied, they can no longer be
292- removed. It is usually not recommended to apply the projectors at
293- too early stages, as they are applied automatically later on
295+ Once a projector has been applied, it can no longer be removed. It is
296+ usually not recommended to apply the projectors at too early stages,
297+ as they are applied automatically later on
294298 (e.g. when computing inverse solutions).
295299 Hint: using the copy method individual projection vectors
296300 can be tested without affecting the original data.
@@ -310,43 +314,85 @@ def apply_proj(self, verbose=None):
310314 from ..evoked import Evoked
311315 from ..io import BaseRaw
312316
313- if self .info ["projs" ] is None or len (self .info ["projs" ]) == 0 :
314- logger .info (
315- "No projector specified for this dataset. "
316- "Please consider the method self.add_proj."
317- )
318- return self
317+ restore = None
318+ if projs is not None :
319+ if isinstance (projs , Projection ):
320+ projs = [projs ]
321+ projs = _check_projs (projs , copy = False )
322+ if not projs :
323+ return self
324+ info = self .info .copy ()
325+ selected_idx = _proj_indices (info ["projs" ], projs )
326+ to_apply = []
327+ for ii in selected_idx :
328+ proj = info ["projs" ][ii ]
329+ if proj ["active" ]:
330+ continue
331+ # avoid emitting the warning twice
332+ with warnings .catch_warnings ():
333+ warnings .simplefilter ("ignore" , RuntimeWarning )
334+ _ , nproj , _ = make_projector ([proj ], info ["ch_names" ], info ["bads" ])
335+ if nproj :
336+ to_apply .append (ii )
337+ if not to_apply :
338+ return self
339+ with info ._unlock ():
340+ attached = info ["projs" ]
341+ keep = [
342+ proj ["active" ] or ii in to_apply for ii , proj in enumerate (attached )
343+ ]
344+ omitted = iter (proj for proj , use in zip (attached , keep ) if not use )
345+ info ["projs" ] = [proj for proj , use in zip (attached , keep ) if use ]
346+ restore = (keep , omitted )
347+ self .info = info
348+
349+ try :
350+ if self .info ["projs" ] is None or len (self .info ["projs" ]) == 0 :
351+ logger .info (
352+ "No projector specified for this dataset. "
353+ "Please consider the method self.add_proj."
354+ )
355+ return self
319356
320- # Exit delayed mode if you apply proj
321- if isinstance (self , BaseEpochs ) and self ._do_delayed_proj :
322- logger .info ("Leaving delayed SSP mode." )
323- self ._do_delayed_proj = False
357+ # Exit delayed mode if you apply proj
358+ if isinstance (self , BaseEpochs ) and self ._do_delayed_proj :
359+ logger .info ("Leaving delayed SSP mode." )
360+ self ._do_delayed_proj = False
324361
325- if all (p ["active" ] for p in self .info ["projs" ]):
326- logger .info (
327- "Projections have already been applied. Setting proj attribute to True."
328- )
329- return self
362+ if all (p ["active" ] for p in self .info ["projs" ]):
363+ logger .info (
364+ "Projections have already been applied. "
365+ "Setting proj attribute to True."
366+ )
367+ return self
330368
331- _projector , info = setup_proj (
332- deepcopy (self .info ), add_eeg_ref = False , activate = True
333- )
334- # let's not raise a RuntimeError here, otherwise interactive plotting
335- if _projector is None : # won't be fun.
336- logger .info ("The projections don't apply to these data. Doing nothing." )
369+ _projector , info = setup_proj (
370+ self .info .copy (), add_eeg_ref = False , activate = True
371+ )
372+ # let's not raise a RuntimeError here, otherwise interactive plotting
373+ if _projector is None : # won't be fun.
374+ logger .info ("The projections don't apply to these data. Doing nothing." )
375+ return self
376+ self ._projector , self .info = _projector , info
377+ if isinstance (self , BaseRaw | Evoked ):
378+ if self .preload :
379+ self ._data = np .dot (self ._projector , self ._data )
380+ else : # BaseEpochs
381+ if self .preload :
382+ for ii , e in enumerate (self ._data ):
383+ self ._data [ii ] = self ._project_epoch (e )
384+ else :
385+ self .load_data () # will automatically apply
386+ logger .info ("SSP projectors applied..." )
337387 return self
338- self ._projector , self .info = _projector , info
339- if isinstance (self , BaseRaw | Evoked ):
340- if self .preload :
341- self ._data = np .dot (self ._projector , self ._data )
342- else : # BaseEpochs
343- if self .preload :
344- for ii , e in enumerate (self ._data ):
345- self ._data [ii ] = self ._project_epoch (e )
346- else :
347- self .load_data () # will automatically apply
348- logger .info ("SSP projectors applied..." )
349- return self
388+ finally :
389+ if restore is not None :
390+ keep , omitted = restore
391+ visible = iter (self .info ["projs" ])
392+ with self .info ._unlock ():
393+ self .info ["projs" ] = [
394+ next (visible ) if use else next (omitted ) for use in keep
395+ ]
350396
351397 def del_proj (self , idx = "all" ):
352398 """Remove SSP projection vector.
@@ -515,19 +561,63 @@ def plot_projs_topomap(
515561 )
516562 return fig
517563
518- def _reconstruct_proj (self , mode = "accurate" , origin = "auto" ):
564+ def reconstruct_proj (self , * , projs = None , mode = "accurate" , origin = "auto" ):
565+ """Apply SSP projectors and reconstruct the resulting signal in sensor space.
566+
567+ Operates in place.
568+
569+ Parameters
570+ ----------
571+ projs : Projection | list of Projection | None
572+ The projector or projectors to apply before reconstruction. All
573+ projectors must already be present in ``self.info["projs"]``. If
574+ ``None``, all projectors attached to the instance are used.
575+ mode : str
576+ Either ``'accurate'`` or ``'fast'``, determines the quality of the
577+ Legendre polynomial expansion used for reconstruction.
578+ origin : array-like, shape (3,) | str
579+ Origin of the sphere in the head coordinate frame and in meters.
580+ Can be ``'auto'`` (default), which means a head-digitization-based
581+ origin fit.
582+
583+ Returns
584+ -------
585+ self : same type as the input data
586+ The modified instance.
587+ """
519588 from ..forward import _map_meg_or_eeg_channels
520589
521- if len (self .info ["projs" ]) == 0 :
522- return self
523- self .apply_proj ()
590+ if projs is None :
591+ if len (self .info ["projs" ]) == 0 :
592+ return self
593+ self .apply_proj ()
594+ mapping_info = self .info
595+ selected_projs = None
596+ else :
597+ self .apply_proj (projs = projs )
598+ selected_projs = [projs ] if isinstance (projs , Projection ) else projs
599+ if len (selected_projs ) == 0 :
600+ return self
601+ mapping_info = self .info .copy ()
602+ with mapping_info ._unlock ():
603+ mapping_info ["projs" ] = [
604+ proj for proj in mapping_info ["projs" ] if proj ["active" ]
605+ ]
524606 for kind in ("meg" , "eeg" ):
525607 kwargs = dict (meg = False )
526608 kwargs [kind ] = True
527609 picks = pick_types (self .info , ** kwargs )
528610 if len (picks ) == 0 :
529611 continue
530- info_from = pick_info (self .info , picks )
612+ info_from = pick_info (mapping_info , picks )
613+ if selected_projs is not None :
614+ with warnings .catch_warnings ():
615+ warnings .simplefilter ("ignore" , RuntimeWarning )
616+ _ , nproj , _ = make_projector (
617+ selected_projs , info_from ["ch_names" ], info_from ["bads" ]
618+ )
619+ if nproj == 0 :
620+ continue
531621 info_to = info_from .copy ()
532622 with info_to ._unlock ():
533623 info_to ["projs" ] = []
@@ -1164,6 +1254,27 @@ def setup_proj(
11641254 return projector , info
11651255
11661256
1257+ def _proj_indices (attached , projs ):
1258+ """Find the indices of projectors in an attached projector list."""
1259+ selected = []
1260+ for pi , proj in enumerate (projs ):
1261+ matches = [
1262+ ii
1263+ for ii , attached_proj in enumerate (attached )
1264+ if _proj_equal (proj , attached_proj , check_active = False )
1265+ ]
1266+ if len (matches ) == 0 :
1267+ raise ValueError (
1268+ f"projs[{ pi } ] does not match any projector in self.info['projs']"
1269+ )
1270+ if len (matches ) > 1 :
1271+ raise ValueError (
1272+ f"projs[{ pi } ] matches multiple projectors in self.info['projs']"
1273+ )
1274+ selected .append (matches [0 ])
1275+ return list (dict .fromkeys (selected ))
1276+
1277+
11671278def _uniquify_projs (projs , check_active = True , sort = True ):
11681279 """Make unique projs."""
11691280 final_projs = []
0 commit comments