DPSS Multi-Tapering and STFT
See original GitHub issueI tried to implement a standard multi-tapered short time fourier analysis only using functionality from scipy.signal
and ran into problematic results.
Reproducing Code Example
import numpy as np
from scipy.signal import windows, stft
from numpy.random import randn
nSamples = 1000
win_size = nSamples // 4
# create a slepian sequence of 6 orthogonal tapers
slepians = windows.dpss(win_size, NW=2, Kmax=6)
signal = randn(nSamples)
# stft for each taper
for order, taper in enumerate(slepians):
freqs, times, pxx = stft(signal, window=taper, nperseg=win_size)
# the mean power over all windows
power_sum = np.abs(pxx).mean()
print(f'order {order}: {power_sum:e}')
This gives something like:
order 0: 6.270107e-02
order 1: 3.343302e+12
order 2: 1.013352e-01
order 3: 2.472677e+13
order 4: 2.233694e-01
order 5: 8.161814e+12
In multi-taper analysis one would now average along the different slepians to get a spectral estimate. However this clearly can’t work here as the odd slepians give a power spectral estimate >10orders of magnitude larger than the even ones. The reason for this is the built-in normalization of SciPy’s stft
, which normalizes by the window norm. However, mathematically the odd Slepians fullfill win.sum() = 0
, in practice it’s often more like win.sum() = 1e-13
, hence SciPy’s normalization blows up the result. I ran some more in depth numerical tests and found some troubling instances where these number can become astronomical.
Suggestion
Maybe add a norm=None
parameter to stft
, right now a certain norm=spectral
is anyways enforced internally in scipy.signal.spectral._spectral_helper(...)
.
Issue Analytics
- State:
- Created 2 years ago
- Comments:7 (3 by maintainers)
Update
Ok I now have a complete script which gets the correct normalizations for both types of
scaling
. The key lines are for normal windowed FFT analysis:The factor 3(!!) for the windowed FFT for
scaling='density'
is really weird, but as I see it it’s really needed to recover the amplitudes. Would be glad if you or someone could shine some light on this and/or check this again…For multi-taper analysis:
periodogram
results to get the amplitudes:Note that for
scaling='spectrum
the slepians have to be normalized by hand, whereas for the other windows (for example ‘hann’ or ‘bartlett’) it works right away.The results look fine now:
I attach the (zipped) script for brevity. mtm_norm_scipy.py.zip
Conclusion
I somehow find both options quite opaque, getting the right normalization involves quite some head scratching and educated guessing for both types of scaling:
scaling='density'
needs a counter-intuitive factor (3(!!)) for normal windowed analysis, and a somewhat reasonable factor for the multi-taper analysisscaling='spectrum'
(default forsignal.stft
) needs more hands-on work (reversing the normalization) for the slepians, and might run into numerical precision problems ifsym=True
, which is the default forscipy.signal.dpss
. But it’s more natural for the standard windowed FFT (plainperiodogram
).I will take a step back now, and see if someone @SciPy with more intuition about Scipy’s inner workings might find a good solution? In any case the attached script shows it’s possible to get a correctly normalized multi-taper analysis, it’s just not really straighforward imho.
Edit: The initial problem of this issue (multi-taper short time fourier) can be addressed by using the oultined
scaling='spectrum'
approach, assignal.stft
hardcodes this scaling (there is noscaling
parameter forstft
!) Settingsym=False
as suggested by @dhruv9vats is the safer option to avoid numerical precision issuesOk, that’s a lot of information to process here…
I agree that it doesn’t look too bad, most and foremost the
sym=True/False
for the slepians has apparently no effect anymore! I traced it to the spectral parameterscaling
, which has the default ofscaling='density'
for bothsignal.periodogram
andsignal.welch
, butscaling='spectrum'
forsignal.stft
. Try to addscaling='spectrum'
to yoursignal.welch(...)
calls above, and you will see everything blows apart! Withscaling='density'
the quadratic norm of the window insidesignall.spectral._spectral_helper()
is taken, and hence the problematic sum-to-zero for the odd slepians disappears. So with this we found that the default behaviors of thesescipy.signal
spectral estimation methods differ, and that matters a lot for multi-taper analysis! Whereas the slepiansym
parameter has no effect forscaling='density'
.Harmonic Amplitudes
I went the other way around, and build a little test-scenario with
scaling='spectrum'
as to be close to my initial issue withscipy.stft
:This gives me:
So by using a plain Hann window I can recover the harmonic amplitude of the test signal, whereas for the slepians neither
sym=True
norsym=False
give the correct result (but they still differ!!). Moreover, when you zoom into the power spectrum you see a wrong double peak, as the normalization bites us:I actually now found a way to get a correct normalization (via reversing scipy’s and a little educated guessing), with the two commented lines in the script above I can get it to work 😃
Discussion
So what to make of it all? I will see that I get a correct normalization working (recovering the harmonic amplitude) with
scaling='density'
. If that works nicely, than at least the defaultscaling
should be the same for allscipy.signal
methods?!Right now out-of-the box multi-taper analysis with
stft
is definitely broken, as it is for the other methods withscaling='spectrum'
. The cleanest solution imho would still be to have an option to turn off automatic normalization for expert users (scaling=None
), so they can take care about normalization themselves.