2026, Jan 13 23:00

Fixing Imaginary Parts in DFT of a Rectangular Window: the Off-By-One Range Bug in Python

Learn why a symmetric rectangular window yields complex Fourier coefficients: an off-by-one in Python's range shifts phase. See the fix and correct DFT code.

Fourier coefficients of a simple rectangular window should come out real in many textbook setups, yet sometimes a small imaginary ripple appears in the result. When that happens, it’s tempting to blame floating-point noise. But if the pattern looks systematic rather than random, there’s usually a structural reason in the data or the indexing. Here’s a concrete example that shows how a single off-by-one detail in Python’s range can introduce a phase and produce a non-negligible imaginary component.

Problem setup

Consider a symmetric window centered at zero, intended to be one from −100 to 100 and zero elsewhere, sampled on a grid from −1000 to 1000. The following code builds that window and computes its Fourier coefficients via the direct double sum:

import numpy as np
import cmath
import matplotlib.pyplot as plt
M = 1000
win = np.zeros(2 * M + 1)
for idx in range(-100, 100):
    win[idx + M] = 1
N = 2001
spec = np.zeros(N, dtype=complex)
for k in range(-M, M + 1):
    for n in range(-M, M + 1):
        spec[k + M] += (1 / N) * win[n + M] * cmath.exp(-1j * 2 * np.pi * k * n / N)

The expectation was a purely real spectrum for this window, but the output shows a small but structured imaginary component. Its shape doesn’t look like random round-off; it exhibits a clear sinusoidal pattern across frequency.

What’s actually going on

The window used in the derivation is symmetric around zero. The constructed window in the code is not. Python’s range is end-exclusive, so range(-100, 100) includes −100 through 99. That makes the nonzero support from −100 to 99, not from −100 to 100. The window is therefore shifted by one sample relative to the intended symmetric definition.

That shift introduces a phase in the discrete Fourier transform. The phase shows up as a nonzero imaginary part in the complex coefficients, which is why the imaginary component is structured rather than random. In other words, the math is fine; the input wasn’t the one assumed by the derivation.

Fix and corrected code

To construct the intended symmetric window, include the upper endpoint so that the ones run from −100 to 100 inclusive. The rest of the computation stays the same.

import numpy as np
import cmath
import matplotlib.pyplot as plt
M = 1000
win = np.zeros(2 * M + 1)
for idx in range(-100, 101):  # -100 to 100 inclusive
    win[idx + M] = 1
N = 2001
spec = np.zeros(N, dtype=complex)
for k in range(-M, M + 1):
    for n in range(-M, M + 1):
        spec[k + M] += (1 / N) * win[n + M] * cmath.exp(-1j * 2 * np.pi * k * n / N)

With this correction, the imaginary part collapses to values near machine precision. Using double precision (as in NumPy), you typically get about 16 significant digits. In this example the real part is on the order of 1e-2, while the imaginary part drops to about 1e-18, which is consistent with numerical limits.

Why this detail matters

Fourier analysis is exquisitely sensitive to alignment. A one-sample asymmetry in a window that’s meant to be centered injects a phase factor and changes the character of the spectrum. If you’re diagnosing unexpected complex components, start by checking the exact sample support and endpoints, especially in languages where range bounds are half-open. Distinguishing between structural causes and floating-point noise saves time and avoids masking real issues with numerical explanations.

Practical closing advice

When results look off, reduce the scenario to a much smaller version and build it up from there. Try tiny supports, like a few samples around zero, and inspect how the spectrum changes as you add points up to 100. This makes it much easier to see alignment and symmetry mistakes. And whenever you rely on an inclusive interval in your derivation, make sure the code builds the same inclusive interval—here that means using an upper bound of 101 with Python’s end-exclusive range. With the window correctly centered, any residual imaginary part at the scale of 1e-18 relative to a 1e-2 real part can be safely attributed to double-precision limits.