Morlet wavelets¶
The Morlet (Gabor) wavelet transform is the central, load-bearing feature of PTSA. Most lab analyses use PTSA primarily to obtain time-resolved power and phase estimates of an EEG signal in a set of frequency bands. This page is the canonical reference for the formula PTSA implements, the meaning of the user-facing parameters, and the differences between PTSA’s parameterization and the ones used by SciPy and MNE.
Formula¶
PTSA’s Morlet wavelet is the standard complex Morlet (Gabor) wavelet parameterized by the number of cycles under the Gaussian envelope. For a target frequency \(f\) and a width \(w\) (number of cycles):
The prefactor \((\sigma_t \sqrt{\pi})^{-1/2}\) gives the wavelet unit \(L^2\) energy. The Gaussian envelope \(e^{-t^2/(2 \sigma_t^2)}\) is evaluated out to \(\pm 3.5 \sigma_t\); values beyond that window are truncated to zero.
Time-frequency decomposition is carried out by convolving the input signal with \(\psi\). PTSA performs the convolution in the frequency domain using FFTW:
where \(\widehat{\,\cdot\,}\) denotes the discrete Fourier transform. The wavelet FFT is precomputed once per frequency and reused across signals and trials, which is the source of PTSA’s speed for long recording sessions.
The complete correction (default)¶
When complete=True (the default since PTSA 2.0.6), the wavelet
is zero-mean corrected so that the integral
\(\int \psi(t) \, dt\) vanishes exactly. The corrected wavelet is:
The \(e^{-w^2/2}\) subtraction would slightly shift the peak frequency away from \(f\) if applied naively, so PTSA also rescales the time axis by
so that the corrected wavelet’s peak in the frequency domain stays at \(f\).
When to flip complete to False:
The correction matters most at small widths (e.g.
width < 6), where the uncorrected wavelet has a non-trivial DC component that biases low-frequency power estimates.For larger widths the correction is numerically negligible (\(e^{-w^2/2}\) shrinks very fast:
w=5already gives \(\sim 4 \times 10^{-6}\)).Setting
complete=Falseis mainly useful for reproducing analyses that explicitly used the uncorrected wavelet, or for cross-checking against another tool that does not apply a zero-mean correction.
Parameters¶
freqsarray of floatFrequencies (Hz) at which to evaluate the transform. The output has a
frequencydimension of the same length. There is no requirement that the frequencies be log-spaced; PTSA simply builds one cached wavelet FFT per entry.widthint, default5Number of cycles of the carrier sinusoid under one standard deviation of the Gaussian envelope (the same convention as MNE’s
n_cycles). This is not a SciPy-style scale factor. The trade-off is the usual time-frequency uncertainty:Higher
width→ narrower bandwidth (tighter frequency resolution), wider time extent (looser time resolution).Lower
width→ broader bandwidth, sharper temporal localization.
Common values in the EEG literature are
width=5(the PTSA default) throughwidth=7;width=4is at the low end of what is typically reported, and thecomplete=Truecorrection becomes important there.outputstr or sequence of str, default('power', 'phase')Any combination of
'power'(squared magnitude of the complex coefficient),'phase'(angle of the coefficient, in radians, in \((-\pi, \pi]\)), and'complex'(the complex coefficient itself).'complex'may not be combined with the other two on the same call.completebool, defaultTrueApply the zero-mean correction described above.
cpusint, default1Number of worker threads used by the C++ kernel.
verbosebool, defaultTruePrint kernel timing information.
Worked example¶
The example below runs end-to-end on synthetic data (no lab files required). It computes the time-resolved power of a 10 Hz sinusoid embedded in white noise and checks that the dominant frequency estimate is at 10 Hz.
import numpy as np
from ptsa.data.timeseries import TimeSeries
from ptsa.data.filters import MorletWaveletFilter
# 2 s of data at 500 Hz: 10 Hz sine plus 0.1-amplitude white noise.
rng = np.random.default_rng(0)
samplerate = 500.0
t = np.arange(0.0, 2.0, 1.0 / samplerate)
signal = np.sin(2 * np.pi * 10.0 * t) + 0.1 * rng.standard_normal(t.size)
# Wrap as a TimeSeries (samplerate coord is required).
ts = TimeSeries.create(
signal[np.newaxis, :],
samplerate,
dims=('channel', 'time'),
coords={'channel': [0], 'time': t},
)
# Decompose at 5, 10, 20, 40 Hz.
freqs = np.array([5.0, 10.0, 20.0, 40.0])
wf = MorletWaveletFilter(freqs=freqs, output='power',
width=5, complete=True,
cpus=1, verbose=False)
power = wf.filter(ts)
# power has shape (freq, channel, time).
assert power.shape == (4, 1, t.size)
# Mean power over the central 1 s (drop edge artifacts).
middle = (t > 0.5) & (t < 1.5)
mean_power = power.values[:, 0, middle].mean(axis=1)
dominant_freq = freqs[mean_power.argmax()]
assert dominant_freq == 10.0
The returned power is a TimeSeries
with dims ('frequency', 'channel', 'time'). The leading boundary
samples (roughly \(3.5 \sigma_t\) on each side, i.e.a few
wavelet widths) carry edge artifacts; users typically discard them
or add a mirror buffer with
add_mirror_buffer() before
filtering.
Parameterization comparison¶
Different libraries parameterize the Morlet wavelet differently. Mixing them is a common source of analysis bugs. The table below summarizes the three conventions you are most likely to encounter.
Library |
Width parameter |
Notes |
|---|---|---|
PTSA (this package) |
|
|
|
|
Compatible parameterization. MNE also offers a zero-mean
option ( |
|
|
The naming conflict (both |
Warning
Do not pass a SciPy-style w value to PTSA’s width (or
vice versa). The two parameterizations differ by both definition
and scale, and using the wrong one silently produces a
well-formed but incorrect time-frequency map.