Worked examples — NumPy FFT — np.fft module
Before we begin, three symbols we will use everywhere, spelled out in plain words:
If any of those feel shaky, revisit the parent parent note and Sampling and Nyquist Theorem first. Everything below is built only on these.
The scenario matrix
Here is the full list of case-classes the FFT can throw at you. Every cell is covered by at least one worked example below.
| # | Case class | What makes it special | Covered by |
|---|---|---|---|
| A | Single clean tone, | bin index happens to equal Hz | Ex 1 |
| B | Single tone, | bin index Hz — must use fftfreq |
Ex 2 |
| C | DC / constant signal | all energy in bin 0 | Ex 3 |
| D | Nyquist frequency exactly | the highest representable freq, | Ex 4 |
| E | Sum of two tones | two peaks, linearity | Ex 5 |
| F | Frequency between bins | spectral leakage, no exact bin | Ex 6 |
| G | Zero signal / degenerate | empty spectrum, edge behaviour | Ex 7 |
| H | Real-world word problem | choosing , from requirements | Ex 8 |
| I | Exam twist: aliasing | an over-Nyquist tone masquerading | Ex 9 |
We also touch conjugate symmetry (Ex 5) and the round-trip inverse (Ex 7) along the way.
Example 1 — Case A: the "lucky" tone where bin = Hz
Forecast: guess the peak bin and whether the raw equals 3. Write your guess down now.
- Compute the window length. s. Why this step? The window length controls the resolution, and here it makes the numbers line up magically — we want to see why it looks magical.
- Compute resolution. Hz. So bin sits at frequency Hz. Why this step? Because , "bin number" and "Hz" are numerically identical — a coincidence, not a law.
- Locate the tone. A 50 Hz tone lands in the bin at , i.e. . Why this step? We use .
- Recover amplitude. Raw magnitude at that bin is . Multiply by : . Why this step? A real sine of amplitude splits its energy between and ; each half carries . The factor undoes that.
Verify: peak at Hz ✓; recovered amplitude ✓.
Example 2 — Case B: bin index is NOT the frequency
Forecast: is the peak still at bin 50? Guess before reading.
- Window & resolution. s, so Hz. Why this step? is the only thing that converts bins ↔ Hz.
- Find the bin. . Why this step? The tone still lives at 50 Hz, but now each bin is only 0.5 Hz wide, so it takes 100 bins to reach 50 Hz.
- Confirm with
rfftfreq.np.fft.rfftfreq(400, d=1/200)[100]returns Hz. Why this step? This is the safe habit: never eyeball, always ask NumPy what a bin means.
Verify: , and Hz ✓. Note bin 50 here would be Hz — the wrong answer if you'd trusted Example 1's coincidence.
Example 3 — Case C: a constant (DC-only) signal
Forecast: how many non-zero bins? What value in bin 0?
- Recognise there is no oscillation. A constant never goes up and down. Why this step? Every non-zero bin measures a wiggle at some frequency; a flat line has no wiggle, so only the "zero-frequency" (DC) bin can be non-zero.
- Compute bin 0. . Why this step? At the exponential is for every term, so is literally the sum of the samples.
- All other bins vanish. For : by orthogonality of the roots of unity. Why this step? This is the orthogonality identity from the parent note: a full turn of a non-zero frequency sums to zero.
Verify: np.fft.fft([2,2,2,2]) = [8, 0, 0, 0] ✓. And the DC amplitude scaling is (not ): = the constant value ✓.
Example 4 — Case D: the Nyquist frequency exactly
Forecast: how many bins carry this tone — one, or the usual two?
- Find Nyquist. Hz — the highest frequency a rate of 8 Hz can represent (see Sampling and Nyquist Theorem). Why this step? Nyquist is the top of the representable band; a tone right at it behaves specially.
- Sample the cosine. , so — the alternating signal . Why this step? At exactly Nyquist, a cosine collapses to a simple sign-flip pattern.
- The bin. ; here Hz, so . This is the single Nyquist bin — it has no separate mirror partner because maps to itself. Why this step? The pair merges at Nyquist, so its energy is not split into two halves.
- Amplitude scaling. Because it isn't split, the Nyquist bin (like DC) uses factor , not . Value , and = the cosine's amplitude. Why this step? Applying here would double-count and give a wrong amplitude of 2.
Verify: np.fft.fft([1,-1]*4)[4] = 8 ✓; amplitude ✓.
Example 5 — Case E: sum of two tones (linearity + symmetry)
Forecast: two peaks — which is taller?
- Resolution. Hz, so bin = Hz. Why this step? again gives the "lucky" alignment.
- Locate peaks. 10 Hz → bin 10, amplitude 2; 30 Hz → bin 30, amplitude 5. The DFT is linear, so the spectrum of a sum is the sum of the spectra — the tones don't interfere. Why this step? Linearity () is what lets us read off each tone independently.
- Recover each amplitude. Multiply each raw peak by : bin 10 gives , bin 30 gives . Why this step? Both are ordinary real sines (neither DC nor Nyquist), so the split-in-half rule applies to both.
- Conjugate symmetry check. For real input, . So bin mirrors bin 10, and bin mirrors bin 30.
rfftdiscards these mirrors. Why this step? It reassures us the "negative-frequency" copies are redundant, not new information.
Verify: peaks at 10 and 30 Hz ✓; recovered amplitudes and ✓; so the 30 Hz peak is taller ✓.
Example 6 — Case F: a frequency between two bins (leakage)
Forecast: one sharp peak, or something messier?
- See the gap. Bins sit at Hz. 10.5 Hz falls between bins 10 and 11. Why this step? The FFT can only report energy at its grid of bins; a tone off-grid has nowhere clean to land.
- Energy smears. The tone's energy leaks into the neighbouring bins, with the biggest amounts in bins 10 and 11 and smaller ripples spreading outward. Why this step? Because the tone is not periodic within the window, it looks like a truncated wave — see Spectral Leakage and Windowing.
- Which bin is tallest? Roughly the nearest bin; the peak height is lower than the true amplitude because the energy is shared. So the naive read-off underestimates. Why this step? It warns you: a peak that isn't exactly on a bin cannot be amplitude-read reliably without a window function.

Verify (numeric): with the sim, the tallest bin is bin 10 or 11 (both near 10.5), i.e. round(peak_freq) is 10 or 11 ✓, and the raw peak amplitude read is strictly less than the true amplitude ✓.
Example 7 — Case G: the all-zeros / degenerate signal + round-trip
Forecast: guess (a) instantly; for (b) guess exactly or approximately.
- (a) Zeros in, zeros out. Every . The spectrum is all zeros. Why this step? A signal with no energy has no frequency content — the degenerate baseline every other case is measured against.
- (b) Round-trip identity. By the parent's inverse formula , the and the sign exactly undo the forward transform. Why this step? This is the sanity check you run whenever you doubt your pipeline — if the round-trip fails, your bug is in your code, not NumPy.
- Test on a mixed signal. For
x = [1.0, 2.0, 0.0, -1.0],np.allclose(x, np.fft.ifft(np.fft.fft(x)).real)isTrue(equal up to floating-point dust). Why this step? "Up to float error" —allclose, not==, because binary floats can't store every intermediate exactly.
Verify: fft(zeros(8)) = all zeros ✓; allclose(x, ifft(fft(x)).real) = True ✓.
Example 8 — Case H: real-world word problem (choosing and )
Forecast: which requirement fixes , and which fixes ? Decide before reading.
- Nyquist fixes . To capture frequencies up to 2000 Hz you need Hz. Pick Hz (comfortable margin, a standard audio rate). Why this step? The Nyquist theorem says must exceed twice the highest frequency, or aliasing corrupts it.
- Resolution fixes . To resolve tones 1 Hz apart you need Hz. Since , you need s. Why this step? Frequency resolution depends only on how long you listen, never on how fast you sample.
- Compute . samples. Why this step? is just rate times duration — it falls out once the other two are chosen.
Verify: ✓ (Nyquist ok); Hz ✓ (meets 1 Hz spacing); ✓.
Example 9 — Case I: exam twist — the aliasing impostor
Forecast: guess where the peak shows up — is it at 70 Hz?
- Check against Nyquist. Hz. But — the tone is above the representable band. Why this step? Any frequency above Nyquist cannot be honestly recorded; it will be disguised.
- Fold it back (aliasing). An over-Nyquist frequency appears at . Here , : the alias is Hz. Why this step? Sampling can't distinguish Hz from Hz — both hit the same sample values, so the FFT reports the lower "folded" frequency.
- Read the FFT. With Hz, the peak sits in bin 30, i.e. it claims the tone is 30 Hz. Why this step? This is the impostor — the maths is correct, but the physics was violated at sampling time.

Verify: alias Hz ✓; peak bin ✓.
Recall Self-test on the matrix
Which case has energy in exactly ONE bin with the scaling? ::: DC (bin 0) and Nyquist (bin ) both use , not .
A 10.5 Hz tone with 1 Hz bins does what? ::: Leaks across neighbouring bins; no single exact peak.
To resolve 1 Hz-apart tones, what must you change? ::: The record length (), not .
A 70 Hz tone sampled at 100 Hz appears where? ::: Aliased to 30 Hz.
ifft(fft(x)) returns x exactly or approximately? ::: Approximately — equal up to floating-point error (use allclose).
Connections
- NumPy FFT — np.fft module — the parent machinery these examples exercise
- Sampling and Nyquist Theorem — Cases D, H, I live here
- Spectral Leakage and Windowing — Case F (off-bin tones)
- Discrete Fourier Transform — the sums we expanded by hand in Cases C, D
- Complex Numbers and Euler's Formula — the probe waves
- scipy.signal — filters that prevent the aliasing of Case I