|
10 | 10 |
|
11 | 11 | from ..fixes import _reshape_view |
12 | 12 | from ..parallel import parallel_func |
13 | | -from ..utils import _check_option, _ensure_int, logger, verbose, warn |
| 13 | +from ..utils import _check_option, _ensure_int, _pl, logger, verbose, warn |
14 | 14 | from ..utils.numerics import _mask_to_onsets_offsets |
15 | 15 |
|
16 | 16 |
|
@@ -258,52 +258,37 @@ def psd_array_welch( |
258 | 258 | # Aligned NaNs across channels → treat as bad annotations. |
259 | 259 | good_mask = ~nan_mask_full |
260 | 260 | t_onsets, t_offsets = _mask_to_onsets_offsets(good_mask[0]) |
261 | | - x_splits = [x[..., t_ons:t_off] for t_ons, t_off in zip(t_onsets, t_offsets)] |
262 | | - # weights reflect the number of samples used from each span. For spans longer |
263 | | - # than `n_per_seg`, trailing samples may be discarded. For spans shorter than |
264 | | - # `n_per_seg`, the wrapped function (`scipy.signal.spectrogram`) automatically |
265 | | - # reduces `n_per_seg` to match the span length (with a warning). |
| 261 | + all_splits = [x[..., t_ons:t_off] for t_ons, t_off in zip(t_onsets, t_offsets)] |
| 262 | + # Drop good data spans shorter than n_per_seg: a single Welch window does not |
| 263 | + # fit them. (Shrinking the window per-span would mix incompatible estimates, |
| 264 | + # and passing them to SciPy as-is raises "noverlap must be less than |
| 265 | + # nperseg".) Warn so the user can lower n_per_seg to keep them. See #13039. |
| 266 | + x_splits = [span for span in all_splits if span.shape[-1] >= n_per_seg] |
| 267 | + n_dropped = len(all_splits) - len(x_splits) |
| 268 | + if n_dropped: |
| 269 | + warn( |
| 270 | + f"{n_dropped} good data span{_pl(n_dropped)} shorter than n_per_seg " |
| 271 | + f"({n_per_seg}) {'was' if n_dropped == 1 else 'were'} excluded from " |
| 272 | + "the PSD estimate; reduce n_per_seg (or n_fft) to include them." |
| 273 | + ) |
| 274 | + if not x_splits: |
| 275 | + raise ValueError( |
| 276 | + f"All good data spans are shorter than n_per_seg ({n_per_seg}); no " |
| 277 | + "data is left to compute the PSD. Reduce n_per_seg (or n_fft)." |
| 278 | + ) |
| 279 | + # weights reflect the number of samples used from each (kept) span; trailing |
| 280 | + # samples beyond the last full window are discarded. |
266 | 281 | step = n_per_seg - n_overlap |
267 | | - span_lengths = [span.shape[-1] for span in x_splits] |
268 | 282 | weights = [ |
269 | | - w if w < n_per_seg else w - ((w - n_overlap) % step) for w in span_lengths |
| 283 | + w - ((w - n_overlap) % step) for w in (s.shape[-1] for s in x_splits) |
270 | 284 | ] |
271 | 285 | agg_func = partial(np.average, weights=weights) |
272 | 286 | if n_jobs > 1: |
273 | 287 | logger.info( |
274 | 288 | f"Data split into {len(x_splits)} (probably unequal) chunks due to " |
275 | 289 | '"bad_*" annotations. Parallelization may be sub-optimal.' |
276 | 290 | ) |
277 | | - if (np.array(span_lengths) < n_per_seg).any(): |
278 | | - logger.info( |
279 | | - "At least one good data span is shorter than n_per_seg, and will be " |
280 | | - "analyzed with a shorter window than the rest of the file." |
281 | | - ) |
282 | | - |
283 | | - 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 | | - } |
298 | | - # swallow SciPy warnings caused by short good data spans |
299 | | - with warnings.catch_warnings(): |
300 | | - warnings.filterwarnings( |
301 | | - action="ignore", |
302 | | - module="scipy", |
303 | | - category=UserWarning, |
304 | | - message=r"nperseg = \d+ is greater than input length", |
305 | | - ) |
306 | | - return _func(*args, **kwargs) |
| 291 | + func = _func |
307 | 292 |
|
308 | 293 | else: |
309 | 294 | # Either no NaNs, or NaNs are not aligned across channels. |
|
0 commit comments