Skip to content

Commit a06d907

Browse files
committed
Raise a clear error for a fixed-length window array on a short span
A named/tuple window is regenerated by SciPy at the shrunk nperseg, so short good-data spans are handled transparently. An explicit ndarray window has a fixed length and cannot be shortened to match, which previously surfaced as a cryptic SciPy length-mismatch error. Detect this case and raise an actionable ValueError pointing the user at passing a shorter window array or reducing n_per_seg/n_fft. Adds a regression test.
1 parent 24e4288 commit a06d907

2 files changed

Lines changed: 38 additions & 0 deletions

File tree

mne/time_frequency/psd.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,12 +271,25 @@ def psd_array_welch(
271271
# so every span is zero-padded to the same length and shares one frequency
272272
# grid; short spans simply get coarser spectral resolution. A per-span
273273
# ``partial`` overrides the nperseg/noverlap baked into ``_func``.
274+
# A named/tuple window (e.g. "hamming") is regenerated by SciPy at the
275+
# shrunk nperseg, so short spans are handled transparently. An explicit
276+
# ndarray/list window has a fixed length and cannot be shortened to match;
277+
# rather than let SciPy raise a cryptic length-mismatch error, bail clearly.
278+
window_is_array = not isinstance(window, (str, tuple))
274279
funcs = []
275280
weights = []
276281
n_short = 0
277282
for span in x_splits:
278283
span_len = span.shape[-1]
279284
if span_len < n_per_seg:
285+
if window_is_array:
286+
raise ValueError(
287+
f"A good data span ({span_len} samples) is shorter than "
288+
f"n_per_seg ({n_per_seg}), but `window` is a fixed-length "
289+
"array that cannot be shortened to match it. Pass a shorter "
290+
"`window` array, or reduce n_per_seg (or n_fft) so that every "
291+
"good data span is at least n_per_seg samples long."
292+
)
280293
span_nperseg = span_len
281294
span_noverlap = min(n_overlap, span_nperseg - 1)
282295
n_short += 1

mne/time_frequency/tests/test_psd.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,31 @@ def test_psd_welch_short_span_kept():
9191
assert np.all(np.isfinite(psds))
9292

9393

94+
def test_psd_welch_short_span_array_window_raises():
95+
"""A fixed-length ndarray window can't shrink for a short span (gh-13039)."""
96+
n_fft = 256
97+
n_overlap = n_fft // 2
98+
n_chan = 2
99+
rng = np.random.default_rng(0)
100+
short = rng.standard_normal((n_chan, 100))
101+
long = rng.standard_normal((n_chan, 5 * n_fft))
102+
x = np.concatenate((short, np.full((n_chan, 1), np.nan), long), axis=-1)
103+
# Named windows are regenerated by SciPy at the shrunk length, but an explicit
104+
# window array is fixed-length and cannot be shortened to fit the 100-sample
105+
# span, so we raise a clear error instead of a cryptic SciPy length mismatch.
106+
with pytest.raises(ValueError, match="fixed-length array"):
107+
psd_array_welch(
108+
x, sfreq=100, n_fft=n_fft, n_overlap=n_overlap, window=np.hamming(n_fft)
109+
)
110+
# A full-length array window on all-long spans still works (guard is scoped).
111+
x_ok = np.concatenate((long, np.full((n_chan, 1), np.nan), long), axis=-1)
112+
psds, freqs = psd_array_welch(
113+
x_ok, sfreq=100, n_fft=n_fft, n_overlap=n_overlap, window=np.hamming(n_fft)
114+
)
115+
assert psds.shape == (n_chan, len(freqs))
116+
assert np.all(np.isfinite(psds))
117+
118+
94119
def _make_psd_data():
95120
"""Make noise data with sinusoids in 2 out of 7 channels."""
96121
rng = np.random.default_rng(0)

0 commit comments

Comments
 (0)