codex/past-exam-of-the-physics-1-paper-of-the-university-of-oxford/2015-impulsively-struck-stick-clearance.py

past-exam-of-the-physics-1-paper-of-the-university-of-oxford/2015-impulsively-struck-stick-clearance.py
"""Original struck-stick geometry; Python 3.14, NumPy 2.3.5,
Matplotlib 3.10.7. Writes only the same-basename PNG in the CWD.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np


def main():
    plt.rcParams.update({"font.size": 12, "figure.facecolor": "white"})
    fig, axes = plt.subplots(1, 3, figsize=(11.8, 5.3))
    critical = 1 / (2 * np.pi)
    for ax, beta, heading in zip(axes, [0.10, critical, 0.22], ["Contact", "Grazing limit", "Clearance"]):
        for psi, color, width, label in [(0, "#b8bfc5", 2, "Initially"),
                                          (np.pi/4, "#b55d15", 2, r"$\psi=\pi/4$"),
                                          (np.pi/2, "#126782", 3, r"$\psi=\pi/2$")]:
            centre = np.array([2 * beta * psi, 0.0])
            direction = np.array([-np.sin(psi), np.cos(psi)])
            endpoints = np.array([centre - 0.5 * direction, centre + 0.5 * direction])
            ax.plot(endpoints[:, 0], endpoints[:, 1], color=color, lw=width, label=label)
            ax.scatter(*centre, color=color, s=20, zorder=4)
        ax.scatter([0], [0], color="#b3443c", s=75, marker="x", lw=2, zorder=6)
        ax.text(-0.12, -0.14, "Pole", color="#b3443c", ha="right", fontsize=11)
        ax.axhline(0, color="#d2d7dc", ls=":", lw=0.7, zorder=0)
        trailing = np.pi * beta - 0.5
        ax.scatter([trailing], [0], facecolors="white", edgecolors="#126782", s=55, lw=1.7, zorder=5)
        ax.text(0.38, 0.60, heading, ha="center", fontsize=14,
                color="#287d53" if beta > critical else "#b3443c")
        ax.text(0.38, 0.43, rf"$\beta={beta:.5f}$", ha="center", fontsize=12)
        ax.text(0.38, -0.61, rf"Trailing end: $x/l={trailing:.3f}$", ha="center", fontsize=11)
        ax.set(xlim=(-0.45, 1.20), ylim=(-0.78, 0.77), aspect="equal")
        ax.set_axis_off()
    handles, labels = axes[0].get_legend_handles_labels()
    fig.legend(handles, labels, loc="upper center", bbox_to_anchor=(0.5, 0.88), ncol=3, frameon=False)
    fig.suptitle("An end impulse: translation must outrun the first quarter-turn", fontsize=17, y=0.97)
    fig.text(0.5, 0.14, r"$V=J/m$,  $\Omega=J/(2\beta ml)$,  $x_{\mathrm{trail}}(\pi/2)=l(\pi\beta-1/2)$",
             ha="center", fontsize=14)
    fig.text(0.5, 0.065, r"Strict clearance: $\beta>1/(2\pi)$; equality just grazes the ideal point pole.",
             ha="center", fontsize=13)
    fig.text(0.5, 0.02, "Snapshots assume free motion up to contact; no post-collision trajectory is implied.",
             ha="center", fontsize=10, color="#596672")
    fig.subplots_adjust(left=0.025, right=0.985, bottom=0.22, top=0.79, wspace=0.12)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
                dpi=140, facecolor="white", transparent=False, metadata={"Software": "Matplotlib"})
    plt.close(fig)


if __name__ == "__main__":
    main()