The resolution of this apparent discrepancy lies in a thorough understanding and application.
- continuous with respect to discrete Fourier transforms and
- energy, power and spectral power density of a given signal
I also struggled with this exact question, so I will try to be as explicit as possible in the discussion below.
Discrete Fourier Transform (DFT)
A continuous signal x (t) satisfying some integrability conditions has the Fourier transform X (f). However, when working with a discrete signal x [n], it is often customary to work with a discrete time Fourier transform (DTFT). I will denote the DTFT as X_ {dt} (f), where dt is equal to the time interval between adjacent samples. The key to answering your question requires that you acknowledge that the DTFT does not match the corresponding Fourier transform! In fact, the two are related as
X_ {dt} (f) = (1 / dt) * X (f)
Furthermore, the discrete Fourier transform (DFT) is simply a discrete DTFT sample. Of course, DFT is what Python returns when using np.fft.fft(...) . So your calculated DFT is not equal to the Fourier transform!
Power spectral density
scipy.signal.welch(..., scaling='density', ...) returns the power spectral density (PSD) estimate of the discrete signal x [n]. A full discussion of PSD is slightly beyond the scope of this publication, but for a simple periodic signal (for example, in your example) PSD S_ {xx} (f) is given as
S_ {xx} = | X (f) | ^ 2 / T
where | X (f) | represents the Fourier transform of the signal, and T is the total duration (in time) of the signal (if your signal x (t) instead was a random process, we would have to take the average of many implementations of the system ...). The total power in the signal is simply the integral of S_ {xx} over the system bandwidth. Using your code above, we can write
import scipy.signal
To contact your calculations np.fft.fft(...) (which return the DFT), we must use the information from the previous section, namely that
X [k] = X_ {dt} (f_k) = (1 / dt) * X (f_k)
Thus, in order to calculate the power spectral density (or apparent power) from the FFT calculations, we need to recognize that
S_ {xx} = | X [k] | ^ 2 * (dt ^ 2) / T
# Compute DFT Xk = np.fft.fft(x)
Your values ββfor P_welch and P_fft should be very close to each other, and also close to the expected signal strength, which can be calculated as
# Power in sinusoidal signal is simply squared RMS, and
Note: P_welch and P_fft will not be exactly equal and probably will not even be equal to the exact digits. This is due to the fact that there are random errors associated with the estimation of the power spectral density. To reduce such errors, the Welch method splits your signal into several segments (the size of which is controlled by the nperseg ), calculates the PSD of each segment and averages the PSD to get a better estimate of the PSD signal (the more segments are averaged, the resulting random error). The FFT method, in fact, is equivalent only to calculating and averaging over one large segment. Thus, we expect some differences between P_welch and P_fft , but we should expect P_welch be more accurate.
Signal energy
As you said, the signal energy can be obtained from a discrete version of the Parseval theorem
# Energy obtained via "integrating" over time E = np.sum(x ** 2)
We would now like to understand how S_xx_welch calculated above via scipy.signal.welch(...) refers to the total energy E in the signal. Above, S_xx_fft = ((np.abs(Xk) * dt) ** 2) / T Reordering the terms in this expression, we see that np.abs(Xk) ** 2 = (T / (dt ** 2)) * S_xx_fft . Moreover,
We know from above that np.sum(S_xx_fft) = P_fft / df_fft and that P_fft and P_welch approximately equal. Next, P_welch = np.sum(S_xx_welch) / df_welch , so we get
np.sum(S_xx_fft) = (df_welch / df_fft) * np.sum(S_xx_welch)
Next, S_xx_fft = ((np.abs(Xk) * dt) ** 2) / T Substituting S_xx_fft into the above equation and rearranging the terms, we arrive at
np.sum(np.abs(Xk) ** 2) = (T / (dt ** 2)) * (df_welch / df_fft) * np.sum(S_xx_welch)
The left-hand side (LHS) in the above equation should now be very close to the expression for the total energy in the signal calculated from the DFT components. Now notice that T / dt = N , where N is the number of sample points in your signal. Separating N , we now have LHS, which by definition is equal to E_fft calculated above. Thus, we can get the full energy in the signal from the Welch PSD through
E , E_fft and E_welch should be very close in value :) As discussed at the end of the previous section, we expect slight differences between E_welch compared to E and E_fft , but this is due to the fact that the values ββobtained from the Welch method reduced the random error (i.e. more precisely).