codexsquare-wave-fourier-ringing.py
square-wave-fourier-ringing.py
"""Show convergence and persistent square-wave Fourier overshoot; output to cwd."""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def odd_sum(x, count):
harmonics = np.arange(1, 2*count, 2)
return (4/np.pi) * (np.sin(np.outer(x, harmonics)) / harmonics).sum(axis=1)
def main():
counts = [3, 10, 50]
x = np.linspace(-np.pi, np.pi, 5001)
zoom = np.linspace(-.55, .55, 2401)
assert np.allclose(odd_sum(np.array([0.0]), 50), 0)
assert np.allclose(odd_sum(-zoom, 10), -odd_sum(zoom, 10), atol=1e-12)
peaks = [(count, np.pi/(2*count), float(odd_sum(np.array([np.pi/(2*count)]), count)[0]))
for count in counts]
dense_count = 10000
limiting_peak = float(odd_sum(np.array([np.pi/(2*dense_count)]), dense_count)[0])
assert np.isclose(limiting_peak, 1.1789797444721675, atol=2e-8)
# Midpoint quadrature gives an independent limiting-height calculation.
u = (np.arange(20000) + .5) / 20000
midpoint_limit = 2 * np.mean(np.sinc(u))
assert np.isclose(midpoint_limit, limiting_peak, atol=2e-8)
plt.rcParams.update({"font.size": 12, "axes.titlesize": 14})
fig, axes = plt.subplots(1, 2, figsize=(11.2, 5.8), dpi=120, facecolor="white")
colors = ["#be7723", "#377baa", "#298366"]
for ax, domain in zip(axes, [x, zoom]):
# Plot the one-sided levels separately: vertical lines are not function values.
left = domain[domain < 0]
right = domain[domain > 0]
ax.plot(left, -np.ones_like(left), color="#303943", lw=1.7, linestyle="--", label="Square-wave levels")
ax.plot(right, np.ones_like(right), color="#303943", lw=1.7, linestyle="--")
ax.scatter([0], [0], color="#303943", s=35, zorder=6)
for count, color in zip(counts, colors):
ax.plot(domain, odd_sum(domain, count), color=color, lw=1.8, label=f"M = {count} odd modes")
ax.set(ylim=(-1.35, 1.35), xlabel="Angular coordinate x (radians)", ylabel="Value")
ax.grid(color="#e6eaed")
ax.spines[["top", "right"]].set_visible(False)
axes[0].set(xlim=(-np.pi, np.pi), title="One complete period")
axes[0].set_xticks([-np.pi, -np.pi/2, 0, np.pi/2, np.pi], ["−π", "−π/2", "0", "π/2", "π"])
axes[1].set(xlim=(-.55, .55), title="The first peaks approach the jump")
axes[1].axhline(limiting_peak, color="#9b566a", lw=1.4, linestyle=":")
for (_, location, height), color in zip(peaks, colors):
axes[1].scatter([location], [height], color=color, s=50, zorder=5)
axes[1].text(-.53, 1.21, "Limiting first peak ≈ 1.17898", color="#87495b", fontsize=11,
bbox={"facecolor": "white", "edgecolor": "none", "alpha": .9})
axes[1].text(-.53, -.52, "At the jump:\nevery sum equals 0", fontsize=11,
bbox={"facecolor": "white", "edgecolor": "none", "alpha": .9})
fig.suptitle("Fourier sums sharpen a square wave while retaining overshoot", fontsize=16, y=.98)
fig.text(.5, .105, "M nonzero odd modes retain frequencies 1, 3, …, 2M−1", ha="center", fontsize=12)
fig.legend(*axes[0].get_legend_handles_labels(), loc="lower center", ncol=4,
frameon=False, bbox_to_anchor=(.5, .024), fontsize=11)
fig.subplots_adjust(left=.07, right=.98, top=.83, bottom=.23, wspace=.25)
fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
facecolor="white", transparent=False)
plt.close(fig)
print({"first_peaks": peaks, "limiting_peak": limiting_peak,
"overshoot_fraction_of_jump": (limiting_peak-1)/2})
if __name__ == "__main__":
main()