Skip to content

Instantly share code, notes, and snippets.

@jakelevi1996
Last active October 31, 2024 11:51
Show Gist options
  • Select an option

  • Save jakelevi1996/52e7ba42332574eda41030b6ad249b4c to your computer and use it in GitHub Desktop.

Select an option

Save jakelevi1996/52e7ba42332574eda41030b6ad249b4c to your computer and use it in GitHub Desktop.
Comparing DFT vs FFT

Comparing DFT vs FFT

Below is a Python script which contains functions for computing the naive Discrete Fourier Transform (DFT), a recursive radix-2 Fast Fourier Transform (FFT) algorithm, and an in-place FFT radix-2 algorithm.

TODO: add timing information for FFT in-place; add timeplot output; compare outputs with FFT in-place; reorganise structure of Gist

import numpy as np
import matplotlib.pyplot as plt
from time import perf_counter

def dft(x):
    """ Compute DFT naively using matrix multiplication """
    N = x.size
    inds = np.arange(N)
    coeffs = np.exp(-2j * np.pi * np.outer(inds, inds) / N)
    return np.matmul(coeffs, x)

def fft(x):
    """
    Wrapper for recursive radix-2 FFT computation, which computes all of the
    odd-term rotations, then calls the recursive sub-function
    """
    N = x.size
    if np.floor(np.log2(N)) != np.log2(N):
        raise ValueError("Array size must be a power of 2")
    M = np.arange(N / 2)                        # Indices
    rotations = np.exp(-2j * np.pi * M / N)     # Odd-term rotations
    return fft_recurse(x, rotations)

def fft_recurse(x, rotations):
    """
    Sub-function for recursive radix-2 FFT computation, which appropriately
    combines FFTs of even terms and odd terms using rotations
    """
    N = x.size
    if N == 1: return x
    if N == 2: return np.array([x[0] + x[1], x[0] - x[1]])
    E = fft_recurse(x[0::2], rotations[0::2])   # Even terms
    O = fft_recurse(x[1::2], rotations[0::2])   # Odd terms
    O_rot = O * rotations                       # Odd terms rotated
    return np.concatenate([E + O_rot, E - O_rot])

def fft_inplace(x):
    """
    Implementation of radix-2 in-place (without recursion) on the input vector
    x. The length of x must be an integer power of 2.
    """
    # Initialise number of samples (N) and initial number of blocks (N / 2)
    N = x.size
    num_blocks = N >> 1
    # Do initial 2-point FFT on all samples and convert the answer to complex
    for i in range(num_blocks):
        x[[i, i + num_blocks]] = (
            x[i] + x[i + num_blocks],
            x[i] - x[i + num_blocks]
        )
    x = x.astype(complex)
    # Initialise odd-term rotations
    M = np.arange(num_blocks)
    rotations = np.exp((-2j * np.pi / N) * M)
    # Update number of blocks and previous block size for next iteration
    num_blocks >>= 1
    prev_block_size = 2
    # Iterate through each stage (each stage has a different number of blocks)
    while prev_block_size < N:
        # Get the rotations for each block of this stage
        block_rotations = rotations[::num_blocks]
        # Iterate through each block of this stage
        for block_start_ind in range(num_blocks):
            # Get the indices and even and rotated odd terms for this block
            block_inds = np.arange(block_start_ind, N, num_blocks)
            even_terms = x[block_inds[0::2]]
            odd_terms  = x[block_inds[1::2]]
            odd_terms_rotated = odd_terms * block_rotations
            # Calculate FFT of this block using blocks from previous stage
            x[block_inds[:prev_block_size]] = even_terms + odd_terms_rotated
            x[block_inds[prev_block_size:]] = even_terms - odd_terms_rotated

        # Next iteration there are half as many blocks, each double the size
        num_blocks >>= 1
        prev_block_size <<= 1
    
    return x

def ifft(X):
    """ Calculate inverse FFT """
    return np.conj(fft(np.conj(X))) / X.size

def plot_sig_and_response(t, x, f, X, name="Signal and response"):
    fig, axes = plt.subplots(2, 1)
    fig.set_size_inches([8, 6])
    axes[0].plot(t, x, "b")
    axes[0].grid(True)
    axes[0].set(xlabel="Time (s)", title="Time domain")
    axes[1].plot(f, np.abs(X), "b")
    axes[1].grid(True)
    axes[1].set(xlabel="Frequency (Hz)", title="Frequency domain")
    fig.suptitle(name, fontsize=20)
    fig.tight_layout(rect=[0, 0, 1, 0.95])
    plt.savefig(name)
    plt.close()

if __name__ == "__main__":
    # Sampling frequency
    f_s = 1e3
    
    # Make plots for N = 256
    N = 256
    t = np.linspace(0, N/f_s, N, endpoint=False)
    x = np.sin(2*np.pi*100*t) + np.sin(2*np.pi*200*t)
    X_dft =  dft(x)
    X_fft =  fft(x)
    f = np.linspace(0, f_s, N, endpoint=False)
    plot_sig_and_response(t, x, f, X_dft, name="DFT results")
    plot_sig_and_response(t, x, f, X_fft, name="FFT results")
    
    # Check error of inverse FFT
    x_ifft = ifft(X_fft)
    print("Inverse FFT error = {:.4e}".format(np.max(np.abs(x - x_ifft))))
    
    # Print table headers for speed tests
    table_headers = ["Input size", "Time taken for DFT (s)",
        "Time taken for FFT (s)", "Factor difference", "Maximum absolute error"]
    print(" | ".join(table_headers))
    print(" | ".join(["---"] * len(table_headers)))
    
    # Run speed tests (TODO: make log-log graph of speed results with repeats)
    for N_pow in range(8, 14):
        N = 2 ** N_pow
        t = np.linspace(0, N/f_s, N, endpoint=False)
        x = np.sin(2*np.pi*100*t) + np.sin(2*np.pi*200*t)
        t0 = perf_counter()
        X_dft =  dft(x)
        t1 = perf_counter()
        X_fft = fft(x)
        t2 = perf_counter()
        print(" | ".join(["{}".format(N), "{:.4g}".format(t1 - t0),
            "{:.4g}".format(t2 - t1), "{:.1f}".format((t1 - t0) / (t2 - t1)),
            "{:.4e}".format(np.max(np.abs(X_dft - X_fft)))]))

Inverse FFT error = 9.4717e-16

Below are time and accuracy results for different sized inputs:

Input size Time taken for DFT (s) Time taken for FFT (s) Factor difference Maximum absolute error
256 0.004445 0.00093 4.8 3.6437e-12
512 0.01783 0.001716 10.4 1.4453e-11
1024 0.06661 0.003368 19.8 5.6465e-11
2048 0.2659 0.00662 40.2 2.1411e-10
4096 1.056 0.01365 77.3 7.7747e-10
8192 4.172 0.02662 156.7 3.4760e-09

The following code can be used to print out the indices for each block in each stage of the in-place FFT, which is useful for gaining intuition of how the in-place FFT has been coded:

import numpy as np

def fft_inplace_inds(num_stages):
    # Initialise number of samples and initial block size
    N = 1 << num_stages
    block_size = N
    # Initialise x
    x = np.empty([N, num_stages], dtype=int)
    x[:, 0] = np.arange(N)

    # Iterate through each column (1 column per stage)
    for col in range(1, num_stages):
        prev_block_size = block_size
        block_size >>= 1
        # Iterate through each block in the column
        for prev_block_start_i in range(0, N, prev_block_size):
            # Calculate even indices
            x[
                prev_block_start_i:(
                    prev_block_start_i + block_size
                ),
                col
            ] = x[
                prev_block_start_i:(
                    prev_block_start_i + prev_block_size
                ):2,
                col - 1
            ]
            # Calculate odd indices
            x[
                (prev_block_start_i + block_size):(
                    prev_block_start_i + prev_block_size
                ),
                col
            ] = x[
                (prev_block_start_i + 1):(
                    prev_block_start_i + prev_block_size
                ):2,
                col - 1
            ]
    return x

if __name__ == "__main__":
    for num_stages in range(1, 6):
        print(
            1 << num_stages,
            fft_inplace_inds(num_stages),
            sep=":\n",
            end="\n\n"
        )

Console outout:

2:
[[0]
 [1]]

4:
[[0 0]
 [1 2]
 [2 1]
 [3 3]]

8:
[[0 0 0]
 [1 2 4]
 [2 4 2]
 [3 6 6]
 [4 1 1]
 [5 3 5]
 [6 5 3]
 [7 7 7]]

16:
[[ 0  0  0  0]
 [ 1  2  4  8]
 [ 2  4  8  4]
 [ 3  6 12 12]
 [ 4  8  2  2]
 [ 5 10  6 10]
 [ 6 12 10  6]
 [ 7 14 14 14]
 [ 8  1  1  1]
 [ 9  3  5  9]
 [10  5  9  5]
 [11  7 13 13]
 [12  9  3  3]
 [13 11  7 11]
 [14 13 11  7]
 [15 15 15 15]]

32:
[[ 0  0  0  0  0]
 [ 1  2  4  8 16]
 [ 2  4  8 16  8]
 [ 3  6 12 24 24]
 [ 4  8 16  4  4]
 [ 5 10 20 12 20]
 [ 6 12 24 20 12]
 [ 7 14 28 28 28]
 [ 8 16  2  2  2]
 [ 9 18  6 10 18]
 [10 20 10 18 10]
 [11 22 14 26 26]
 [12 24 18  6  6]
 [13 26 22 14 22]
 [14 28 26 22 14]
 [15 30 30 30 30]
 [16  1  1  1  1]
 [17  3  5  9 17]
 [18  5  9 17  9]
 [19  7 13 25 25]
 [20  9 17  5  5]
 [21 11 21 13 21]
 [22 13 25 21 13]
 [23 15 29 29 29]
 [24 17  3  3  3]
 [25 19  7 11 19]
 [26 21 11 19 11]
 [27 23 15 27 27]
 [28 25 19  7  7]
 [29 27 23 15 23]
 [30 29 27 23 15]
 [31 31 31 31 31]]

Below are plots of the results for an input size of 256:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment