Skip to content
The Lumenary

Math laboratory

Gibbs overshoot, Fejer means and Parseval

Samples the Fourier partial sums S_N of the square wave near its jump: the maximum stays near (2/pi) Si(pi) = 1.17898 as N grows, an overshoot of about 9% of the jump. The Fejer means stay below 1. Parseval gives the L^2 error of S_N, and the sum of 1/k^2 over odd k tends to pi^2/8. Sampled maxima for finitely many N: evidence, with the limits proved in the lesson.

Back to the labNumerical approximation
Gibbs overshoot, Fejer means and ParsevalPythonNumerical approximationComputationally tested
"""Gibbs overshoot, Fejer's cure and Parseval, for the square wave.

The square wave f = sign(sin x) has Fourier series (4/pi) sum over odd k of
sin(k x)/k. Its partial sums S_N overshoot the jump at 0 by about 9% of the
jump however large N is (the Gibbs phenomenon, limit (2/pi) Si(pi)). The
Fejer means (averages of S_0, ..., S_N) never exceed 1, because the Fejer
kernel is a positive approximate identity. Parseval's identity gives the
exact L^2 error of S_N and the sum 1 + 1/9 + 1/25 + ... = pi^2/8.
"""

import math

import numpy as np

def partial_sum(n, x):
    """S_n(x) for the square wave: (4/pi) * sum of sin(kx)/k, odd k <= n."""
    x = np.asarray(x, dtype=float)
    total = np.zeros_like(x)
    for k in range(1, n + 1, 2):
        total += np.sin(k * x) / k
    return 4 / math.pi * total

def fejer_mean(n, x):
    """sigma_n(x) = (S_0 + S_1 + ... + S_n)(x) / (n + 1)."""
    x = np.asarray(x, dtype=float)
    current = np.zeros_like(x)
    total = np.zeros_like(x)
    for m in range(n + 1):
        if m % 2 == 1:
            current = current + 4 / math.pi * np.sin(m * x) / m
        total += current
    return total / (n + 1)

def sine_integral(t, steps=2000):
    """Si(t) = integral of sin(u)/u from 0 to t, by Simpson's rule."""
    if steps % 2:
        steps += 1
    h = t / steps
    def g(u):
        return 1.0 if u == 0 else math.sin(u) / u
    s = g(0) + g(t)
    s += sum((4 if i % 2 else 2) * g(i * h) for i in range(1, steps))
    return s * h / 3

def l2_error_squared(n):
    """(1/pi) * integral of (f - S_n)^2 over (-pi, pi), by Parseval:
    2 minus the sum of (4/(pi k))^2 over odd k <= n."""
    return 2 - sum((4 / (math.pi * k)) ** 2 for k in range(1, n + 1, 2))

def odd_reciprocal_squares(n):
    """1 + 1/9 + 1/25 + ... over odd k <= n."""
    return sum(1 / k ** 2 for k in range(1, n + 1, 2))

def main():
    grid = np.linspace(1e-6, math.pi / 2, 20001)
    gibbs = 2 / math.pi * sine_integral(math.pi)
    print("Square wave sign(sin x): jump of size 2 at x = 0")
    print(f"Gibbs limit (2/pi) Si(pi) = {gibbs:.6f}")
    print(f"{'N':>5} {'max S_N':>10} {'overshoot/jump':>15} {'max Fejer':>10}")
    for n in [9, 49, 199, 999]:
        top = float(np.max(partial_sum(n, grid)))
        fej = float(np.max(fejer_mean(n, grid)))
        print(f"{n:>5} {top:>10.6f} {(top - 1) / 2:>15.4f} {fej:>10.6f}")
    print("The overshoot does not shrink; it moves towards the jump.")
    print("The Fejer means stay below sup |f| = 1 (positive kernel).")
    print()
    print("Parseval: L^2 error of S_N, and sum over odd k of 1/k^2")
    for n in [1, 9, 99, 999]:
        print(f"  N = {n:>3}: error^2 = {l2_error_squared(n):.6f},"
              f" partial sum = {odd_reciprocal_squares(n):.6f}")
    print(f"  pi^2/8 = {math.pi ** 2 / 8:.6f}")
    print()
    print("Epistemic status: numerical evidence. Maxima are sampled on a grid of")
    print("20001 points and Si(pi) is computed by Simpson's rule; finitely many N")
    print("are shown. That the overshoot tends to (2/pi) Si(pi) - 1 and that")
    print("Fejer means of continuous functions converge uniformly are theorems")
    print("proved in the lesson, not consequences of this table.")

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

Used in