Step-by-Step Guide: Implementing an FFT Algorithm in Python The Fast Fourier Transform (FFT) is one of the most fundamental algorithms in modern computing. It transitions a signal from its sequential time domain to its constituent frequency domain, revealing hidden patterns in everything from audio tracks to seismic activity.
While production environments rely heavily on heavily optimized libraries like SciPy’s fft module or NumPy, building an FFT from scratch provides invaluable insight into how the algorithm achieves its blistering speed. This guide walks through implementing the classic Cooley-Tukey Radix-2 FFT algorithm entirely from scratch using Python. 1. The Core Problem: Navigating the Bottleneck
A standard Discrete Fourier Transform (DFT) calculates individual frequency components by multiplying the input data vector against a transformation matrix. Mathematically, it evaluates the formula:
Xk=∑n=0N−1xn⋅e−i2πNkncap X sub k equals sum from n equals 0 to cap N minus 1 of x sub n center dot e raised to the negative i the fraction with numerator 2 pi and denominator cap N end-fraction k n power
If we directly translate this formula into Python code using matrix multiplication, it scales terribly:
import numpy as np def dft_slow(x): “”“Computes the Discrete Fourier Transform natively.”“” x = np.asarray(x, dtype=float) N = x.shape[0] n = np.arange(N) k = n.reshape((N, 1)) # Calculate twiddle factors matrix M = np.exp(-2jnp.pi * k * n / N) return np.dot(M, x) Use code with caution. Why a Slow DFT Fails Time Complexity: It demands O(N²) operations.
Scale Bottleneck: Processing a 1-second audio clip sampled at 44.1 kHz requires nearly 2 billion operations. 2. The Solution: Divide and Conquer
The Cooley-Tukey Radix-2 algorithm solves this scaling dilemma by exploiting mathematical symmetries. If the input size N is a power of 2, the formula can be cleanly split into its even-indexed components ( x2mx sub 2 m end-sub ) and odd-indexed components ( x2m+1x sub 2 m plus 1 end-sub
Xk=∑m=0N/2−1x2m⋅e−i2πN/2km+e−i2πNk∑m=0N/2−1x2m+1⋅e−i2πN/2kmcap X sub k equals sum from m equals 0 to cap N / 2 minus 1 of x sub 2 m end-sub center dot e raised to the negative i the fraction with numerator 2 pi and denominator cap N / 2 end-fraction k m power plus e raised to the negative i the fraction with numerator 2 pi and denominator cap N end-fraction k power sum from m equals 0 to cap N / 2 minus 1 of x sub 2 m plus 1 end-sub center dot e raised to the negative i the fraction with numerator 2 pi and denominator cap N / 2 end-fraction k m power
This structural breakdown splits a single O(N²) problem into two smaller
sub-problems. By recursively repeating this division down to a single element, the total runtime complexity collapses down to an incredibly efficient framework. 3. Writing the Recursive FFT from Scratch
Let’s assemble a standalone, recursive Python function that implements this mathematical strategy directly.
import numpy as np def fft_recursive(x): “”“A recursive implementation of the 1D Cooley-Tukey Radix-2 FFT.”“” x = np.asarray(x, dtype=complex) N = x.shape[0] # Base Case: Vector of length 1 returns itself if N <= 1: return x # Enforce Radix-2 structural limitation if N % 2 != 0: raise ValueError(“The size of the input array must be a power of 2.”) # Step 1: Subdivide vector into even and odd indices x_even = fft_recursive(x[0::2]) x_odd = fft_recursive(x[1::2]) # Step 2: Compute exponential twiddle factors factor = np.exp(-2j * np.pi * np.arange(N) / N) # Step 3: Combine divided solutions using symmetry properties half_N = N // 2 combined_result = np.zeros(N, dtype=complex) # First half calculations: X_k = Even_k + twiddle * Odd_k combined_result[:half_N] = x_even + factor[:half_N] * xodd # Second half calculations exploiting periodicity: X{k + N/2} = Even_k - twiddle * Odd_k combined_result[half_N:] = x_even + factor[half_N:] * x_odd return combined_result Use code with caution. 4. Validating and Graphing the Performance
To verify our pure Python implementation, we will compare its numerical output with NumPy’s highly tailored internal C implementation (np.fft.fft), and then generate a visual verification plot using Matplotlib. Verification Script
# Create a sample synthetic wave array test_signal = np.random.random(1024) # Run verification test against standard library is_accurate = np.allclose(fft_recursive(test_signal), np.fft.fft(test_signal)) print(f”Is our scratching implementation accurate? {is_accurate}“) Use code with caution. Decomposing and Visualizing Mixed Signals
Let’s construct a noisy signal composed of two prominent sine waves (50 Hz and 120 Hz) to watch the frequency separation engine in action.
import matplotlib.pyplot as plt # Generate a synthetic timeline sampling_rate = 1000 # Hz time_step = 1.0 / sampling_rate time_axis = np.arange(0, 1, time_step) # Generate a compound wave sequence (50Hz wave + 120Hz wave) clean_signal = np.sin(2 * np.pi * 50 * time_axis) + 0.5 * np.sin(2 * np.pi * 120 * time_axis) noisy_signal = clean_signal + 2.0 * np.random.normal(size=time_axis.shape) # Run our custom FFT algorithm fft_output = fft_recursive(noisy_signal) frequencies = np.arange(len(fft_output)) * (sampling_rate / len(fft_output)) # Filter to display only real-world positive frequencies half_size = len(fft_output) // 2 # Plot the outcome plt.figure(figsize=(12, 5)) plt.plot(frequencies[:half_size], np.abs(fft_output)[:half_size], color=‘purple’) plt.title(“Frequency Spectrum Identified via Custom FFT”) plt.xlabel(“Frequency (Hz)”) plt.ylabel(“Magnitude”) plt.grid(True) plt.show() Use code with caution. 5. Summary of Key Implementation Lessons
Leave a Reply