codex/past-exam-of-the-mathematics-course-of-the-university-of-oxford/2023/prelims/mathematics-v-fourier-extensions.py

past-exam-of-the-mathematics-course-of-the-university-of-oxford/2023/prelims/mathematics-v-fourier-extensions.py
"""Plot the two Fourier extensions; write the PNG to the working directory.

Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
Run from the mirrored _media directory to put the image beside its page's media.
"""

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np


def main():
    pi = np.pi
    x = np.linspace(-3 * pi, 3 * pi, 18001)
    r = (x + pi) % (2 * pi) - pi
    even = np.minimum(np.abs(r), pi / 2)
    odd = np.sign(r) * even
    # Break the plotted limit at jumps instead of drawing vertical connectors.
    jump_indices = np.flatnonzero(np.abs(np.diff(r)) > pi)
    for j in jump_indices:
        odd[max(j - 1, 0) : j + 2] = np.nan
    odd[[0, -1]] = np.nan

    cosine_sum = np.full_like(x, 3 * pi / 8)
    sine_sum = np.zeros_like(x)
    for n in range(1, 61):
        an = 2 * (np.cos(n * pi / 2) - 1) / (pi * n**2)
        bn = 2 * np.sin(n * pi / 2) / (pi * n**2) - (-1) ** n / n
        cosine_sum += an * np.cos(n * x)
        sine_sum += bn * np.sin(n * x)

    plt.rcParams.update({"font.size": 10, "axes.spines.top": False,
                         "axes.spines.right": False})
    fig, axes = plt.subplots(2, 1, figsize=(10, 6.4), sharex=True,
                             layout="constrained", facecolor="white")
    for ax, limit, partial, title in zip(
        axes, (even, odd), (cosine_sum, sine_sum),
        ("Even periodic extension: cosine series",
         "Odd periodic extension: sine series"), strict=True
    ):
        ax.set_facecolor("white")
        ax.plot(x / pi, limit, color="#242424", linewidth=2.1,
                label="Limit of the series", zorder=2)
        ax.plot(x / pi, partial, color="#1577b8", linewidth=1.0,
                label=r"Partial sum through $n=60$", zorder=3)
        ax.set_title(title, loc="left")
        ax.set_ylabel("Value")
        ax.grid(alpha=0.18)
        ax.axhline(0, color="#888888", linewidth=0.6)
        ax.set_xlim(-3, 3)
        ax.legend(loc="lower right", fontsize=9)
    axes[0].set_ylim(-0.15, 1.9)
    axes[0].set_yticks([0, pi / 2], ["0", r"$\pi/2$"])
    axes[1].set_ylim(-2.1, 2.1)
    axes[1].set_yticks([-pi / 2, 0, pi / 2],
                      [r"$-\pi/2$", "0", r"$\pi/2$"])
    jumps = np.array([-3, -1, 1, 3])
    for side in [-1, 1]:
        axes[1].scatter(jumps, np.full(4, side * pi / 2), s=32,
                        facecolor="white", edgecolor="#242424", zorder=5,
                        clip_on=False)
    axes[1].scatter(jumps, np.zeros(4), s=24, color="#242424", zorder=6,
                    clip_on=False)
    axes[1].annotate("Gibbs oscillations", xy=(0.965, 1.82),
                     xytext=(1.45, 2.0), fontsize=9,
                     arrowprops={"arrowstyle": "->", "color": "#1577b8"},
                     color="#1577b8")
    axes[1].set_xticks(np.arange(-3, 4),
                      [r"$-3\pi$", r"$-2\pi$", r"$-\pi$", "0",
                       r"$\pi$", r"$2\pi$", r"$3\pi$"])
    axes[1].set_xlabel("x")
    fig.savefig("mathematics-v-fourier-extensions.png", dpi=120,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()