Fourier Transforms
Why the FFT Changed Everything
The Fast Fourier Transform (FFT) is arguably the most important numerical algorithm of the 20th century. It converts a signal from the time domain to the frequency domain in operations, enabling everything from audio compression (MP3) to medical imaging (MRI) to wireless communications (OFDM). Without the FFT, a 1024-point DFT would require complex multiplications; with the FFT, it takes .
Definition: The Discrete Fourier Transform (DFT)
The Discrete Fourier Transform (DFT)
For a signal of length , the DFT is defined as:
The inverse DFT (IDFT) recovers the original signal:
In NumPy:
X = np.fft.fft(x) # forward DFT
x = np.fft.ifft(X) # inverse DFT
The DFT assumes the signal is periodic with period . This implicit periodicity causes spectral leakage when the signal does not contain an integer number of cycles in the analysis window.
Definition: DFT as a Matrix-Vector Product
DFT as a Matrix-Vector Product
The DFT can be written as , where the DFT matrix has entries:
with the -th root of unity. The matrix is unitary: .
N = len(x)
n = np.arange(N)
k = n.reshape(-1, 1)
F = np.exp(-2j * np.pi * k * n / N)
X_matrix = F @ x # same as np.fft.fft(x)
The FFT algorithm exploits the symmetry and periodicity of to reduce the matrix-vector product to operations via divide-and-conquer.
Definition: Frequency Axis with fftfreq
Frequency Axis with fftfreq
The function np.fft.fftfreq(N, d) returns the frequency bins
corresponding to each DFT index, where is the sample spacing:
For , the frequencies wrap to negative values.
Use np.fft.fftshift to rearrange the output so that zero frequency
is at the center:
freqs = np.fft.fftfreq(N, d=1/fs)
X_shifted = np.fft.fftshift(X)
f_shifted = np.fft.fftshift(freqs)
Definition: FFT Normalization Conventions
FFT Normalization Conventions
NumPy supports three normalization modes via the norm parameter:
| Mode | Forward () | Inverse () |
|---|---|---|
None (default) |
No scaling | Divide by |
"ortho" |
Divide by | Divide by |
"forward" |
Divide by | No scaling |
X = np.fft.fft(x, norm="ortho") # unitary DFT
The "ortho" convention makes the DFT a unitary transform, preserving
energy: (Parseval's theorem).
Definition: 2D and N-Dimensional DFT
2D and N-Dimensional DFT
The 2D DFT of an image of size is:
Since the 2D DFT is separable, it can be computed as row-wise 1D FFTs followed by column-wise 1D FFTs:
X2d = np.fft.fft2(x_2d) # 2D FFT
x_2d = np.fft.ifft2(X2d) # 2D inverse FFT
Xnd = np.fft.fftn(x_nd) # N-dimensional FFT
Definition: Zero-Padding for Frequency Interpolation
Zero-Padding for Frequency Interpolation
Appending zeros to a signal before computing the DFT increases the number of frequency bins, providing a smoother spectral representation. Zero-padding to length interpolates the spectrum but does not improve spectral resolution (which is determined by the observation duration ):
X_padded = np.fft.fft(x, n=4*N) # 4x zero-padding
The frequency resolution remains , but zero-padding reveals the shape of the underlying spectral peaks more clearly.
Zero-padding is equivalent to sinc interpolation of the spectrum. It is essential for applications like radar range profiling where you need to locate spectral peaks between DFT bins.
Theorem: Parseval's Theorem
For a signal and its DFT , the total energy is preserved:
With the "ortho" normalization: .
The DFT is a rotation in -dimensional space. Rotations preserve lengths (norms), so the total energy measured in the time domain equals the total energy measured in the frequency domain.
Expand the frequency-domain sum
$
Interchange summation order
$
Apply orthogonality
N$ gives the result.
Theorem: Convolution Theorem
Circular convolution in the time domain corresponds to pointwise multiplication in the frequency domain:
where denotes circular (periodic) convolution. For linear convolution, zero-pad both sequences to length before applying the DFT.
The DFT diagonalizes circulant matrices. Convolution by is multiplication by a circulant matrix built from , so in the DFT basis it becomes a diagonal (pointwise) operation.
Theorem: Nyquist-Shannon Sampling Theorem
A bandlimited signal with maximum frequency f_\max can be perfectly reconstructed from its samples if and only if:
f_s > 2 f_\max
The minimum sampling rate f_s = 2 f_\max is called the Nyquist rate. Sampling below this rate causes aliasing β high-frequency components fold back into the baseband and cannot be separated.
Each sample provides one equation. A bandlimited signal has 2 f_\max T degrees of freedom in time , so you need at least that many samples per second to capture all the information.
Example: FFT of a Multi-Tone Signal
Create a signal sampled at for 0.5 seconds. Compute its FFT and plot the magnitude spectrum.
Generate the signal and compute the FFT
import numpy as np
import matplotlib.pyplot as plt
fs = 1000
T = 0.5
t = np.arange(0, T, 1/fs)
x = np.sin(2*np.pi*50*t) + 0.5*np.sin(2*np.pi*120*t)
X = np.fft.fft(x)
freqs = np.fft.fftfreq(len(x), d=1/fs)
# Plot only positive frequencies
mask = freqs >= 0
plt.stem(freqs[mask], 2*np.abs(X[mask])/len(x))
plt.xlabel("Frequency (Hz)")
plt.ylabel("Amplitude")
The magnitude spectrum shows peaks at 50 Hz (amplitude 1.0) and 120 Hz (amplitude 0.5), matching the original signal components.
Example: Verifying the DFT Matrix
For , construct the DFT matrix explicitly and
verify that matches np.fft.fft(x).
Also verify that is unitary.
Build the DFT matrix
N = 8
n = np.arange(N)
k = n.reshape(-1, 1)
F = np.exp(-2j * np.pi * k * n / N)
x = np.random.randn(N)
X_matrix = F @ x
X_fft = np.fft.fft(x)
print(f"Match: {np.allclose(X_matrix, X_fft)}") # True
Check unitarity
F_normed = F / np.sqrt(N)
print(np.allclose(F_normed.conj().T @ F_normed, np.eye(N))) # True
The normalized DFT matrix is unitary, confirming that the DFT is an orthonormal change of basis.
Example: Zero-Padding Reveals Spectral Shape
A signal contains two closely-spaced sinusoids at 100 Hz and 105 Hz, sampled at Hz for 0.1 seconds (100 samples). Show how zero-padding to 1024 points reveals the peak structure without improving the true resolution of Hz.
Compare FFT with and without zero-padding
fs = 1000
t = np.arange(0, 0.1, 1/fs)
x = np.sin(2*np.pi*100*t) + np.sin(2*np.pi*105*t)
X_100 = np.fft.fft(x) # 100 points
X_1024 = np.fft.fft(x, n=1024) # zero-padded to 1024
f_100 = np.fft.fftfreq(100, 1/fs)
f_1024 = np.fft.fftfreq(1024, 1/fs)
With 100 points ( Hz), the two tones merge into one peak. With 1024 points, the spectral envelope is smoother but still shows a single broad peak. True resolution requires a longer observation window, not more zero-padding.
FFT Explorer: Frequency and Time Domain
Adjust signal parameters and observe how the FFT magnitude spectrum changes. Try adding multiple frequency components and varying the number of samples to see spectral leakage.
Parameters
DFT Matrix Visualizer
Visualize the real and imaginary parts of the DFT matrix for different sizes . Notice the harmonic structure of the rows and the symmetry.
Parameters
Aliasing When Sampling Below Nyquist
Watch what happens as the sampling rate drops below the Nyquist rate. The animation shows how an undersampled sinusoid appears as a lower-frequency alias.
Parameters
FFT Butterfly Diagram (N = 8)
Fourier Transform Recipes
# Code from: ch07/python/fourier_transforms.py
# Load from backend supplements endpointQuick Check
What is the computational complexity of the radix-2 FFT algorithm for an -point DFT?
The FFT recursively splits the DFT into two -point DFTs at each of levels.
Quick Check
Zero-padding a signal from to points before computing the FFT:
Improves frequency resolution by a factor of 4
Interpolates the spectrum for a smoother visualization
Reduces spectral leakage
Doubles the signal bandwidth
Zero-padding is equivalent to sinc interpolation of the underlying DTFT.
Common Mistake: Forgetting fftshift for Centered Spectra
Mistake:
Plotting the raw FFT output directly: the negative frequencies appear at the right side of the array, not centered at zero.
Correction:
Use np.fft.fftshift on both the FFT output and the frequency axis:
X_centered = np.fft.fftshift(np.fft.fft(x))
f_centered = np.fft.fftshift(np.fft.fftfreq(N, 1/fs))
Common Mistake: Normalization Mismatch Between Forward and Inverse
Mistake:
Computing the forward FFT with norm="ortho" and then the inverse
FFT with the default norm=None, producing a result scaled by
.
Correction:
Always use the same norm argument for both fft and ifft:
X = np.fft.fft(x, norm="ortho")
x_back = np.fft.ifft(X, norm="ortho") # matches!
Key Takeaway
The FFT computes the DFT in operations instead of .
Use np.fft.fftfreq for the frequency axis and np.fft.fftshift for
centered spectra. Zero-padding interpolates the spectrum but does not
improve resolution. Always match the norm argument between forward
and inverse transforms.
Key Takeaway
The DFT is a matrix-vector product . Understanding this linear algebra perspective is essential for grasping OFDM, where subcarriers are the rows of the DFT matrix, and the inverse FFT is the modulator.
Why This Matters: OFDM: The FFT as a Modulation Scheme
Orthogonal Frequency-Division Multiplexing (OFDM) β used in WiFi, 4G LTE, and 5G NR β transmits data on orthogonal subcarriers. The transmitter applies an inverse FFT to map frequency-domain symbols to a time-domain waveform, and the receiver applies a forward FFT to recover them. The entire OFDM transceiver is essentially two FFT operations sandwiching a wireless channel.
The circular convolution property of the DFT is what makes OFDM work: adding a cyclic prefix converts the linear channel convolution into circular convolution, enabling simple per-subcarrier equalization via division in the frequency domain.
See full treatment in Chapter 15
Historical Note: Cooley and Tukey (1965): Rediscovering the FFT
1965James Cooley and John Tukey published their FFT algorithm in 1965, reducing the DFT from to . The algorithm was actually discovered earlier by Gauss around 1805 for interpolating asteroid orbits, but his work went unnoticed. Cooley-Tukey's paper ignited a revolution in digital signal processing, making real-time spectral analysis computationally feasible for the first time.
Historical Note: Joseph Fourier and the Theory of Heat
1807Joseph Fourier introduced his transform in 1807 while studying heat conduction. His radical claim that any periodic function could be decomposed into a sum of sines and cosines was met with skepticism by Lagrange and Laplace. Two centuries later, the Fourier transform is the cornerstone of signal processing, physics, and engineering.
DFT (Discrete Fourier Transform)
A transform that converts a finite sequence of samples into complex coefficients representing the signal's frequency content.
Related: FFT Normalization Conventions, IDFT
FFT (Fast Fourier Transform)
An efficient algorithm for computing the DFT in operations instead of the naive .
Aliasing
The phenomenon where high-frequency signal components are misrepresented as lower frequencies when the sampling rate is below the Nyquist rate (f_s < 2 f_\max).
Related: Nyquist rate, anti-aliasing filter
Spectral Leakage
The spreading of a signal's energy across multiple frequency bins when the observation window does not contain an integer number of signal cycles, caused by the implicit rectangular windowing of the DFT.
Related: window function, The Discrete Fourier Transform (DFT)
Zero-Padding
Appending zeros to a signal before computing the DFT to increase the number of frequency bins (spectral interpolation) without improving the underlying frequency resolution.
Related: The Discrete Fourier Transform (DFT), frequency resolution
FFT Normalization Conventions
| Convention | Forward Scale | Inverse Scale | Parseval | Use Case |
|---|---|---|---|---|
| Default (None) | 1 | 1/N | Standard DSP, most textbooks | |
| Ortho | Unitary DFT, energy preservation | |||
| Forward | 1/N | 1 | Physics convention |