I’m trying to wrap my head around MNE-Python’s implementation of Morlet wavelets, especially as compared to SciPy’s implementation. Here’s what I think I’ve figured out so far:
scipy’s morlet2
function parameterizes in terms of M
(total number of samples in the wavelet), s
(“width”) and w
(“omega0”). MNE parameterizes in terms of freq
(frequency in Hz), n_cycles
(float), and sigma
(float or None). For now I’m only considering the sigma=None
condition (in which n_cycles
is fixed across frequencies and temporal resolution varies with frequency). I’m pretty sure that SciPy’s w
AKA “omega0” is the same as n_cycles
, so I think that means that:
w = n_cycles
s = sfreq * w / (2 * pi * freq)
M = 2 * ceil(s) - 1
However, if I define the SciPy inputs in terms of the MNE-Python inputs in this way, The results don’t match: I have to add in a factor of 5 into the definition of M
, and then the SciPy output matches the MNE output (up to a scaling factor):
M = 2 * ceil(5 * s) - 1 # note the 5 here
This multiplier 5 shows up in the MNE-Python code here, in the definition of the time vector: mne-python/tfr.py at f26528d78764c83f754873c40f17e40d5eb08d2d · mne-tools/mne-python · GitHub, but I can’t figure out why it is there. I know it’s been over a decade since you wrote this code @agramfort, but any chance you can explain how that 5
got there? Based on the comments you seem to be following the math in https://doi.org/10.1523/JNEUROSCI.18-11-04244.1998 but I can’t find a 5
anywhere in there either.
MWE:
import matplotlib.pyplot as plt
import numpy as np
from mne.time_frequency import morlet
from scipy.signal import morlet2
sfreq = 1000
freqs = [17]
n_cycles = 3
w = n_cycles
s = sfreq * w / (2 * np.pi * freqs[0])
M_1 = int(2 * np.ceil(s) - 1)
M_5 = int(2 * np.ceil(5 * s) - 1) # note the 5 here
mne_wav = morlet(sfreq=sfreq, freqs=freqs, n_cycles=n_cycles, zero_mean=False)[0]
scipy_wav_1 = morlet2(M=M_1, s=s, w=w)
scipy_wav_5 = morlet2(M=M_5, s=s, w=w)
with plt.ion():
fig, axs = plt.subplots(2, 1, sharex=True)
axs[0].plot(mne_wav.real)
axs[1].plot(scipy_wav_5.real, label='with five')
axs[1].plot(scipy_wav_1.real, label='without five')
axs[0].set_title('MNE')
axs[1].set_title('SciPy')
axs[1].legend()
Another question I have is why the energy normalization is different between SciPy and MNE; SciPy multiplies by pi ** (-1/4) * sqrt(1 / s)
whereas MNE divides by sqrt(0.5) * np.linalg.norm(W.ravel())
. Wikipedia seems to agree more closely with SciPy on this one (at least, there is a pi ** -1/4
term in their equation: Morlet wavelet - Wikipedia).