Skip to content

Commit b140596

Browse files
committed
Fix psd_array_welch for good spans shorter than n_overlap (#13039)
When good data spans (between bad annotations) are shorter than n_per_seg, SciPy reduces nperseg to the span length but leaves noverlap unchanged, so a span shorter than n_overlap raised 'noverlap must be less than nperseg'. Reduce noverlap per-span to stay < nperseg (nfft unchanged so frequency bins match across spans). Adds a regression test.
1 parent 6881480 commit b140596

3 files changed

Lines changed: 32 additions & 0 deletions

File tree

doc/changes/dev/13039.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix :func:`mne.time_frequency.psd_array_welch` (and Welch-method ``compute_psd``) so that good data spans shorter than ``n_overlap`` no longer raise ``noverlap must be less than nperseg``; such spans now reduce ``noverlap`` to fit, by :newcontrib:`Cedric Conday`.

mne/time_frequency/psd.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,20 @@ def psd_array_welch(
281281
)
282282

283283
def func(*args, **kwargs):
284+
# A good data span shorter than n_per_seg makes SciPy reduce nperseg to
285+
# the span length; reduce noverlap to match so it stays < nperseg.
286+
# Otherwise, a span shorter than n_overlap raises "noverlap must be less
287+
# than nperseg". nfft is left unchanged so every span yields the same
288+
# frequency bins (the spans are then combined by weighted average).
289+
# See #13039.
290+
epoch = args[0]
291+
if epoch.shape[-1] < n_per_seg:
292+
span_len = epoch.shape[-1]
293+
kwargs = {
294+
**kwargs,
295+
"nperseg": span_len,
296+
"noverlap": min(n_overlap, max(span_len - 1, 0)),
297+
}
284298
# swallow SciPy warnings caused by short good data spans
285299
with warnings.catch_warnings():
286300
warnings.filterwarnings(

mne/time_frequency/tests/test_psd.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,23 @@ def test_bad_annot_handling():
5858
np.testing.assert_allclose(got[0], want[0], rtol=1e-15, atol=0)
5959

6060

61+
def test_psd_welch_short_span_with_overlap():
62+
"""Good spans shorter than n_overlap must not crash (gh-13039)."""
63+
n_fft = 256
64+
n_overlap = n_fft // 2 # 128
65+
n_chan = 2
66+
rng = np.random.default_rng(0)
67+
# A good span (100 samples) shorter than n_overlap, then a bad-annotation
68+
# (aligned NaN), then a long span. Previously raised from SciPy:
69+
# "noverlap must be less than nperseg".
70+
short = rng.standard_normal((n_chan, 100))
71+
long = rng.standard_normal((n_chan, 5 * n_fft))
72+
x = np.concatenate((short, np.full((n_chan, 1), np.nan), long), axis=-1)
73+
psds, freqs = psd_array_welch(x, sfreq=100, n_fft=n_fft, n_overlap=n_overlap)
74+
assert psds.shape == (n_chan, len(freqs))
75+
assert np.all(np.isfinite(psds))
76+
77+
6178
def _make_psd_data():
6279
"""Make noise data with sinusoids in 2 out of 7 channels."""
6380
rng = np.random.default_rng(0)

0 commit comments

Comments
 (0)