5.4.7 · D4Scientific Computing (Python)

Exercises — NumPy FFT — np.fft module

3,338 words15 min readBack to topic

Level 1 — Recognition

Goal: can you read the formulas and match names to jobs?

L1.1

State, in one line each, what these return: np.fft.fft(x), np.fft.rfft(x), np.fft.fftfreq(N, d), np.fft.rfftfreq(N, d), np.fft.ifft(X).

Recall Solution
  • np.fft.fft(x) → the forward DFT: a complex array of length ; each entry is the complex amplitude (DFT coefficient) of frequency-bin (including negative-frequency bins).
  • np.fft.rfft(x) → forward DFT of a real input, returning only the non-redundant bins (for even that is bins, 0 up to Nyquist).
  • np.fft.fftfreq(N, d) → the frequency in Hz of each fft bin, including negative frequencies, with sample spacing .
  • np.fft.rfftfreq(N, d) → the frequencies for rfft (only through Nyquist, all ).
  • np.fft.ifft(X) → the inverse DFT, taking a spectrum back to the time domain.

L1.2

Recall the DFT definition. Fill the blanks:

Recall Solution

The exponent carries a minus sign (it is the conjugate of the probe wave we correlate against), and runs from to — exactly output bins for input samples.

L1.3

A signal has samples taken at Hz. Compute the record length and the frequency resolution .

Recall Solution

. (equivalently Hz). The two formulas agree, as they must.


Level 2 — Application

Goal: plug numbers in and get a spectrum right.

L2.1

Samples are spaced by s and you have of them. What frequency does bin correspond to?

Recall Solution

First get the rate: Hz. Then . (Here s, so bin index numerically equals frequency — a lucky coincidence of s, not a general rule.)

L2.2

You run np.fft.rfft on a pure real sine of amplitude with samples. The tallest bin of np.abs(X) reads . Recover the amplitude and confirm it equals 5.

Recall Solution

Where does the come from — the full "why". Write the real sine using Euler's formula: So a real sine is literally two complex probe-waves: one at with weight and one at with weight each carries half the amplitude, . Now recall the orthogonality fact (proved in L5.1): when you correlate against the matching probe, every mismatched term cancels and the matching term is summed times. That multiplies whichever weight sits at that frequency by . Hence the bin at reads To invert this and get back, multiply by : The DC bin is different: a constant offset is one term, not a pair, so it is scaled by (not ) and you divide by just .

L2.3

Compute by hand np.fft.fft([1, 1, 1, 1]). What is bin 0, and why are the rest zero?

Recall Solution

. For : This is a geometric series with and , so . Result: . A constant signal has only zero-frequency content — no oscillation exists to detect.

Figure s01 (below): the left panel plots the flat time signal ; the right panel plots its magnitude spectrum . The point of the picture is that all the energy collapses into the single DC bin () and every oscillating probe returns exactly zero — a flat line "rings" only for the zero-frequency probe.

Figure — NumPy FFT — np.fft module

L2.4

A recording uses Hz. What is the highest frequency you can faithfully represent, and which rfft bin holds it if ?

Recall Solution

The Nyquist frequency (see Sampling and Nyquist Theorem) is half the sampling rate: rfft returns bins (for even ), so the Nyquist frequency sits in the last bin, index . Any real tone above 4000 Hz would alias (fold back) and appear at the wrong frequency.


Level 3 — Analysis

Goal: reason about why a spectrum looks the way it does.

L3.1

For real input, prove (the conjugate-symmetry that lets rfft throw half the bins away). Recall is the complex conjugate — the mirror of across the real axis.

Recall Solution

Start from the definition and substitute for : Since for every integer , this is Now is real so , and the whole sum is the conjugate of (taking the conjugate flips the sign of the exponent, ): Consequence: the top half of the spectrum is the mirror-conjugate of the bottom half, so rfft keeps only the non-redundant half with no loss.

L3.2

Two experimenters record the same 40 Hz tone. Alice uses , Bob uses , both at Hz. Whose spectrum resolves neighbouring frequencies more finely, and by what factor?

Recall Solution

Resolution is .

  • Alice: (window s).
  • Bob: (window s). Bob's resolution is finer by a factor of . Longer recording ⇒ tighter bins. Note the sampling rate is identical, so both see the same maximum frequency (Nyquist 500 Hz) — a longer record buys resolution, not range.

Figure s02 (below): both spectra are plotted near 40 Hz. The red dashed line marks the true 40 Hz peak — notice both curves peak at the same place (same range), but Bob's blue curve (longer window s) has bins spaced 4× closer together than Alice's yellow curve, so it can separate tones that Alice would merge. This is the visual meaning of "resolution = ."

Figure — NumPy FFT — np.fft module

L3.3

You fft a signal and plot np.abs(X) directly against np.arange(N). The plot shows a mystery second peak near the far right end. What is it, and how do you fix the plot?

Recall Solution

The "mystery" peak is the negative-frequency twin of your tone. fft lays the bins out as so the top of the array holds negative frequencies masquerading as high indices. Fix: call np.fft.fftshift(X) (and fftshift(fftfreq(...))) to move zero frequency to the centre, giving a spectrum symmetric about 0. Then the two peaks sit at and , mirror images, exactly as Euler's formula predicts for a real cosine.


Level 4 — Synthesis

Goal: build a complete analysis pipeline and predict its output.

L4.1

Write the exact NumPy code to (a) build a 2-second signal at Hz that is (a 60 Hz tone on a DC offset of 1), (b) take its real spectrum, (c) print the recovered amplitude of the 60 Hz peak and the DC value. Predict both printed numbers.

Recall Solution
import numpy as np
fs = 500.0
N  = 1000                 # 2 s * 500 Hz
t  = np.arange(N) / fs
x  = 2*np.sin(2*np.pi*60*t) + 1
 
X   = np.fft.rfft(x)
f   = np.fft.rfftfreq(N, d=1/fs)
amp = np.abs(X) * 2 / N            # sinusoid scaling
 
k60 = np.argmin(np.abs(f - 60))    # bin nearest 60 Hz
print(f[k60], amp[k60])            # ~60.0 Hz, ~2.0
print(np.abs(X[0]) / N)            # DC = 1.0

Predictions:

  • The tone bin recovers amplitude (the factor undoes the scaling of a real sine).
  • DC bin: . The sine averages to 0 over whole cycles, so the mean is the offset . Note DC uses , not .

L4.2

Show the correct amplitude normalization in code for a signal that mixes a DC offset, a sine, and (for even ) a Nyquist-frequency component — implementing the special-case for DC/Nyquist and everywhere else.

Recall Solution

The gap most people leave is implementing the special case. Here is the pattern:

import numpy as np
X   = np.fft.rfft(x)              # real input, length N
N   = x.size
amp = np.abs(X) * 2 / N           # start: assume every bin is a sine pair
amp[0] = np.abs(X[0]) / N         # DC bin has NO mirror twin -> use 1/N
if N % 2 == 0:                    # even N: last rfft bin is exactly Nyquist
    amp[-1] = np.abs(X[-1]) / N   # Nyquist bin also has no twin -> use 1/N

Why: the factor assumes the energy was split into a and mirror pair. Bin 0 (DC) is a single constant term, and — only when is even — the last bin lands exactly on Nyquist and is likewise unpaired. Both must be divided by , not . For odd the rfft output does not include an exact Nyquist bin, so only the DC correction is needed (the if N % 2 == 0 guard handles this automatically). Run it on L4.1's signal and the DC entry now reads exactly while the 60 Hz entry reads .

L4.3

The Convolution Theorem says convolution in time equals multiplication in frequency. Use np.fft to compute the circular convolution of a = [1,2,3,4] with b = [1,0,0,0] and predict the result before running.

Recall Solution

b = [1,0,0,0] is the identity for circular convolution (a single spike at position 0). Convolving anything with it returns that thing unchanged, so we expect . Via the theorem:

a = np.array([1,2,3,4], float)
b = np.array([1,0,0,0], float)
c = np.fft.ifft(np.fft.fft(a) * np.fft.fft(b)).real
print(np.round(c, 6))   # [1. 2. 3. 4.]

Why it works: (all bins 1, since a unit spike contains every frequency equally), so multiplying leaves untouched, and ifft returns . Result: .

L4.4

You must analyse a tone that is not a whole number of cycles in your window (say 60.5 Hz in a 1-second window). What artefact appears, what causes it, and what tool fixes it?

Recall Solution

The artefact is spectral leakage — energy smears into neighbouring bins instead of sitting in one clean spike. Cause: the DFT assumes the window repeats periodically; a non-integer number of cycles creates a discontinuity at the wrap-around, and a sharp jump contains many frequencies. Fix: apply a window function (Hann, Hamming, ...) that tapers the signal smoothly to zero at both ends, removing the discontinuity. See Spectral Leakage and Windowing. Higher-level windows and spectral tools live in scipy.signal.


Level 5 — Mastery

Goal: derive and verify the deep facts by hand.

L5.1

Prove the orthogonality relation that makes Fourier analysis work: and explain in one sentence why it guarantees ifft(fft(x)) == x.

Recall Solution

Let .

  • Case : then , so each of the terms is and the sum is .
  • Case : but . The geometric series gives Why it matters: when you plug into the inverse formula , the orthogonality makes every "wrong" frequency cancel and leaves exactly the original sample — so the round trip is exact. This is the algebraic heart of Discrete Fourier Transform.

L5.2

By hand, compute the full np.fft.fft([0, 1, 0, -1]) (a discrete sine sampled at 4 points). Interpret each of the four bins physically. Recall denotes the complex conjugate.

Recall Solution

Use with . Only and contribute.

  • (DC / mean is zero — a pure oscillation).
  • .
  • .
  • .

Result: . Interpretation: energy only in bins 1 and 3, which are the / frequency pair (note , i.e. the conjugate — confirming conjugate symmetry for real input). The purely imaginary values are the signature of a sine (odd function); a cosine would give purely real bins.

L5.3 (capstone)

Given , verify Parseval's theorem numerically and state it: total energy is conserved between domains, Compute both sides by hand.

Recall Solution

Left side (time energy): . Right side (frequency energy): first fft([1,2,3,4]):

  • .
  • , so .
  • , so .
  • , so .

Sum . Divide by : . Both sides equal 30 ✓. Meaning: the FFT just repackages energy from time into frequency — none is created or destroyed. The is bookkeeping from NumPy's convention.



Connections