Skip to content
The Lumenary

Math laboratory

Fourier series: Gibbs overshoot and Parseval's identity

Evaluates partial sums of the square-wave Fourier series at their first peak x = pi/(2n) and shows the peak tending to (2/pi) Si(pi) = 1.17898 instead of 1 (the Gibbs phenomenon), checks that the sums are 0 at the jump, and uses Parseval's identity for the sawtooth to approach sum 1/n^2 = pi^2/6 with error about 1/N. Numerical evidence.

Back to the labNumerical approximation
Fourier series: Gibbs overshoot and Parseval's identityPythonNumerical approximationComputationally tested
"""Fourier series: the Gibbs overshoot and Parseval's identity, measured.

The square wave sign(sin x) has Fourier series (4/pi) * sum over odd k of
sin(kx)/k. Its partial sum with n non-zero terms, S_n, has derivative
(2/pi) sin(2nx)/sin(x), so its first maximum to the right of the jump at 0 is
at x = pi/(2n), exactly. We evaluate S_n there: the peak does not shrink to 1
but tends to (2/pi) Si(pi) = 1.17898..., an overshoot of about 9% of the jump.
At x = 0 every partial sum is 0, the average of the one-sided limits, as
Dirichlet's theorem predicts. Then Parseval's identity for the sawtooth
f(x) = x on (-pi, pi), whose sine coefficients are 2(-1)^(n+1)/n, gives
sum 1/n^2 = pi^2/6; we watch the partial sums close in, with error ~ 1/N.

Evidence: numerical (floating point, Simpson's rule for Si(pi)), with the
locations of the maxima and the Parseval formula derived exactly.
"""

import math

def square_partial(x, n):
    """S_n(x) = (4/pi) * sum_{j=1..n} sin((2j-1)x)/(2j-1)."""
    return 4 / math.pi * sum(math.sin((2 * j - 1) * x) / (2 * j - 1)
                             for j in range(1, n + 1))

def first_peak(n):
    """Value of S_n at its first maximum to the right of 0, x = pi/(2n)."""
    return square_partial(math.pi / (2 * n), n)

def sine_integral(x, steps=2000):
    """Si(x) = integral of sin(t)/t from 0 to x, by Simpson's rule."""
    if steps % 2:
        raise ValueError("steps must be even")
    f = lambda t: 1.0 if t == 0 else math.sin(t) / t
    h = x / steps
    s = f(0) + f(x)
    s += 4 * sum(f((2 * i - 1) * h) for i in range(1, steps // 2 + 1))
    s += 2 * sum(f(2 * i * h) for i in range(1, steps // 2))
    return s * h / 3

def gibbs_limit():
    """(2/pi) Si(pi), the limit of the first peaks."""
    return 2 / math.pi * sine_integral(math.pi)

def sawtooth_coefficient(n):
    """b_n = (1/pi) * integral of x sin(nx) over (-pi, pi) = 2(-1)^(n+1)/n."""
    return 2 * (-1) ** (n + 1) / n

def parseval_partial(N):
    """(1/4) * sum_{n<=N} b_n^2, which Parseval says tends to pi^2/6."""
    return sum(sawtooth_coefficient(n) ** 2 for n in range(1, N + 1)) / 4

def main():
    g = gibbs_limit()
    print("Square wave sign(sin x): peak of S_n at x = pi/(2n)")
    print("     n      peak    S_n(0)   S_n(pi/2)")
    for n in [1, 5, 10, 50, 200, 1000]:
        print(f"{n:6d}   {first_peak(n):.5f}   {square_partial(0.0, n):+.1f}"
              f"   {square_partial(math.pi / 2, n):.5f}")
    print(f"limit (2/pi) Si(pi) = {g:.5f}: overshoot {100 * (g - 1) / 2:.2f}%"
          " of the jump")
    print("The peak moves towards the jump but never shrinks: the convergence")
    print("is pointwise, not uniform, near a discontinuity.")
    print()
    print("Parseval for f(x) = x on (-pi, pi): sum 1/n^2 = pi^2/6")
    print("      N    partial sum      error   N * error")
    for N in [10, 100, 1000, 10000]:
        err = math.pi ** 2 / 6 - parseval_partial(N)
        print(f"{N:7d}   {parseval_partial(N):.9f}   {err:.2e}   {N * err:.4f}")
    print()
    print("Epistemic status: numerical evidence. The peak locations and the")
    print("coefficients are derived exactly; the values are floating-point")
    print("and Si(pi) comes from Simpson's rule. Finitely many n cannot prove")
    print("the limits; the Gibbs constant and Parseval's identity are proved")
    print("in the lesson and its references.")

if __name__ == "__main__":
    main()
Runs entirely in your browser, in an isolated worker. Nothing is sent to a server.

Used in