Repository navigation
Open
Labels
Description
Bug summary
If my understanding is correct:
NFFTofmlab._spectral_helpercorresponds tonpersegof spectral funtions ofscipy.signal.
It's the actual length of signal (segment) for DFT.pad_toofmlab._spectral_helpercorresponds tonfftof spectral funtions ofscipy.signal.
It's the length of DFT, i.e. parameternoffftfuntion.
matplotlib/lib/matplotlib/mlab.py
Line 385 in 3418bad
result = np.fft.fft(result, n=pad_to, axis=0)[:numFreqs, :]
But the implementation of Matplotlib is really confusion:
-
If length of signal is less than NFFT, signal will pad to length of NFFT.
matplotlib/lib/matplotlib/mlab.py
Lines 342 to 351 in 3418bad
# zero pad x and y up to NFFT if they are shorter than NFFT if len(x) < NFFT: n = len(x) x = np.resize(x, NFFT) x[n:] = 0 if not same_data and len(y) < NFFT: n = len(y) y = np.resize(y, NFFT) y[n:] = 0 And the window is determined by NFFT instead of actual length of signal. This would cause wrong window correction.
matplotlib/lib/matplotlib/mlab.py
Lines 376 to 380 in 3418bad
if not np.iterable(window): window = window(np.ones(NFFT, x.dtype)) if len(window) != NFFT: raise ValueError( "The window length must match the data's first dimension") -
The Nyquist frequency is
NFFT/2instead ofpad_to/2.
matplotlib/lib/matplotlib/mlab.py
Lines 411 to 416 in 3418bad
# if we have a even number of frequencies, don't scale NFFT/2 if not NFFT % 2: slc = slice(1, -1, None) # if we have an odd number, just don't scale DC else: slc = slice(1, None, None)
Code for reproduction
import numpy as np
from matplotlib import mlab
fs = 1000
t = np.arange(0, 1, 1/fs)
rng = np.random.default_rng(42)
s = np.sin(2 * np.pi * 100 * t) + 0.5 * rng.normal(size=t.shape)
nfft = 2048
nsignal = len(s) #1000
psd, freqs = mlab.psd(s, NFFT=nfft, Fs=fs, window=mlab.window_none)
mag = np.abs(np.fft.rfft(s, n=nfft))
psd1 = (mag)**2/nsignal/fs
if not nfft % 2:
psd1[1:-1] *= 2
else:
psd1[1:] *= 2
relative_error = np.abs( 2 * (psd-psd1)/(psd + psd1) )
print(relative_error.max())Actual outcome
0.6876640419947717
Expected outcome
0
Activity
jklymak commentedon Dec 27, 2022
This implementation is pretty clunky, and we strongly recommend just using scipy. Indeed, I was hoping to deprecate the whole thing, but there wasn't sufficient support (#22920)
You are not quite using the parameters correctly, but I agree the results are still incorrect:
yields:
So, the padded normalization is incorrect by a factor of 1000/2048.
gapplef commentedon Dec 28, 2022
If
NFFTwas meant to be the actual length of signal (segment) for DFT, I think the following code is the source of the bug.matplotlib/lib/matplotlib/mlab.py
Lines 342 to 351 in 3418bad
And the problem should be fixed with:
Also the following code need be fixed for correct one-sided psd:
matplotlib/lib/matplotlib/mlab.py
Lines 411 to 416 in 3418bad
jklymak commentedon Dec 28, 2022
@gapplef Maybe?
_spectral_helperis written so thatpad_toends up asnto FFT. That is not what I would have done, and I don't understand why it is that way. However, as above, I think we should not be dragging around a different API fromscipy.signal, and should just remove all this, the practical problem is that trying to fix the inconsistencies will introduce fixes that are not necessarily back-compatible.QuLogic commentedon Dec 28, 2022
cc @oscargus
misjob commentedon Jan 9, 2024
Also the zero padding if NFFT is less then the signal length should be performed after the detrend. Otherwise the zero padding would screw up the "mean" and "linear" calculations.