Can't find the right energy using scipy.signal.welch Can't find the right energy using scipy.signal.welch numpy numpy

Can't find the right energy using scipy.signal.welch


The resolution to this apparent discrepancy lies in a careful understanding and application of

  • continuous vs. discrete Fourier transforms, and
  • energy, power, and power spectral density of a given signal

I too have 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 certain integrability conditions has a Fourier transform X(f). When working with a discrete signal x[n], however, it is often conventional to work with the discrete-time Fourier transform (DTFT). I will denote the DTFT as X_{dt}(f), where dt equals the time interval between adjacent samples. The key to answering your question requires that you recognize that the DTFT is not equal to the corresponding Fourier transform! In fact, the two are related as

X_{dt}(f) = (1 / dt) * X(f)

Further, the discrete Fourier transform (DFT) is simply a discrete sample of the DTFT. The DFT, of course, is what Python returns when using np.fft.fft(...). Thus, your computed DFT is not equal to the Fourier transform!

Power spectral density

scipy.signal.welch(..., scaling='density', ...) returns an estimate of the power spectral density (PSD) of discrete signal x[n]. A full discussion of the PSD is a bit beyond the scope of this post, but for a simple periodic signal (such as that in your example), the PSD S_{xx}(f) is given as

S_{xx} = |X(f)|^2 / T

where |X(f)| is the Fourier transform of the signal and T is the total duration (in time) of the signal (if your signal x(t) were instead a random process, we'd have to take an ensemble average over many realizations of the system...). The total power in the signal is simply the integral of S_{xx} over the system's frequency bandwidth. Using your code above, we can write

import scipy.signal# Estimate PSD `S_xx_welch` at discrete frequencies `f_welch`f_welch, S_xx_welch = scipy.signal.welch(x, fs=fs)# Integrate PSD over spectral bandwidth# to obtain signal power `P_welch`df_welch = f_welch[1] - f_welch[0]P_welch = np.sum(S_xx_welch) * df_welch

To make contact with your np.fft.fft(...) computations (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, to compute the power spectral density (or total power) from the FFT computations, we need to recognize that

S_{xx} = |X[k]|^2 * (dt ^ 2) / T

# Compute DFTXk = np.fft.fft(x)# Compute corresponding frequenciesdt = time[1] - time[0]f_fft = np.fft.fftfreq(len(x), d=dt)# Estimate PSD `S_xx_fft` at discrete frequencies `f_fft`T = time[-1] - time[0]S_xx_fft = ((np.abs(Xk) * dt) ** 2) / T# Integrate PSD over spectral bandwidth to obtain signal power `P_fft`df_fft = f_fft[1] - f_fft[0]P_fft = np.sum(S_xx_fft) * df_fft

Your values for P_welch and P_fft should be very close to each other, as well as close to the expected power in the signal, which can be computed as

# Power in sinusoidal signal is simply squared RMS, and# the RMS of a sinusoid is the amplitude divided by sqrt(2).# Thus, the sinusoidal contribution to expected power isP_exp = (amp / np.sqrt(2)) ** 2 # For white noise, as is considered in this example,# the noise is simply the noise PSD (a constant)# times the system bandwidth. This was already# computed in the problem statement and is given# as `noise_power`. Simply add to `P_exp` to get# total expected signal power.P_exp += noise_power

Note: P_welch and P_fft will not be exactly equal, and likely not even equal within numerical accuracy. This is attributable to the fact that there are random errors associated with the estimation of the power spectral density. In an effort to reduce such errors, Welch's method splits your signal into several segments (the size of which is controlled by the nperseg keyword), computes the PSD of each segment, and averages the PSDs to obtain a better estimate of the signal's PSD (the more segments averaged over, the less the resulting random error). The FFT method, in effect, is equivalent to only computing and averaging over one, large segment. Thus, we expect some differences between P_welch and P_fft, but we should expect that P_welch is more accurate.

Signal Energy

As you stated, the signal energy can be obtained from the discrete version of Parseval's theorem

# Energy obtained via "integrating" over timeE = np.sum(x ** 2)# Energy obtained via "integrating" DFT components over frequency.# The fact that `E` = `E_fft` is the statement of # the discrete version of Parseval's theorem.N = len(x)E_fft = np.sum(np.abs(Xk) ** 2) / N

We'd like to now understand how S_xx_welch, computed above via scipy.signal.welch(...), relates to the total energy E in the signal. From above, S_xx_fft = ((np.abs(Xk) * dt) ** 2) / T. Rearranging the terms in this expression, we see that np.abs(Xk) ** 2 = (T / (dt ** 2)) * S_xx_fft. Further,

From above, we know that np.sum(S_xx_fft) = P_fft / df_fft and that P_fft and P_welch are approximately equal. Further, P_welch = np.sum(S_xx_welch) / df_welch so that we obtain

np.sum(S_xx_fft) = (df_welch / df_fft) * np.sum(S_xx_welch)

Further, S_xx_fft = ((np.abs(Xk) * dt) ** 2) / T. Substituting S_xx_fft into the equation above and rearranging 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 look very close to the expression for the total energy in the signal as computed from the DFT components. Now, note that T / dt = N, where N is the number of sample points in your signal. Dividing through by N, we now have a LHS that is, by definition, equal to the E_fft computed above. Thus, we can obtain the total energy in the signal from Welch's PSD via

# Signal energy from Welch's PSDE_welch = (1. / dt) * (df_welch / df_fft) * np.sum(S_xx_welch)

E, E_fft, and E_welch should all be very close in value :) As discussed at the end of the preceding section, we do expect some slight differences between E_welch compared to E and E_fft, but this is attributable to the fact that values derived from Welch's method have reduced random error (i.e. are more accurate).