TAI BUI
Back to curriculum
BuildPython

The Fourier Transform

Every signal is a sum of sine waves. The Fourier transform tells you which ones, and how strong each one is.

TIME: ~90 minutesPREREQUISITES: Phase 1, Lessons 01-04, 19 (complex numbers)

Learning Objectives

  • Implement the DFT from scratch and verify it against the O(N log N) Cooley-Tukey FFT
  • Interpret frequency coefficients: extract amplitude, phase, and power spectrum from a signal
  • Apply the convolution theorem to perform convolution via FFT multiplication
  • Connect Fourier frequency decomposition to transformer positional encodings and CNN convolution layers

The Problem

An audio recording is a sequence of pressure measurements over time. A stock price is a sequence of values over days. An image is a grid of pixel intensities over space. All three are data in the time domain (or space domain): you see values changing over some index, and that's it.

Plenty of real patterns don't show up there. Is that audio signal a pure tone or a chord? Does the stock have a weekly cycle? Does the image have a repeating texture? Those are all questions about frequency content, and the time domain simply doesn't show frequency.

The Fourier transform is the tool that moves data from the time domain into the frequency domain. It decomposes a signal into sine waves of different frequencies, and for each one it hands you an amplitude (how strong it is) and a phase (where it starts).

Frequency-domain thinking shows up constantly in ML. A convolutional layer's convolution operation is multiplication in the frequency domain. Transformer positional encodings represent position through frequency decomposition. Audio models, speech recognition, music generation, run on spectrograms: frequency representations of sound, not waveforms. Time series models hunt for periodic patterns the same way. Learn the Fourier transform once and you have the vocabulary for all of it.

Warm-up0/3
1. A song playing on speakers and a week of daily stock-closing prices are both just a sequence of numbers changing over an index (time, or day). Looking only at that raw sequence, can you tell at a glance whether there's a repeating weekly pattern, or how many separate musical notes are layered together?Warm-up
Two generic raw sequences, a song's pressure over time and a week of stock closing prices, both drawn as plain wiggly lines with no structure called out, asking whether a repeating pattern or layered notes are visible at a glance.
Illustrative sequences, generated with numpy, seed 3.
2. Comparing every item in a list to every other item, one pair at a time, is one way to search for a match. If you double the length of the list, would you expect the number of pairwise comparisons to roughly double as well, or grow by more than that?Warm-up
Four items arranged in a circle with a line drawn between every pair, labelled 6 comparisons, next to eight items arranged the same way with no lines drawn and a question mark over the comparison count, asking whether doubling the list roughly doubles the comparisons.
3. A complex number is written a + bi. What does its magnitude measure?Warm-up
A point z equals 3 plus 4i plotted on the complex plane, with dashed legs down to the real axis labelled a equals 3 and across to the imaginary axis labelled b equals 4, and the vector from the origin drawn without its length given, asking what the magnitude measures.
Check: The Problem0/1
1. What does the Fourier transform do to a signal?Define
A raw time-domain waveform feeding into a box labelled Fourier Transform, with the output left as a single question mark, asking what the transform does to the signal.

The Concept

The DFT definition

Given N samples x[0], x[1], ..., x[N-1], the Discrete Fourier Transform produces N frequency coefficients X[0], X[1], ..., X[N-1]:

X[k] = sum_{n=0}^{N-1} x[n] * e^(-2*pi*i*k*n/N)

for k = 0, 1, ..., N-1

Each X[k] is a complex number: its magnitude |X[k]| gives the amplitude of frequency k, and its phase angle(X[k]) gives that frequency's phase offset.

e^(-2*pi*i*k*n/N) is a rotating phasor at frequency k, and the DFT is really running a correlation: it checks the signal against each of N equally-spaced frequencies in turn. Energy at frequency k makes that correlation large. No energy there and it sits near zero.

What each coefficient means

X[0]: the DC component. The sum of all samples, proportional to the mean. It's the constant, zero-frequency offset of the signal.

X[0] = sum_{n=0}^{N-1} x[n] * e^0 = sum of all samples

X[k] for 1 <= k <= N/2: positive frequencies. X[k] represents frequency k cycles per N samples. Higher k means higher frequency: faster oscillation.

X[N/2]: the Nyquist frequency. The highest frequency you can represent with N samples. Push past it and you get aliasing, high frequencies pretending to be low ones.

X[k] for N/2 < k < N: negative frequencies. For real-valued signals, X[N-k] = conj(X[k]): the negative frequencies just mirror the positive ones. That's why all the useful information sits in the first N/2 + 1 coefficients.

Inverse DFT

The inverse DFT reconstructs the original signal from its frequency coefficients:

x[n] = (1/N) * sum_{k=0}^{N-1} X[k] * e^(2*pi*i*k*n/N)

for n = 0, 1, ..., N-1

Two things change from the forward DFT: the sign in the exponent flips positive, and a 1/N normalization factor gets added.

Run the DFT and then the inverse, and you get the exact original signal back: no error, no information lost. The DFT isn't compressing or destroying anything. It's a change of basis, the same information written in a different coordinate system.

The FFT: making it fast

The DFT as defined above is O(N^2): N output coefficients, each one a sum over N input samples. At N = 1 million, that's 10^12 operations.

The Fast Fourier Transform computes the identical result in O(N log N): about 20 million operations instead of a trillion, at that same N. That gap is the whole reason frequency analysis is practical at scale.

The Cooley-Tukey algorithm, the most common FFT, gets there by divide and conquer:

  1. Split the signal into even-indexed and odd-indexed samples.
  2. Compute the DFT of each half recursively.
  3. Combine the two half-size DFTs using "twiddle factors" e^(-2pii*k/N).
X[k] = E[k] + e^(-2*pi*i*k/N) * O[k]          for k = 0, ..., N/2 - 1
X[k + N/2] = E[k] - e^(-2*pi*i*k/N) * O[k]    for k = 0, ..., N/2 - 1

where E = DFT of even-indexed samples
      O = DFT of odd-indexed samples

Each level of recursion does O(N) work, and there are log2(N) levels, so the total comes out to O(N log N).

Mermaid diagram

The catch: the FFT needs the signal length to be a power of 2. In practice, you zero-pad to the next one.

Spectral analysis

The power spectrum is |X[k]|^2, the squared magnitude of each frequency coefficient. It tells you how much energy sits at each frequency.

The phase spectrum is angle(X[k]), the phase offset of each frequency. Most analysis tasks care only about the power spectrum and throw the phase away.

Power at frequency k:  P[k] = |X[k]|^2 = X[k].real^2 + X[k].imag^2
Phase at frequency k:  phi[k] = atan2(X[k].imag, X[k].real)

Frequency resolution

The DFT's frequency resolution comes from two knobs: the number of samples N and the sampling rate fs.

Frequency of bin k:      f_k = k * fs / N
Frequency resolution:    delta_f = fs / N
Maximum frequency:       f_max = fs / 2  (Nyquist)

Want to tell two close frequencies apart? Take more samples. Want to capture higher frequencies at all? Raise the sampling rate.

The convolution theorem

One of the most important results in signal processing, and directly relevant to CNNs.

Convolution in the time domain equals pointwise multiplication in the frequency domain.

x * h = IFFT(FFT(x) . FFT(h))

where * is convolution and . is element-wise multiplication

Why it matters:

  • Direct convolution of two signals of length N and M costs O(N*M).
  • FFT-based convolution costs O(N log N): transform both, multiply, transform back.
  • For large kernels, that difference is dramatic.
  • This is exactly what a convolutional layer with a large receptive field is doing under the hood.

One catch: the DFT computes circular convolution, where the signal wraps around at the edges. For linear convolution (no wraparound), zero-pad both signals to length N + M - 1 first.

Mermaid diagram

Windowing

Spectral leakage is spurious high-frequency content that shows up because the DFT assumes the signal is periodic: it treats your N samples as one period of an infinitely repeating signal. If the signal doesn't start and end at the same value, that mismatch creates a discontinuity at the boundary, and the discontinuity leaks into frequencies that were never actually in the signal.

Windowing fixes this by tapering the signal to zero at both ends before you run the DFT.

Common windows:

WindowShapeMain lobe widthSide lobe levelUse case
RectangularFlat (no window)NarrowestHighest (-13 dB)When signal is exactly periodic in N samples
HannRaised cosineModerateLow (-31 dB)General purpose spectral analysis
HammingModified cosineModerateLower (-42 dB)Audio processing, speech analysis
BlackmanTriple cosineWideVery low (-58 dB)When side lobe suppression is critical
Hann window:    w[n] = 0.5 * (1 - cos(2*pi*n / (N-1)))
Hamming window: w[n] = 0.54 - 0.46 * cos(2*pi*n / (N-1))

Apply a window by multiplying it element-wise with the signal before the DFT: X = DFT(x * w).

DFT properties

PropertyTime DomainFrequency Domain
Linearityax + byaX + bY
Time shiftx[n - k]X[f] * e^(-2piifk/N)
Frequency shiftx[n] * e^(2piif0n/N)X[f - f0]
Convolutionx * hX * H (pointwise)
Multiplicationx * h (pointwise)X * H (circular convolution, scaled by 1/N)
Parseval's theoremsum |x[n]|^2(1/N) * sum |X[k]|^2
Conjugate symmetry (real input)x[n] realX[k] = conj(X[N-k])

Two rows there are easy to skim past. Shifting a signal in time only rotates each frequency's phase: the magnitude, how much of that frequency exists, never moves. Shifting a signal's frequency does the mirror-image thing, sliding the whole spectrum over instead. And multiplying two signals pointwise in time turns into a circular convolution in frequency, the flip side of the convolution theorem above.

Parseval's theorem: total energy is the same in both domains. Nothing gets lost or added, only re-expressed.

Connection to positional encodings

The original Transformer encodes position with sinusoids:

PE(pos, 2i)   = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

Each dimension pair (2i, 2i+1) oscillates at its own frequency, geometrically spaced from high (dimensions 0, 1) down to low (the last dimensions). Stack them up and every position gets a unique pattern across all frequency bands, the same way a set of Fourier coefficients uniquely pins down a signal.

That construction buys three properties:

  • Uniqueness: no two positions share an encoding.
  • Bounded values: sin and cos never leave [-1, 1].
  • Relative position: the encoding of position p+k is a linear function of the encoding at position p, so the model can learn to attend by relative offset instead of absolute position.

Connection to CNNs

A convolution layer slides a learned filter (kernel) across the input, whether that input is a signal or an image. That sliding operation is, mathematically, convolution.

By the convolution theorem, you could just as well:

  1. FFT the input
  2. FFT the kernel
  3. Multiply the two in the frequency domain
  4. IFFT the result back

Standard CNNs skip this and use direct convolution, which wins for small 3x3 kernels. But for large kernels or global convolution, FFT-based convolution pulls ahead by a wide margin. Some architectures push this further: FNet replaces attention entirely with an FFT, and gets competitive accuracy at O(N log N) instead of attention's O(N^2).

Spectrograms and the Short-Time Fourier Transform

The Short-Time Fourier Transform runs FFTs on overlapping windows of a signal instead of the whole thing at once, then stacks the results into a spectrogram: time on one axis, frequency on the other, and intensity at each point showing how much energy that frequency carried at that moment.

That solves a real gap: a single FFT tells you the frequency content of an entire signal, and nothing about when each frequency happened. A chirp (frequency rising over time) and a chord (every frequency at once) can produce the same magnitude spectrum, even though they sound nothing alike.

STFT procedure:
1. Choose a window size (e.g., 1024 samples)
2. Choose a hop size (e.g., 256 samples -- 75% overlap)
3. For each window position:
   a. Extract the windowed segment
   b. Apply a Hann/Hamming window
   c. Compute FFT
   d. Store the magnitude spectrum as one column of the spectrogram

Spectrograms are the standard input format for audio ML. Speech models like Whisper and DeepSpeech run on mel-spectrograms specifically: the frequency axis gets remapped to the mel scale, which tracks human pitch perception more closely than raw Hz does.

Aliasing

Sample a signal at rate fs and any frequency content above fs/2, the Nyquist frequency, comes back as a fake lower frequency: an aliased copy. A 90 Hz sine wave sampled at 100 Hz produces exactly the same samples as a 10 Hz sine wave. Once you're holding just the samples, there's no way to tell which one you actually recorded.

Example:
  True signal: 90 Hz sine wave
  Sampling rate: 100 Hz
  Apparent frequency: 100 - 90 = 10 Hz

  The samples from the 90 Hz signal at 100 Hz sampling rate
  are identical to the samples from a 10 Hz signal.
  No amount of math can recover the original 90 Hz.

That's why analog-to-digital converters run an anti-aliasing filter first, stripping out anything above Nyquist before sampling happens. The ML version of this shows up when downsampling feature maps without low-pass filtering first. Some architectures fix it with anti-aliased pooling layers.

Zero-padding does not increase resolution

Common misconception: zero-padding a signal before the FFT improves frequency resolution. It doesn't. Zero-padding interpolates between the frequency bins you already have, which makes the spectrum look smoother, but it can't manufacture detail that was never in the original samples.

Real frequency resolution depends only on how long you observed the signal: T = N / fs. Resolving two frequencies separated by delta_f takes at least T = 1 / delta_f seconds of data, full stop. No amount of padding gets around that.

Check: The Concept0/35
1. What does X[0], the DC component of the DFT, represent?Define
Four sample bars, 2, 4, 6 and 8, with the formula X[0] equals sum of x[n] times e to the zero, left as a question mark, asking what X[0] represents.
2. What does Parseval's theorem state?Define
The time-domain energy sum of x squared next to the frequency-domain quantity one over N times sum of X squared, with an unresolved question-mark relation between them, asking how Parseval's theorem relates them.
3. What does the convolution theorem state?Define
Two parallel routes producing the same output y: a direct time-domain box labelled x convolved with h, and a frequency-domain path with FFT of x and FFT of h feeding into a blank operation, then IFFT, asking what fills the blank.
4. What does the DFT take in and give back?Define
Six real-valued time samples feeding into a box labelled DFT, with the output left as a single question mark, asking what comes back and how many values that is.
5. How does the FFT relate to the DFT?Define
A DFT path labelled O of N squared and an FFT path with its cost left blank, both arrows pointing into the same box, X of k, asking whether they give the same coefficients and how the FFT's cost compares.
6. How does the inverse DFT differ from the forward DFT formula?Define
The forward DFT formula given in full, next to the inverse DFT formula with its scale factor and its exponent's sign left as blanks, asking what fills them.
7. In the DFT output, what does index k stand for?Define
A sampling rate of 8000 Hz, 16 samples, and bin k equal to 3, with the frequency that bin stands for left as a question mark.
8. What is the Nyquist frequency for a signal sampled at rate fs?Define
A sampling rate of 300 Hz, with the Nyquist frequency left as a question mark, asking for the highest frequency these samples can represent without aliasing.
9. What does the power spectrum |X[k]|^2 show?Define
A single frequency coefficient X of k equals 6 plus 8i, with its squared magnitude left as a question mark, asking what that quantity shows.
10. What does the phase spectrum angle(X[k]) tell you?Define
One sine component that does not start at zero at the left edge of the window, with its starting point marked, asking what angle of X of k tells you about this component.
11. What is spectral leakage?Define
One window of a sine wave covering 6.3 cycles, so the value at the start of the window and the value at the end don't match, with both endpoints marked, asking what this mismatch is called.
Illustrative, generic cycle count, computed with numpy.
12. What does a window function such as Hann do?Define
A full-amplitude signal feeding into a box labelled with an unnamed function, with the output also left blank, asking what a window function such as Hann does at this step.
13. In the Cooley-Tukey FFT, what is a twiddle factor?Define
Two half-size DFTs, E of k and O of k, both feeding into a box labelled combine via a blank, asking what the combining factor is called.
14. What is circular convolution?Define
Four values, x of 0 through 3, arranged around a ring instead of a line, so index 3 sits next to index 0, asking whether the DFT's convolution includes this wraparound.
15. What is linear convolution, and how do you get it through the DFT?Define
A signal x and a filter h both feeding into a box labelled with an unnamed method, asking what method produces a clean result with no wraparound contamination.
16. What is aliasing?Define
Two sine curves at different generic cycle counts per window, one slow and one fast, both passing through the exact same marked sample points, asking what this phenomenon is called.
Illustrative, generic cycles per window, computed with numpy.
17. Why does the Cooley-Tukey FFT run in O(N log N) instead of the direct DFT's O(N^2)?Explain
A chain of boxes showing 8 samples splitting into two groups of 4, then four groups of 2, then eight groups of 1, noted as 3 levels, with the total work across all levels left as a question mark.
18. What causes spectral leakage in the DFT, and how does windowing address it?Explain
One window of a sine wave repeated twice side by side to show the DFT's periodic-extension assumption, with a dashed line marking the seam where the curve jumps, asking what this assumption causes.
Illustrative, generic cycle count (3.7), computed with numpy.
19. You want to resolve two tones that sit very close together. With delta_f = fs / N, which change gives finer frequency resolution?Explain
A frequency axis with tick marks spaced by the baseline resolution of 2 Hz for fs equal to 100 Hz and N equal to 50 samples, asking which single change narrows the real spacing between ticks.
20. Why does zero-padding a signal before the FFT NOT increase the true frequency resolution?Explain
A flow from a signal of N samples through zero-padding to 4N samples and then an FFT, asking whether this reveals frequency detail that wasn't present before.
21. In the original Transformer's sinusoidal positional encodings, why does each dimension pair oscillate at its own frequency?Explain
A stack of dimension-pair rows for a positional encoding, from dim 0,1 at the highest frequency down to the last pair at the lowest frequency, asking why each pair gets its own frequency.
22. Why can't a single FFT of an entire signal distinguish a chirp (frequency increasing over time) from a chord (all frequencies present at once)?Explain
Two time-domain waveforms, a chirp whose frequency visibly rises over time and a chord made of several steady frequencies at once, asking why a single FFT of the whole signal can't distinguish them.
Illustrative waveforms, generated with numpy.
23. When is FFT-based convolution particularly advantageous over direct convolution in CNN-style architectures?Explain
Two cost formulas, direct convolution costing N times K and FFT-based convolution costing N log N, asking which wins when the kernel size K is large.
24. Why can the inverse DFT recover the original signal with no error?Explain
A signal x transformed into X and then back, with the final result x prime left as a question mark, asking whether it exactly equals the original or loses information.
25. For a real-valued signal, why do the useful DFT coefficients sit in the first N/2 + 1 entries?Explain
A number line of eight DFT indices, with index 3 and its mirror index 5 highlighted and connected by an arrow labelled X of k related to X of N minus k by an unresolved relation, asking why only the first half of coefficients carries new information.
26. A signal is sampled at 1000 Hz and you take N = 500 samples. Which frequency does bin k = 30 correspond to?Apply
A signal sampled at 1000 Hz with 500 samples, asking which frequency bin k equal to 30 corresponds to.
27. Using the lesson's rule for a frequency between fs/2 and fs, a 70 Hz sine is sampled at 100 Hz. What frequency do the samples appear to have?Apply
A true 70 Hz sine signal sampled at 100 Hz, with the apparent frequency the samples show left as a question mark.
28. Two tones sit at 10 Hz and 12 Hz. Using T = 1 / delta_f, what is the minimum observation time needed to resolve them as separate peaks?Apply
Two tones at 10 Hz and 12 Hz, with the minimum observation time needed to resolve them, using T equals 1 over delta f, left as a question mark.
29. Your vision model gets noticeably worse on inputs shifted by just a pixel or two, and its outputs look jagged and moire-patterned after you added a strided downsampling layer. What's the most likely cause?Analyze
An input image feeding through a strided downsampling layer with no filter run first, the output left as a question mark, with a note that the model is now worse on small pixel shifts and looks jagged and moire-patterned.
30. A signal with 10.5 cycles in the window has its power spread across many bins instead of one clean peak. What is the best fix?Analyze
A bar chart of power spectrum values across bins 7 through 14 for a signal with 10.5 cycles in the window, showing the power spread across several bins instead of one clean peak, asking for the best fix.
Computed with numpy, a real 10.5-cycle sine over 64 samples.
31. You convolve two signals through FFT multiplication without zero-padding. The first few output samples contain values from the end of the signal. What is going on?Analyze
A convolution of x equals 4, 5, 6 and h equals 1, 1 run without zero-padding, with y[1] and y[2] given as 9 and 11 but y[0] left as a question mark, noting that the first sample contains a value from the end of the signal.
32. A signal of length 8 has time-domain energy sum |x[n]|^2 = 50. A colleague checks the FFT and says Parseval holds because sum |X[k]|^2 = 50. What is wrong?Analyze
A time-domain energy of 50 for an 8-sample signal next to a colleague's claim that the frequency-domain sum is also 50, with a question mark asking whether this is consistent with Parseval's theorem.
33. You must locate when the pitch of a whale call rises across a 30-second recording, as well as which frequencies it covers. Which approach fits?Design
A card describing a 30-second whale-call recording whose pitch rises over time, noting that both the timing and the frequency content are needed, asking which analysis approach fits.
34. You must apply a 2,000-sample kernel to a signal of 1,000,000 samples and keep the exact, non-wrapped result at the lowest cost. Which design is best?Design
A card listing a signal of 1,000,000 samples and a kernel of 2,000 samples, noting the need for an exact, non-wrapped result at the lowest cost, asking which design fits.
35. You are designing a position encoding that stays bounded, gives each position a unique code, and lets a model attend by relative offset. Which design matches the lesson's idea?Design
A checklist of three requirements for a position encoding: bounded values, a unique code per position, and a relative-offset property, asking which design satisfies all three.

Build It

Step 1: DFT from scratch

The O(N^2) DFT falls straight out of the definition.

python
import math
 
class Complex:
    ...
 
def dft(x):
    N = len(x)
    result = []
    for k in range(N):
        total = Complex(0, 0)
        for n in range(N):
            angle = -2 * math.pi * k * n / N
            w = Complex(math.cos(angle), math.sin(angle))
            xn = x[n] if isinstance(x[n], Complex) else Complex(x[n])
            total = total + xn * w
        result.append(total)
    return result

Step 2: Inverse DFT

Same structure as the forward DFT: positive exponent, divide by N.

python
def idft(X):
    N = len(X)
    result = []
    for n in range(N):
        total = Complex(0, 0)
        for k in range(N):
            angle = 2 * math.pi * k * n / N
            w = Complex(math.cos(angle), math.sin(angle))
            total = total + X[k] * w
        result.append(Complex(total.real / N, total.imag / N))
    return result

Step 3: FFT (Cooley-Tukey)

The recursive FFT needs a power-of-2 length: split into even and odd, recurse on each half, combine with twiddle factors.

python
def fft(x):
    N = len(x)
    if N <= 1:
        return [x[0] if isinstance(x[0], Complex) else Complex(x[0])]
    if N % 2 != 0:
        return dft(x)
 
    even = fft([x[i] for i in range(0, N, 2)])
    odd = fft([x[i] for i in range(1, N, 2)])
 
    result = [Complex(0)] * N
    for k in range(N // 2):
        angle = -2 * math.pi * k / N
        twiddle = Complex(math.cos(angle), math.sin(angle))
        t = twiddle * odd[k]
        result[k] = even[k] + t
        result[k + N // 2] = even[k] - t
    return result

Step 4: Spectral analysis helpers

The power-spectrum formula and the convolution theorem from the Concept section turn directly into two small helpers: one squares the magnitude of each coefficient, the other runs convolution through FFT multiplication instead of a nested loop.

python
def power_spectrum(X):
    return [xk.real ** 2 + xk.imag ** 2 for xk in X]
 
def convolve_fft(x, h):
    N = len(x) + len(h) - 1
    padded_N = 1
    while padded_N < N:
        padded_N *= 2
 
    x_padded = x + [0.0] * (padded_N - len(x))
    h_padded = h + [0.0] * (padded_N - len(h))
 
    X = fft(x_padded)
    H = fft(h_padded)
 
    Y = [xk * hk for xk, hk in zip(X, H)]
 
    y = idft(Y)
    return [y[n].real for n in range(N)]
Check: Build It0/5
1. In the fft code, N = 8 and k = 3. Which values are written to result[3] and result[7]?Apply
Boxes for even[3], odd[3] and twiddle at k equal 3 feeding into two output boxes, result[3] and result[7], both left as question marks.
2. In the fft code, what happens when it is called on a list of length 6?Apply
The fft function called on a 6-sample signal, splitting once into two length-3 halves, with what happens to each odd-length half left as a question mark.
3. convolve_fft is called with len(x) = 5 and len(h) = 3. What padded length does it use, and how long is the returned list?Apply
The convolve_fft function called with len(x) equal to 5 and len(h) equal to 3, with both the padded length it uses and the length of the returned list left as question marks.
4. With x = [1, 2, 3, 4, 0, 0, 0, 0] and h = [1, 1, 1, 0, 0, 0, 0, 0], what is the circular convolution at output index 2?Apply
The arrays x and h from the stem, with the circular convolution at output index 2 left as a question mark.
5. Your idft returns values exactly N times larger than the original signal, and the DFT then inverse test fails. Which part of the idft code is missing?Analyze
The idft function's loop with its final line redacted to a placeholder, noting that the output comes out exactly N times too large, asking which part is missing.

Use It

For real code, reach for numpy's FFT: it's backed by highly optimized C libraries, and there's no reason to hand-roll Cooley-Tukey once you understand it.

python
import numpy as np
 
signal = np.sin(2 * np.pi * 5 * np.arange(256) / 256)
spectrum = np.fft.fft(signal)
freqs = np.fft.fftfreq(256, d=1/256)
 
power = np.abs(spectrum) ** 2
 
positive_freqs = freqs[:len(freqs)//2]
positive_power = power[:len(power)//2]

For windowing and more advanced spectral analysis:

python
from scipy.signal import windows, stft
 
window = windows.hann(256)
windowed = signal * window
spectrum = np.fft.fft(windowed)

For convolution:

python
from scipy.signal import fftconvolve
 
result = fftconvolve(signal, kernel, mode='full')

For spectrograms:

python
from scipy.signal import stft
 
frequencies, times, Zxx = stft(signal, fs=sample_rate, nperseg=256)
spectrogram = np.abs(Zxx) ** 2

The spectrogram comes out as a matrix of shape (n_frequencies, n_time_frames), where each column is the power spectrum at one time window. That matrix, not the raw waveform, is what audio ML models actually take as input.

Check: Use It0/1
1. In the numpy example, signal = sin(2*pi*5*arange(256)/256) is run through np.fft.fft with freqs from fftfreq(256, d=1/256). Where is the peak of positive_power?Apply
The lesson's numpy example code, generating a 5 Hz sine over 256 samples and taking the positive half of its power spectrum, asking where the peak sits.

Ship It

Run code/fourier.py to generate outputs/prompt-spectral-analyzer.md.

Author's Note

The place this lesson actually pays off isn't when you're writing an FFT by hand. NumPy already did that better than you will, and you should use it. It pays off the day your vision model gets twitchy on small input shifts after you add a strided downsample, and the fix turns out to be exactly the aliasing math above: no anti-aliasing filter, high frequencies folding into low ones, edges coming out jagged and moire-patterned instead of clean. Or you're staring at a spectrogram-based speech model, wondering why the frequency axis looks warped, and the answer is the mel scale from the spectrograms section. Learn the mechanics once here, forget most of the derivations within a year, and you'll still recognize the shape of the problem for the rest of your career.

Exercises

  1. Pure tone identification. Create a signal with a single sine wave at an unknown frequency (between 1 and 50 Hz), sampled at 128 Hz for 1 second. Use your DFT to identify the frequency. Verify the answer matches. Now add Gaussian noise with standard deviation 0.5 and repeat. How does noise affect the spectrum?

  2. FFT vs DFT verification. Generate a random signal of length 64. Compute both DFT (O(N^2)) and FFT. Verify that all coefficients match to within 1e-10. Time both functions on signals of length 256, 512, 1024, and 2048. Plot the ratio of DFT time to FFT time.

  3. Convolution theorem proof by example. Create signal x = [1, 2, 3, 4, 0, 0, 0, 0] and filter h = [1, 1, 1, 0, 0, 0, 0, 0]. Compute their circular convolution directly (nested loop). Then compute it via FFT (transform, multiply, inverse transform). Verify the results match. Now do linear convolution by zero-padding appropriately.

  4. Windowing effects. Create a signal that is the sum of two sine waves at 10 Hz and 12 Hz (very close). Sample at 128 Hz for 1 second. Compute the power spectrum with no window, Hann window, and Hamming window. Which window makes it easiest to distinguish the two peaks? Why?

  5. Positional encoding analysis. Generate the sinusoidal positional encodings for d_model = 128 and max_pos = 512. For each pair of positions (p1, p2), compute the dot product of their encodings. Show that the dot product depends only on |p1 - p2|, not on the absolute positions. What happens to the dot product as the distance increases?

Key Terms

TermWhat it means
DFT (Discrete Fourier Transform)Converts N time-domain samples into N frequency-domain coefficients. Each coefficient is the correlation with a complex sinusoid at that frequency
FFT (Fast Fourier Transform)An O(N log N) algorithm to compute the DFT. The Cooley-Tukey algorithm splits even/odd indices recursively
Inverse DFTReconstructs the time-domain signal from frequency coefficients. Same formula as DFT with flipped exponent sign and 1/N scaling
Frequency binEach index k in the DFT output represents frequency k*fs/N Hz. The "bin" is the discrete frequency slot
DC componentX[0], the zero-frequency coefficient. Proportional to the signal mean
Nyquist frequencyfs/2, the maximum frequency representable at sampling rate fs. Frequencies above this alias
Power spectrum|X[k]|^2, the squared magnitude of each frequency coefficient. Shows energy distribution across frequencies
Phase spectrumangle(X[k]), the phase offset of each frequency component. Often ignored in analysis
Spectral leakageSpurious frequency content caused by treating a non-periodic signal as periodic. Reduced by windowing
Window functionA tapering function (Hann, Hamming, Blackman) applied before DFT to reduce spectral leakage
Twiddle factorThe complex exponential e^(-2pii*k/N) used to combine sub-DFTs in the FFT butterfly computation
Convolution theoremConvolution in time domain equals pointwise multiplication in frequency domain. Fundamental to signal processing and CNNs
Circular convolutionConvolution where the signal wraps around. This is what the DFT naturally computes
Linear convolutionStandard convolution without wraparound. Achieved by zero-padding before DFT
Parseval's theoremTotal energy is preserved through the Fourier transform. sum |x[n]|^2 = (1/N) sum |X[k]|^2
AliasingWhen frequencies above Nyquist appear as lower frequencies due to insufficient sampling rate

Further Reading

Code

Ships With