Implementation of a Radix-2 Fast Fourier Transform

Import standard modules:


In [ ]:
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
from IPython.display import HTML 
HTML('../style/course.css') #apply general CSS

import cmath

This assignment is to implement a python-based Fast Fourier Transform (FFT). Building on $\S$ 2.8 ➞ we will implement a 1-D radix-2 Cooley-Tukey-based FFT using both decimation in time (DIT) and decimation in frequency (DIF) for an $N = 2^n$ input function.

From $\S$ 2.8.2 ➞ the discrete Fourier transform (DFT) is defined as:

$$ \mathscr{F}_{\rm D}\{y\}_k = Y_k = \sum_{n\,=\,0}^{N-1} y_n\,e^{-\imath 2\pi \frac{nk}{N}}, $$

That is, the $k^{th}$ element of the Fourier transformed spectrum $Y$ is a sum over all $n$ elements of the function $y$, each multipled by a complex twiddle factor $e^{-\imath 2\pi \frac{nk}{N}}$. In $\S$ 2.8.5 ➞ two methods for computing the DFT for a size $N = 2^n$ discrete function. A double loop to compute all elements of the Fourier-transformed spectrum, and a matrix multiplication by generating the Fourier kernel $K$. The compute time to perform the DFT is $\mathcal{O}(N^2)$, this is it takes $cN^2$ operations where $c > 1$ is a constant factor. Though as note in $\S$ 2.8.5 ➞ the matrix implementation is much fast that the loop because this algorithm takes advantage of fast vector math libraries.

The DFT code is replicated here as it will be used to compare our implementation of the FFT:


In [ ]:
def loop_DFT(x):
    """
    Implementing the DFT in a double loop
    Input: x = the vector we want to find the DFT of
    """
    #Get the length of the vector (will only work for 1D arrays)
    N = x.size
    #Create vector to store result in
    X = np.zeros(N, dtype=complex)
    for k in range(N):
        for n in range(N):
            X[k] += np.exp(-1j * 2.0* np.pi* k * n / N) * x[n]
    return X

def matrix_DFT(x):
    """
    Implementing the DFT in vectorised form
    Input: x = the vector we want to find the DFT of
    """
    #Get the length of the vector (will only work for 1D arrays)
    N = x.size
    #Create vector to store result in
    n = np.arange(N)
    k = n.reshape((N,1))
    K = np.exp(-1j * 2.0 * np.pi * k * n / N)
    return K.dot(x)

In $\S$ 2.8.6 ➞ the fast Fourier transform was introduced as using recursion to implement a Fourier transform in $\mathcal{O}(N\log_2N)$ computations, significantly reducing the computational cost of computing the Fourier transform, especially for large $N$. A 'one layer' fast Fourier transform was presented which split the input function into two, and applied the twiddle factor to all values in the layer before calling the matrix-based DFT. This code is replicated below.


In [ ]:
def one_layer_FFT(x):
    """An implementation of the 1D Cooley-Tukey FFT using one layer"""
    N = x.size
    if N%2 > 0:
        print "Warning: length of x is not a power of two, returning DFT"
        return matrix_DFT(x)
    else:
        X_even = matrix_DFT(x[::2])
        X_odd = matrix_DFT(x[1::2])
        factor = np.exp(-2j * np.pi * np.arange(N) / N)
        return np.concatenate([X_even + factor[:N / 2] * X_odd, X_even + factor[N / 2:] * X_odd])

We can easily show that each of these functions produce the same results by introducting a discrete test function $x$ and showing that the same results are reported by each function call:


In [ ]:
x = np.random.random(256)  # create random vector to take the DFT of

print np.allclose(loop_DFT(x), matrix_DFT(x)) # returns True if all values are equal (within numerical error)
print np.allclose(matrix_DFT(x), one_layer_FFT(x)) # returns True if all values are equal (within numerical error)

We can also time each function to report of the amount of time is takes to return a finished spectrum.


In [ ]:
print 'Double Loop DFT:'
%timeit loop_DFT(x)
print '\nMatrix DFT:'
%timeit matrix_DFT(x)
print '\nOne Layer FFT + Matrix DFT:'
%timeit one_layer_FFT(x)

As we can see the matrix DFT is significatly faster than the double loop DFT, this is because of the fast vectorization functions in numpy. And, the 'one-layer' FFT is about twice as fast as the matrix DFT because of the FFT architecture. We can go one fast and use the built-in numpy FFT:


In [ ]:
print np.allclose(one_layer_FFT(x), np.fft.fft(x))

print 'numpy FFT:'
%timeit np.fft.fft(x)

The numpy FFT is very fast, in part because of the low-level programing implementation, but fundamentally because it uses an FFT architecture. Our goal for this assignment is to implement such an architecture.

Decimation-in-Time (DIT) FFT

Decimation-in-Frequency (DIF) FFT


In [ ]:

Extras


In [ ]:

  • butterfly figures
  • dit equation
  • implement dit
  • dif equation
  • implement dif
  • compare results with test cases: correct answer and timing, plot spectra
  • extra: non 2^n, in place FFT