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:

Resolving patch:
diff --git a/mne/time_frequency/spectrum.py b/mne/time_frequency/spectrum.py
--- a/mne/time_frequency/spectrum.py
+++ b/mne/time_frequency/spectrum.py
@@ -279,6 +279,7 @@ def __init__(self, inst, method, fmin, fmax, tmin, tmax, picks,
         # 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 @@ def __init__(self, inst, method, fmin, fmax, tmin, tmax, picks,
         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 @@ def __init__(self, inst, method, fmin, fmax, tmin, tmax, picks, proj, *,
         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.


Test patch:
diff --git a/mne/time_frequency/tests/test_spectrum.py b/mne/time_frequency/tests/test_spectrum.py
--- 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