Skip to content

Commit 7d1444a

Browse files
committed
Speed up spectrum_fit
1 parent b1fb893 commit 7d1444a

3 files changed

Lines changed: 37 additions & 18 deletions

File tree

mne/filter.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1619,6 +1619,8 @@ def _mt_spectrum_proc(
16191619
if filter_length is None:
16201620
filter_length = x.shape[-1]
16211621
filter_length = min(_to_samples(filter_length, sfreq, "", ""), x.shape[-1])
1622+
pick_mask = np.zeros(len(x), dtype=bool)
1623+
pick_mask[picks] = True
16221624
get_wt = partial(
16231625
_get_window_thresh, sfreq=sfreq, mt_bandwidth=mt_bandwidth, p_value=p_value
16241626
)
@@ -1627,7 +1629,7 @@ def _mt_spectrum_proc(
16271629
if n_jobs == 1:
16281630
freq_list = list()
16291631
for ii, x_ in enumerate(x):
1630-
if ii in picks:
1632+
if pick_mask[ii]:
16311633
x[ii], f = _mt_spectrum_remove_win(
16321634
x_, sfreq, line_freqs, notch_widths, window_fun, threshold, get_wt
16331635
)
@@ -1636,7 +1638,7 @@ def _mt_spectrum_proc(
16361638
data_new = parallel(
16371639
p_fun(x_, sfreq, line_freqs, notch_widths, window_fun, threshold, get_wt)
16381640
for xi, x_ in enumerate(x)
1639-
if xi in picks
1641+
if pick_mask[xi]
16401642
)
16411643
freq_list = [d[1] for d in data_new]
16421644
data_new = np.array([d[0] for d in data_new])
@@ -1748,17 +1750,16 @@ def _mt_spectrum_remove(
17481750
indices = np.unique(np.r_[indices_1, indices_2])
17491751
rm_freqs = freqs[indices]
17501752

1751-
fits = list()
1752-
for ind in indices:
1753-
c = 2 * A[0, ind]
1754-
fit = np.abs(c) * np.cos(freqs[ind] * rads + np.angle(c))
1755-
fits.append(fit)
1756-
1757-
if len(fits) == 0:
1753+
if len(indices) == 0:
17581754
datafit = 0.0
17591755
else:
1756+
c = 2 * A[0, indices]
17601757
# fitted sinusoids are summed, and subtracted from data
1761-
datafit = np.sum(fits, axis=0)
1758+
datafit = np.sum(
1759+
np.abs(c)[:, np.newaxis]
1760+
* np.cos(freqs[indices, np.newaxis] * rads + np.angle(c)[:, np.newaxis]),
1761+
axis=0,
1762+
)
17621763

17631764
return x - datafit, rm_freqs
17641765

mne/time_frequency/multitaper.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -274,12 +274,10 @@ def _mt_spectra(x, dpss, sfreq, n_fft=None, remove_dc=True):
274274
# only keep positive frequencies
275275
freqs = rfftfreq(n_fft, 1.0 / sfreq)
276276

277-
# The following is equivalent to this, but uses less memory:
278-
# x_mt = fftpack.fft(x[:, np.newaxis, :] * dpss, n=n_fft)
279-
n_tapers = dpss.shape[0] if dpss.ndim > 1 else 1
280-
x_mt = np.zeros(x.shape[:-1] + (n_tapers, len(freqs)), dtype=np.complex128)
281-
for idx, sig in enumerate(x):
282-
x_mt[idx] = rfft(sig[..., np.newaxis, :] * dpss, n=n_fft)
277+
# Compute the tapered FFTs in one batched call across all leading dims.
278+
# This is faster than looping over signals, at the cost of a larger
279+
# temporary than the old per-signal FFT path.
280+
x_mt = rfft(x[..., np.newaxis, :] * dpss, n=n_fft, axis=-1)
283281
# Adjust DC and maybe Nyquist, depending on one-sided transform
284282
x_mt[..., 0] /= np.sqrt(2.0)
285283
if n_fft % 2 == 0:

mne/time_frequency/tests/test_multitaper.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44

55
import numpy as np
66
import pytest
7-
from numpy.testing import assert_array_almost_equal
7+
from numpy.testing import assert_array_almost_equal, assert_allclose
8+
from scipy.fft import rfft
89

910
from mne.time_frequency import psd_array_multitaper
10-
from mne.time_frequency.multitaper import dpss_windows
11+
from mne.time_frequency.multitaper import _mt_spectra, dpss_windows
1112
from mne.utils import _record_warnings
1213

1314

@@ -78,3 +79,22 @@ def test_adaptive_weights_convergence():
7879
):
7980
psd_array_multitaper(data, sfreq, adaptive=True, max_iter=2)
8081
psd_array_multitaper(data, sfreq, adaptive=True, max_iter=200)
82+
83+
84+
def test_mt_spectra_batched_matches_reference():
85+
"""Test that batched tapered spectra match an explicit channel loop."""
86+
rng = np.random.default_rng(0)
87+
x = rng.standard_normal((3, 17))
88+
sfreq = 200.0
89+
dpss, _ = dpss_windows(17, 2.5, 4, low_bias=False)
90+
91+
x_mt, freqs = _mt_spectra(x, dpss, sfreq)
92+
x = x - np.mean(x, axis=-1, keepdims=True)
93+
ref = np.array([rfft(sig[np.newaxis, :] * dpss, axis=-1) for sig in x])
94+
ref[..., 0] /= np.sqrt(2.0)
95+
if 17 % 2 == 0:
96+
ref[..., -1] /= np.sqrt(2.0)
97+
98+
assert x_mt.shape == ref.shape
99+
assert_allclose(x_mt, ref)
100+
assert freqs.shape[0] == x_mt.shape[-1]

0 commit comments

Comments
 (0)