Examples DFT and FFT

The following example builds up a square pulse and it applies the functions contained in the module fourier.py to compute the DFT of the function just defined.
Let us import the necessary modules:

In [1]:
import numpy as np
import matplotlib.pyplot as plt
import fourier as f

Now we build the square pulse function, defined as:
\[f(t) = \begin{cases} 0 & \textrm{for}\ 0 \leq t < \frac{\pi}{2} \\ 1 & \textrm{for}\ \frac{\pi}{2} \leq t \leq \frac{3\pi}{2} \\ 0 & \textrm{for}\ \frac{3\pi}{2} < t. \\ \end{cases}\] Notice that, to avoid the sharp discontinuity at \(t=\pi/2\) and \(t=3\pi/2\) we set the value of the function in these points as the average of the two values \(0\) and \(1\), namely: \(0.5\)!

In [26]:
N=128
dt=2.0*np.pi/N
t=np.arange(0.0, 2.0*np.pi, dt)
y=np.zeros(N)

# Defines the rectangular pulse function in the vector y

for j in range(N):
    y[j]=0.0         # Zero everywhere...
    if (( t[j] > 0.5*np.pi ) & ( t[j] < 1.5*np.pi )):
        y[j]=1.0     # except between pi/2 and 3pi/2...
    elif ( t[j] == 0.5*np.pi ):
        y[j] = 0.5   # whilst we set 0.5 in pi/2...
    elif ( t[j] == 1.5*np.pi ):
        y[j] = 0.5   # ... and 3pi/2!

We now plot the vector to be sure the definition was correct:

In [27]:
plt.plot(t,y)
plt.title("A square pulse")
plt.xlabel(r"$t$")
plt.ylabel(r"$y(t)$")
plt.xlim(0.0, 2.0*np.pi)
plt.ylim(-0.2, +1.2)
plt.show()

We now compute and print the Fourier coefficients of the function by using the dft function in the fourier.py module (already included before as "f"!)

In [28]:
cf=f.dft(y,dx)
print(cf)
[  5.00000000e-01 +0.00000000e+00j  -3.18245968e-01 -4.68375339e-17j
   1.90819582e-17 -1.72644847e-17j   1.05911478e-01 +5.03069808e-17j
  -1.90819582e-17 -1.03570006e-17j  -6.33420766e-02 -4.16333634e-17j
  -5.20417043e-18 +1.73786445e-17j   4.50245469e-02 +4.16333634e-17j
  -1.10946196e-16 -5.16080234e-17j  -3.47906424e-02 -1.90819582e-16j
   4.36933476e-17 -6.74198397e-18j   2.82307475e-02 +1.38777878e-16j
  -1.51788304e-18 -2.10963417e-17j  -2.36487750e-02 +0.00000000e+00j
   4.70543743e-17 +2.09713356e-18j   2.02531447e-02 +1.24900090e-16j
  -1.38777878e-17 +0.00000000e+00j  -1.76247176e-02 -9.71445147e-17j
   2.73218947e-17 +2.95845285e-17j   1.55207718e-02 +3.46944695e-17j
   2.92734587e-17 -4.07261407e-17j  -1.37909912e-02 +0.00000000e+00j
   6.24500451e-17 -1.36657375e-18j   1.23372857e-02 +1.14491749e-16j
  -1.36724267e-17 -3.72965547e-17j  -1.10928977e-02 -1.02348685e-16j
  -7.80625564e-17 +1.78808846e-17j   1.00107936e-02 -5.29090660e-17j
   3.03576608e-18 +4.22095859e-17j  -9.05685865e-03 +5.55111512e-17j
   1.43114687e-16 +1.10095149e-16j   8.20572536e-03 +7.63278329e-17j
   3.33066907e-16 +1.66533454e-16j  -7.43811833e-03 +6.93889390e-17j
   1.05167611e-16 -8.09926139e-17j   6.73910885e-03 +5.55111512e-16j
   8.37004077e-17 -3.72681730e-17j  -6.09693484e-03 +5.96744876e-16j
   8.15320034e-17 -4.68754651e-16j   5.50218329e-03 +5.20417043e-18j
   1.63878220e-16 +1.24466409e-16j  -4.94721106e-03 -1.43548368e-16j
   8.93382590e-17 +3.15614280e-16j   4.42572658e-03 -4.94396191e-17j
  -4.07660017e-17 +5.64743157e-19j  -3.93248203e-03 +6.40112963e-16j
  -1.40902914e-15 +1.02083532e-16j   3.46304308e-03 +2.42861287e-17j
  -1.73472348e-16 +3.46944695e-17j  -3.01361380e-03 +8.32667268e-17j
  -3.44559450e-16 -2.20005364e-17j   2.58090139e-03 +2.87964097e-16j
   2.81025203e-16 +1.56358856e-16j  -2.16200992e-03 +2.60208521e-18j
  -1.18720138e-16 +2.69540615e-17j   1.75435554e-03 +3.90312782e-17j
   5.36773626e-17 +1.50053581e-16j  -1.35559734e-03 +1.21430643e-16j
   4.59810141e-16 +1.93002494e-16j   9.63579970e-04 -1.17093835e-16j
   1.01416271e-15 +5.67807841e-16j  -5.76284621e-04 +2.02962647e-16j
  -8.58688121e-17 +5.24608598e-16j   1.91786110e-04 +1.34441069e-16j
   0.00000000e+00 +3.33066907e-16j]

We had already computed the theoretical spectrum during the lectures as:

 

\[c_n = \begin{cases} 1/2 & \textrm{for}\ n=0 \\ 0 & \textrm{for even}\ n \\ \frac{1}{n\pi} (-1)^{\frac{n-1}{2}} & \textrm{for odd}\ n \\ \end{cases} \]
You may notice that:
  1. The frequency \(n=0\) is exactly reproduced as \(1/2\);
  2. For \(n>0\), only the frequencies corresponding to odd values of \(n\) are different from \(0\);
  3. The odd frequencies are purely real, that is their imaginary part is always vanishing.


Now, we compute the spectrum of the function to compare the numerical and theoretical power-law spectrum, by using the function spectrum contained in the module fourier.py:

In [29]:
# Define a vector for the harmonics
nn=np.arange(0,N/2+1)
# Compute the numerical spectrum
sp=f.spectrum(cf,dt)
# Compute the theoretical spectrum
# Define a vector of doubles
thsp = np.ndarray(N/2+1, dtype=np.float64)
for n in nn:
    if ( n == 0 ):
        thsp[n] = np.pi / 2.0
    elif ( np.mod(n,2) == 0 ):
        thsp[n] = 0.0
    else:
        thsp[n] = (4.0/np.pi)*(n**-2.0)
plt.xscale("log")
plt.yscale("log")
plt.xlim(1.0,N/2+1.0)
plt.ylim(-10,1.0)
plt.plot(nn,sp,'b-',thsp,'ro')
plt.show()

Now, we try to reconstruct the function by usign the idft function for several values of the maximum harmonics Nmax:

In [30]:
Nmax=3
ff=f.idft(cf,dt,Nmax)
plt.title("Original and reconstructed function for Nmax="+str(Nmax))
plt.xlabel(r"$t$")
plt.ylabel(r"$y(t)$")
plt.xlim(0.0,2.0*np.pi)
plt.ylim(-0.2,+1.2)
plt.plot(t,y,'b-',t,ff,'ro')
plt.show()
In [31]:
Nmax=11
ff=f.idft(cf,dt,Nmax)
plt.title("Original and reconstructed function for Nmax="+str(Nmax))
plt.xlabel(r"$t$")
plt.ylabel(r"$y(t)$")
plt.xlim(0.0,2.0*np.pi)
plt.ylim(-0.2,+1.2)
plt.plot(t,y,'b-',t,ff,'ro')
plt.show()
In [32]:
Nmax=41
ff=f.idft(cf,dt,Nmax)
plt.title("Original and reconstructed function for Nmax="+str(Nmax))
plt.xlabel(r"$t$")
plt.ylabel(r"$y(t)$")
plt.xlim(0.0,2.0*np.pi)
plt.ylim(-0.2,+1.2)
plt.plot(t,y,'b-',t,ff,'ro')
plt.show()
In [33]:
Nmax=64
ff=f.idft(cf,dt,Nmax)
plt.title("Original and reconstructed function for Nmax="+str(Nmax))
plt.xlabel(r"$t$")
plt.ylabel(r"$y(t)$")
plt.xlim(0.0,2.0*np.pi)
plt.ylim(-0.2,+1.2)
plt.plot(t,y,'b-',t,ff,'ro')
plt.show()

FFTs in Numpy

Being numpy a mathematical library, it includes some functions to carry out the computation of FFTs. The main functions included in the numpy.fft submodule are:
rfft(A), irfft(A), rfft2(A), irfft2(A), rfftn(A), irfftn(A)
which compute the FFT of a real 1, 2, n-dimensional numpy.ndarray-s.
Analogously, there are functions to compute the FFT of complex 1, 2, \(n\)-dimensional numpy.ndarray-s:
fft(A), ifft(A), fft2(A), ifft2(A), fftn(A), ifftn(A)
We are interested, for the moment, to 1-dimensional transforms, therefore to the functions rfft(A) and irfft(A). We notice that:
  • the function ak=rfft(A) returns a vector of complex numbers with dimension N/2+1 as expected, containing only the frequencies \(n\omega\) with \(n=0, \ldots, N/2\);
  • the values ak[0] and ak[N/2] are ensured to have imaginary part exactly equal to zero, at difference with our "rough" dft function in the fourier.py module;
  • the direct transform function rfft() return a non-normalized fft, meaning that the results are NOT divided by the total number of points \(N\).


Example: let us consider the sawtooth function \(s(t)\), defined as: \[ s(t) = \begin{cases} t & \textrm{for}\ 0 \leq t < \pi \\ 0 & \textrm{for}\ t = \pi \\ t - 2\pi & \textrm{for}\ \pi < t \leq 2\pi \\ \end{cases} \] and let us apply the numpy.fft functions to compute the Fourier coefficients and to reconstruct the function:

In [34]:
# Define the sawtooth function s(t)
N2 = int( N / 2 )
s=np.zeros(N, dtype=np.float64)
s[0:N2] = t[0:N2]
s[N2] = 0.0
s[N2+1:] = t[N2+1:] - 2.0*np.pi
plt.title("Sawtooth function")
plt.xlabel(r"$t$")
plt.ylabel(r"$s(t)$")
plt.plot(t,s,'-')
plt.show()
In [44]:
# Computes the Fourier coefficients of the function through the python rfft function
cfs = np.fft.rfft(s)
# Normalize the coefficients by dividing for N and print them...
cfs = cfs / N
print(cfs)
[ -3.46944695e-18 +0.00000000e+00j  -4.09394740e-16 -9.99799194e-01j
   9.12217982e-17 +4.99598340e-01j   5.20417043e-18 -3.32730723e-01j
   1.02348685e-16 +2.49196293e-01j  -1.33573708e-16 -1.98995002e-01j
   1.13475572e-16 +1.65460136e-01j  -3.46944695e-18 -1.41448786e-01j
   1.73472348e-17 +1.23389475e-01j  -6.93889390e-18 -1.09298027e-01j
  -5.65273287e-17 +9.79839134e-02j   7.45931095e-17 -8.86895090e-02j
  -2.08166817e-17 +8.09097113e-02j   0.00000000e+00 -7.42948179e-02j
  -3.71477390e-17 +6.85950256e-02j   3.98986399e-17 -6.36271305e-02j
   1.04083409e-17 +5.92537156e-02j  -3.64291930e-17 -5.53696833e-02j
   3.74453705e-17 +5.18932780e-02j  -1.04083409e-17 -4.87599427e-02j
   2.94902991e-17 +4.59180192e-02j   0.00000000e+00 -4.33256766e-02j
   4.40866329e-17 +4.09486772e-02j   4.51028104e-17 -3.87587261e-02j
   1.73472348e-17 +3.67322318e-02j  -3.98986399e-17 -3.48493659e-02j
  -2.70370296e-17 +3.30933385e-02j   2.42861287e-17 -3.14498356e-02j
  -2.77555756e-17 +2.99065760e-02j  -1.43114687e-17 -2.84529606e-02j
  -1.37289721e-17 +2.70797918e-02j  -8.45677695e-18 -2.57790465e-02j
  -3.46944695e-18 +2.45436926e-02j   8.45677695e-18 -2.33675379e-02j
  -1.89331425e-17 +2.22451063e-02j   5.94142791e-17 -2.11715348e-02j
  -1.73472348e-17 +2.01424880e-02j   3.46944695e-17 -1.91540857e-02j
  -2.75090096e-18 +1.82028430e-02j   3.29597460e-17 -1.72856186e-02j
  -1.04083409e-17 +1.63995711e-02j   1.73472348e-17 -1.55421219e-02j
  -2.53023061e-17 +1.47109232e-02j   5.20417043e-17 -1.39038301e-02j
  -4.33680869e-17 +1.31188770e-02j   1.04083409e-17 -1.23542567e-02j
  -4.23519094e-17 +1.16083021e-02j  -5.72458747e-17 -1.08794707e-02j
  -1.73472348e-17 +1.01663303e-02j   8.84708973e-17 -9.46754697e-03j
  -2.32699512e-17 +8.78187364e-03j   5.55111512e-17 -8.10814083e-03j
  -2.08166817e-17 +7.44524776e-03j   3.29597460e-17 -6.79215449e-03j
  -2.18328592e-17 +6.14787495e-03j   1.38777878e-17 -5.51147048e-03j
  -1.04083409e-17 +4.88204400e-03j   6.59194921e-17 -4.25873466e-03j
  -4.48562444e-18 +3.64071288e-03j   2.60208521e-17 -3.02717575e-03j
  -1.73472348e-18 +2.41734273e-03j   5.20417043e-18 -1.81045153e-03j
  -1.28616103e-17 +1.20575430e-03j   2.77555756e-17 -6.02513835e-04j
  -3.46944695e-18 +0.00000000e+00j]

From the coefficients we can notice the following:
  1. the \(c_0\) and \(c_{N/2}\) coefficients have imaginary parts exactly zero;
  2. the \(c_0\) coefficient has also the real part equal to zero, as it is to be expected, the coefficient \(c_0\) being the average of the function \(s(t)\);
  3. all the coefficients have the real part identically equal to zero;
  4. both the even and odd coefficients have imaginary parts different from zero.


We can now use the function spectrum from the fourier.py module, to calculate the spectrum of the function and to compare with a spectrum \(n^{-2}\) again:

In [49]:
# Compute the numerical spectrum
sps=f.spectrum(cfs,dt)
# Plot the spectrum along with n^-2 function
plt.title(r"Spectrum of the sawtooth function $s(t)$")
plt.xlabel(r"$n$")
plt.ylabel(r"$E(n)$")
plt.xscale("log")
plt.yscale("log")
plt.xlim(1.0,N/2+1.0)
plt.ylim(1.e-10,100.0)
# Notice that we multiply n^-2 for a factor 10, to 
plt.plot(nn,sps,'b-',10.0*nn**(-2.0),'ro')
plt.show()
We may now reconstruct, as usual, the function by using the first \(Nmax\) harmonics. However, in order to use the irfft function, we choose a different approach, namely:
  1. we copy the original vector containing the Fourier's coefficients into a new vector;
  2. we define Nmax and we put all the harmonics greter than Nmax equal to zero in this new vector;
  3. we transform back the coefficients to get the function \(s(t,N_\textrm{max})\) reconstructed with the first Nmax harmonics.


Here is the function reconstruct:

In [67]:
def reconstruct( ak, n ):
    Nyq = ak.size - 1           # Nyquist's number
    Ns = 2 * Nyq                # Number of points in the output vector
    sr = np.zeros( Ns, dtype=np.float64 )   # Initialize the output vector with zeros
    if ( n > Nyq ):
        print("Error! The number of harmonics for the reconstruction must be lesser than the Nyquist number!")
        return sr
    else:
        akk = np.zeros(Nyq+1, dtype=np.complex128)  # Define a new vector for the Fourier's coefficients
        akk[:n+1] = Ns * ak[:n+1]                   # Copy the Fourier's coefficients up to n, into the new vector
                                                    # Note the product for Ns to re-normalize the coefficients
        sr = np.fft.irfft(akk)                      # Inverse Fourier transform of the reconstructed function
    return sr
In [69]:
Nmax=5
plt.title("Sawtooth function and reconstructed function for Nmax = " + str(Nmax))
plt.xlabel(r"$t$")
plt.ylabel(r"$s(t), s(t,N_{max})$")
plt.xlim(0.0,2.0*np.pi)
plt.ylim(-np.pi,+np.pi)
srec=reconstruct(cfs,Nmax)
plt.plot(t,s,'b-',t,srec,'ro')
plt.show()
In [70]:
Nmax=11
plt.title("Sawtooth function and reconstructed function for Nmax = " + str(Nmax))
plt.xlabel(r"$t$")
plt.ylabel(r"$s(t), s(t,N_{max})$")
plt.xlim(0.0,2.0*np.pi)
plt.ylim(-np.pi,+np.pi)
srec=reconstruct(cfs,Nmax)
plt.plot(t,s,'b-',t,srec,'ro')
plt.show()
In [71]:
Nmax=32
plt.title("Sawtooth function and reconstructed function for Nmax = " + str(Nmax))
plt.xlabel(r"$t$")
plt.ylabel(r"$s(t), s(t,N_{max})$")
plt.xlim(0.0,2.0*np.pi)
plt.ylim(-np.pi,+np.pi)
srec=reconstruct(cfs,Nmax)
plt.plot(t,s,'b-',t,srec,'ro')
plt.show()
In [72]:
Nmax=64
plt.title("Sawtooth function and reconstructed function for Nmax = " + str(Nmax))
plt.xlabel(r"$t$")
plt.ylabel(r"$s(t), s(t,N_{max})$")
plt.xlim(0.0,2.0*np.pi)
plt.ylim(-np.pi,+np.pi)
srec=reconstruct(cfs,Nmax)
plt.plot(t,s,'b-',t,srec,'ro')
plt.show()