Your code is bit unclear, especially generation of your signal. Python allows for vectorized operations so it is good to use it. What's more, it is good to clearly specify the sampling frequency of your signal and use it then. Also please remember to normalize your FFT by length of your signal (in this particular case) and multiply by 2 (half of spectrum is removed so energy must be preserved).
Phenomena you are facing is connected with fact that your frequency is not matching exactly frequency bin for it. Because of that energy is leaking into other frequency bins. This is due to fact that you don't have integer amount of cycles of your sinusoid. Please search for spectral leakage. Easy way to check that is to use the equation for frequency spacing in frequency domain, that is:
$$\Delta f=\dfrac{f_s}{N} $$
Where $f_s $ is your sampling frequency. Then your frequency vector is:
$$f=[0, \; \Delta f, \; 2\Delta f, \; ... \;,(N-2)\Delta f, \;(N-1)\Delta f] $$
If frequency you are analysing matches exactly one in your frequency vector, then amplitude will be correctly estimated, otherwise - leakage. In order to minimize this effect, you might want to apply window to your signal. It will reduce amount of energy leaking to other bins and in slight way will improve estimation of original amplitude, although you won't be really able to retrieve it in 100%.
Here is small modification of your code to help you visualize that. The red curve is ideal spectrum and blue one is for given number of frequency bins (strictly connected to your signal length).
import numpy as np
import matplotlib.pyplot as plt
import time
def get_fft(y, fs):
""" Get the FFT of a given signal and corresponding frequency bins.
Parameters:
y - signal
fs - sampling frequency
Returns:
(mag, freq) - tuple of spectrum magitude and corresponding frequencies
"""
n = len(y) # Get the signal length
dt = 1/float(fs) # Get time resolution
fft_output = np.fft.rfft(y) # Perform real fft
rfreqs = np.fft.rfftfreq(n, dt) # Calculatel frequency bins
fft_mag = np.abs(fft_output) # Take only magnitude of spectrum
# Normalize the amplitude by number of bins and multiply by 2
# because we removed second half of spectrum above the Nyqist frequency
# and energy must be preserved
fft_mag = fft_mag * 2 / n
return np.array(fft_mag), np.array(rfreqs)
def build_signal(A, f, N, fs):
""" Generate cosinusoidal signal of a given frequency and amplitude.
Parameters:
A - amplitude of a signal
f - fundamental frequency
N - number of signal samples
fs - sampling frequency
Returns:
t - time vector
y - generated signal
"""
dt = 1/float(fs) # Get the time domain spacing
T = N*dt # Upper time limit
t = np.arange(0, T, dt) # Construct time vector
y = A * np.cos(2*np.pi*f*t) # Generate the signal
return t, y
if __name__ == "__main__":
# Define the parameters
fs = 100 # Sampling frequency (must satisfy the Nyquist theorem)
f = 10 # Signal fundamental frequency
A = 0.5 # Signal amplitude
# Prepare figure for plotting
plt.figure()
plt.ion()
plt.show(block=False)
f_off = 1 # Frequency offset in Hz for zoom
# Precomute the spectrum without any leakage
(t, y) = build_signal(A, f, 1000, fs)
(MAG, FREQ) = get_fft(y, fs)
for N in np.arange(1000, 10010, 1):
# Clear and scale
plt.clf()
# Generate the signal
(t, y) = build_signal(A, f, N, fs)
# Get the spectrum and frequencies
(mag, freq) = get_fft(y, fs)
# Plot the reference spectrum
plt.plot(FREQ, MAG, '-or')
plt.hold('on')
# Plot the spectrum for given number of bins
plt.plot(freq, mag, '-o')
plt.title('Frequency spectrum for %d bins' % N)
plt.xlabel('Frequency')
plt.ylabel('Amplitude')
plt.grid('on')
plt.hold('off')
plt.legend(['Ideal spectrum', 'Spectrum for %d bins' % N])
plt.xlim((f-f_off, f+f_off))
plt.ylim((0, A))
plt.draw()
plt.pause(0.5)
Two intermediate steps. Signal is 10 Hz sinusoid with an amplitude of 0.5, sampling frequency is 100 Hz.
Exact match:
Frequency component you are analysis falls between two frequency bins: