cjo
cjo

Reputation: 21

Calculate non-integer frequency with NumPy FFT

I would like to calculate the frequency of a periodic time series using NumPy FFT. As an example, let's say my time series y is defined as follows:

import numpy as np
freq = 12.3
x = np.arange(10000)
y = np.cos(x * 2 * np.pi * freq / 10000)

If the frequency is an integer, I can calculate it using np.argmax(np.abs(np.fft.fft(y))). However, in case the frequency is not an integer, how do I calculate the frequency with more precision?

EDIT: To clarify, we are not supposed to know how the time series y is generated. The above code snippet is just an artificial example of how a non-integer frequency could come up. Obviously if we already know the function that generates the time series, we don't need FFT to determine the frequency.

Upvotes: 2

Views: 2262

Answers (3)

hotpaw2
hotpaw2

Reputation: 70743

You can try using either interpolation or zero-padding (which is equivalent to entire vector interpolation) to potentially improve your frequency estimation, if the S/N allows. Sinc kernel interpolation is more accurate than parabolic interpolation.

Upvotes: 0

Warren Weckesser
Warren Weckesser

Reputation: 114956

You can pad the data with zeros before computing the FFT.

For example, here's your original calculation. It finds the Fourier coefficient with the maximum magnitude at frequency 12.0:

In [84]: freq = 12.3

In [85]: x = np.arange(10000)

In [86]: y = np.cos(x * 2 * np.pi * freq / 10000)

In [87]: f = np.fft.fft(y)

In [88]: k = np.argmax(np.abs(f))

In [89]: np.fft.fftfreq(len(f), d=1/10000)[k]
Out[89]: 12.0

Now recompute the Fourier transform, but pad the input to have a length of six times the original length (you can adjust that factor as needed). With the padded signal the Fourier coefficient with maximum magnitude is associated with frequency 12.333:

In [90]: f = np.fft.fft(y, 6*len(y))

In [91]: k = np.argmax(np.abs(f))

In [92]: np.fft.fftfreq(len(f), d=1/10000)[k]
Out[92]: 12.333333333333332

Here's a plot that illustrates the effect of padding the signal. The signal is not the same as above; I used different values with a much shorter signal to make it easier to see the effect. The shapes of the lobes are not changed, but the number of points at which the frequency is sampled is increased.

plot

The plot is generated by the following script:

import numpy as np
import matplotlib.pyplot as plt

fs = 10
T = 1.4
t = np.arange(T*fs)/fs

freq = 2.6
y = np.cos(2*np.pi*freq*t)

fy = np.fft.fft(y)
magfy = np.abs(fy)
freqs = np.fft.fftfreq(len(fy), d=1/fs)
plt.plot(freqs, magfy, 'd', label='no padding')

for (factor, markersize) in [(2, 9), (16, 4)]:
    fy_padded = np.fft.fft(y, factor*len(y))
    magfy_padded = np.abs(fy_padded)
    freqs_padded = np.fft.fftfreq(len(fy_padded), d=1/fs)
    plt.plot(freqs_padded, magfy_padded, '.', label='padding factor %d' % factor,
             alpha=0.5, markersize=markersize)

plt.xlabel('Frequency')
plt.ylabel('Magnitude of Fourier Coefficient')
plt.grid()
plt.legend(framealpha=1, shadow=True)

plt.show()

Upvotes: 1

Djib2011
Djib2011

Reputation: 7442

You need to give your signal more resolution

import numpy as np
freq = 12.3
x = np.arange(100000)  # 10 times more resolution
y = np.cos(x * 2 * np.pi * freq / 10000)  # don't change this

print(np.argmax(np.abs(np.fft.fft(y))) / 10)  # divide by 10
# 12.3

The number of data points in x need to be 10 times more than the number you divide y with. You could get the same effect like this:

x = np.arange(10000)
y = np.cos(x * 2 * np.pi * freq / 1000)

print(np.argmax(np.abs(np.fft.fft(y))) / 10) 
# 12.3

If you want to find the frequency with two decimals the resolution needs to be 100 times more.

freq = 12.34
x = np.arange(10000)  
y = np.cos(x * 2 * np.pi * freq / 100)  # 100 times more resolution

print(np.argmax(np.abs(np.fft.fft(y))) / 100)  # divide by 100
# 12.34

Upvotes: 1

Related Questions