Repository: mne-tools/mne-python

Issue:
compute_psd does not apply projs correctly
I tested it for `compute_psd()`. It shows the exact same behavior as `psd_welch()`: The projection is not applied during plotting, even if the projection kwarg is set to True `raw_CAR.compute_psd(proj=True)`.

_Originally posted by @moritz-gerster in https://github.com/mne-tools/mne-python/issues/10656#issuecomment-1249716482_

Discussion:
drammock: cc @moritz-gerster 

Patch:
diff --git a/mne/time_frequency/spectrum.py b/mne/time_frequency/spectrum.py
index e4597834e..f3b23c140 100644
--- a/mne/time_frequency/spectrum.py
+++ b/mne/time_frequency/spectrum.py
@@ -279,6 +279,7 @@ class BaseSpectrum(ContainsMixin, UpdateChannelsMixin):
         # apply proj if desired
         if proj:
             inst = inst.copy().apply_proj()
+        self.inst = inst
 
         # prep times and picks
         self._time_mask = _time_mask(inst.times, tmin, tmax, sfreq=self.sfreq)
@@ -943,18 +944,20 @@ class Spectrum(BaseSpectrum):
         super().__init__(inst, method, fmin, fmax, tmin, tmax, picks, proj,
                          n_jobs=n_jobs, verbose=verbose, **method_kw)
         # get just the data we want
-        if isinstance(inst, BaseRaw):
+        if isinstance(self.inst, BaseRaw):
             start, stop = np.where(self._time_mask)[0][[0, -1]]
             rba = 'NaN' if reject_by_annotation else None
-            data = inst.get_data(self._picks, start, stop + 1,
-                                 reject_by_annotation=rba)
+            data = self.inst.get_data(self._picks, start, stop + 1,
+                                      reject_by_annotation=rba)
         else:  # Evoked
-            data = inst.data[self._picks][:, self._time_mask]
+            data = self.inst.data[self._picks][:, self._time_mask]
         # compute the spectra
         self._compute_spectra(data, fmin, fmax, n_jobs, method_kw, verbose)
         # check for correct shape and bad values
         self._check_values()
         del self._shape
+        # save memory
+        del self.inst
 
     def __getitem__(self, item):
         """Get Spectrum data.
@@ -1043,21 +1046,23 @@ class EpochsSpectrum(BaseSpectrum, GetEpochsMixin):
         super().__init__(inst, method, fmin, fmax, tmin, tmax, picks, proj,
                          n_jobs=n_jobs, verbose=verbose, **method_kw)
         # get just the data we want
-        data = inst.get_data(picks=self._picks)[:, :, self._time_mask]
+        data = self.inst.get_data(picks=self._picks)[:, :, self._time_mask]
         # compute the spectra
         self._compute_spectra(data, fmin, fmax, n_jobs, method_kw, verbose)
         self._dims = ('epoch',) + self._dims
-        self._shape = (len(inst),) + self._shape
+        self._shape = (len(self.inst),) + self._shape
         # check for correct shape and bad values
         self._check_values()
         del self._shape
         # we need these for to_data_frame()
-        self.event_id = inst.event_id.copy()
-        self.events = inst.events.copy()
-        self.selection = inst.selection.copy()
+        self.event_id = self.inst.event_id.copy()
+        self.events = self.inst.events.copy()
+        self.selection = self.inst.selection.copy()
         # we need these for __getitem__()
-        self.drop_log = deepcopy(inst.drop_log)
-        self._metadata = inst.metadata
+        self.drop_log = deepcopy(self.inst.drop_log)
+        self._metadata = self.inst.metadata
+        # save memory
+        del self.inst
 
     def __getitem__(self, item):
         """Subselect epochs from an EpochsSpectrum.
diff --git a/tutorials/simulation/70_point_spread.py b/tutorials/simulation/70_point_spread.py
index 41f20f2e9..71744d9b8 100644
--- a/tutorials/simulation/70_point_spread.py
+++ b/tutorials/simulation/70_point_spread.py
@@ -63,11 +63,11 @@ inv_op = read_inverse_operator(fname_inv)
 
 raw = mne.io.read_raw_fif(op.join(data_path, 'MEG', 'sample',
                                   'sample_audvis_raw.fif'))
+raw.info['bads'] = []
 raw.set_eeg_reference(projection=True)
 events = mne.find_events(raw)
 event_id = {'Auditory/Left': 1, 'Auditory/Right': 2}
 epochs = mne.Epochs(raw, events, event_id, baseline=(None, 0), preload=True)
-epochs.info['bads'] = []
 evoked = epochs.average()
 
 labels = mne.read_labels_from_annot('sample', subjects_dir=subjects_dir)


Test patch:
diff --git a/mne/time_frequency/tests/test_spectrum.py b/mne/time_frequency/tests/test_spectrum.py
index a132d57cc..2a1eeb5ed 100644
--- a/mne/time_frequency/tests/test_spectrum.py
+++ b/mne/time_frequency/tests/test_spectrum.py
@@ -44,15 +44,19 @@ def test_spectrum_params(method, fmin, fmax, tmin, tmax, picks, proj, n_fft,
     raw.compute_psd(**kwargs)
 
 
+def _get_inst(inst, request, evoked):
+    # ↓ XXX workaround:
+    # ↓ parametrized fixtures are not accessible via request.getfixturevalue
+    # ↓ https://github.com/pytest-dev/pytest/issues/4666#issuecomment-456593913
+    return evoked if inst == 'evoked' else request.getfixturevalue(inst)
+
+
 @requires_h5py
 @pytest.mark.parametrize('inst', ('raw', 'epochs', 'evoked'))
 def test_spectrum_io(inst, tmp_path, request, evoked):
     """Test save/load of spectrum objects."""
     fname = tmp_path / f'{inst}-spectrum.h5'
-    # ↓ XXX workaround:
-    # ↓ parametrized fixtures are not accessible via request.getfixturevalue
-    # ↓ https://github.com/pytest-dev/pytest/issues/4666#issuecomment-456593913
-    inst = evoked if inst == 'evoked' else request.getfixturevalue(inst)
+    inst = _get_inst(inst, request, evoked)
     orig = inst.compute_psd()
     orig.save(fname)
     loaded = read_spectrum(fname)
@@ -145,10 +149,7 @@ def test_spectrum_to_data_frame(inst, request, evoked):
 
     # setup
     is_epochs = inst == 'epochs'
-    # ↓ XXX workaround:
-    # ↓ parametrized fixtures are not accessible via request.getfixturevalue
-    # ↓ https://github.com/pytest-dev/pytest/issues/4666#issuecomment-456593913
-    inst = evoked if inst == 'evoked' else request.getfixturevalue(inst)
+    inst = _get_inst(inst, request, evoked)
     extra_dim = () if is_epochs else (1,)
     extra_cols = ['freq', 'condition', 'epoch'] if is_epochs else ['freq']
     # compute PSD
@@ -182,3 +183,18 @@ def test_spectrum_to_data_frame(inst, request, evoked):
     _pick_first = spectrum.pick(picks).to_data_frame()
     _pick_last = spectrum.to_data_frame(picks=picks)
     assert_frame_equal(_pick_first, _pick_last)
+
+
+# not testing with Evoked because it already has projs applied
+@pytest.mark.parametrize('inst', ('raw', 'epochs'))
+def test_spectrum_proj(inst, request):
+    """Test that proj is applied correctly (gh 11177)."""
+    inst = request.getfixturevalue(inst)
+    has_proj = inst.compute_psd(proj=True)
+    no_proj = inst.compute_psd(proj=False)
+    assert not np.array_equal(has_proj.get_data(), no_proj.get_data())
+    # make sure only the data (and the projs) were different
+    has_proj._data = no_proj._data
+    with has_proj.info._unlock():
+        has_proj.info['projs'] = no_proj.info['projs']
+    assert has_proj == no_proj