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

past-exam-of-the-mathematics-course-of-the-university-of-oxford/2023/prelims/mathematics-v-string-frequencies.py
"""Illustrate both string-frequency equations; write the PNG to the cwd.

Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
"""

import matplotlib

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


def symmetric_root(n, alpha):
    """Bisection on the positive branch (n*pi, n*pi + pi/2)."""
    left, right = n * np.pi + 1e-10, (n + 0.5) * np.pi
    for _ in range(60):
        mid = (left + right) / 2
        if 1 / np.tan(mid) - alpha * mid > 0:
            left = mid
        else:
            right = mid
    return (left + right) / 2


def main():
    pi = np.pi
    alpha = 1.0
    z = np.linspace(0, 4.1 * pi, 6000)
    plt.rcParams.update({"font.size": 10, "axes.spines.top": False,
                         "axes.spines.right": False})
    fig, axes = plt.subplots(2, 1, figsize=(9, 6.2), sharex=True,
                             layout="constrained", facecolor="white")
    axes[0].plot(z / pi, np.sin(z), color="#1577b8", linewidth=1.8,
                 label=r"$\sin z$")
    axes[0].axhline(0, color="#777777", linewidth=0.8)
    axes[0].scatter(np.arange(1, 5), np.zeros(4), color="#c53b32",
                    s=32, zorder=5, label=r"Allowed $z=n\pi$, $n\geq1$")
    axes[0].scatter([0], [0], s=32, facecolor="white", edgecolor="#777777",
                    zorder=5)
    axes[0].annotate("Zero is excluded", xy=(0, 0), xytext=(0.15, 0.6),
                     fontsize=9, arrowprops={"arrowstyle": "->"})
    axes[0].set_title("Antisymmetric modes: the central mass is at rest",
                      loc="left")
    axes[0].set_ylim(-1.4, 1.4)
    axes[0].set_ylabel(r"$\sin z$")
    axes[0].legend(loc="lower left", fontsize=9)

    for n in range(5):
        branch = np.linspace(n * pi + 0.007, (n + 1) * pi - 0.007, 1800)
        axes[1].plot(branch / pi, 1 / np.tan(branch), color="#1577b8",
                     linewidth=1.7, label=r"$\cot z$" if n == 0 else None)
        if n > 0:
            axes[1].axvline(n, color="#bbbbbb", linewidth=0.7,
                            linestyle="--")
    axes[1].plot(z / pi, alpha * z, color="#b27710", linewidth=1.7,
                 label=r"$\alpha z$, illustrated with $\alpha=1$")
    roots = np.array([symmetric_root(n, alpha) for n in range(4)])
    axes[1].scatter(roots / pi, alpha * roots, color="#c53b32", s=32,
                    zorder=5, label="One intersection on each positive branch")
    axes[1].axhline(0, color="#777777", linewidth=0.8)
    axes[1].set_ylim(-2, 13)
    axes[1].set_ylabel("Value")
    axes[1].set_title(r"Symmetric modes: $\cot z=\alpha z$, "
                      r"$\alpha=m/(2\rho\ell)$", loc="left")
    axes[1].legend(loc="upper left", fontsize=9)
    axes[1].set_xlabel(r"$z=k\ell$")
    ticks = np.arange(0, 4.5, 0.5)
    axes[1].set_xticks(ticks, ["0", r"$\pi/2$", r"$\pi$", r"$3\pi/2$",
                              r"$2\pi$", r"$5\pi/2$", r"$3\pi$",
                              r"$7\pi/2$", r"$4\pi$"])
    for ax in axes:
        ax.set_facecolor("white")
        ax.set_xlim(0, 4.1)
        ax.grid(alpha=0.18)
    fig.savefig("mathematics-v-string-frequencies.png", dpi=120,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()