codex/past-exam-of-the-physics-1-paper-of-the-university-of-oxford/2015-resit-pebble-terminal-approach.py

past-exam-of-the-physics-1-paper-of-the-university-of-oxford/2015-resit-pebble-terminal-approach.py
"""Original superterminal-pebble plots. Python 3.14, NumPy 2.3.5,
Matplotlib 3.10.7; writes the same-basename PNG in the current directory.
"""

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, "axes.spines.top": False,
                         "axes.spines.right": False, "figure.facecolor": "white"})
    blue, orange, green = "#126782", "#b55d15", "#287d53"
    radius, rho_p, rho_w, coefficient, gravity = 0.02, 2500.0, 1000.0, 0.7, 9.81
    b = 3 * coefficient * rho_w / (8 * rho_p * radius)
    terminal = np.sqrt(gravity * (1 - rho_w / rho_p) / b)
    entry = np.sqrt(2 * gravity * 5)
    depths = np.linspace(0, 1.5, 1200)
    ratio_squared_excess = (entry**2 / terminal**2 - 1) * np.exp(-2 * b * depths)
    speeds = terminal * np.sqrt(1 + ratio_squared_excess)
    # Algebraic form avoids cancellation when the fractional excess is small.
    fractional_excess = ratio_squared_excess / (np.sqrt(1 + ratio_squared_excess) + 1)
    z1 = np.log((entry**2 / terminal**2 - 1) / (1.01**2 - 1)) / (2 * b)
    z5 = np.log((entry**2 / terminal**2 - 1) / (1.05**2 - 1)) / (2 * b)
    fig, (ax, excess) = plt.subplots(1, 2, figsize=(11.3, 5.7), gridspec_kw={"width_ratios": (1, 1.12)})
    ax.plot(depths, speeds, color=blue, lw=2.5, label=r"$v(z)$")
    ax.axhline(terminal, color=orange, lw=1.5, ls="--", label=rf"$v_t={terminal:.3f}$ m/s")
    ax.scatter([0], [entry], color=blue, s=40, zorder=4)
    ax.annotate(rf"Entry: {entry:.2f} m/s", (0, entry), (0.2, 8.2),
                arrowprops={"arrowstyle": "->", "color": blue}, color=blue)
    ax.set(xlim=(0, 1.5), ylim=(0, 10.5), xlabel="Underwater depth z (m)",
           ylabel="Downward speed (m/s)", title="The pebble decelerates after entry")
    ax.legend(frameon=False, loc="upper right")
    ax.grid(alpha=0.16)
    excess.semilogy(depths, fractional_excess, color=blue, lw=2.5)
    for tolerance, depth, color, text_x, text_y in ((0.05, z5, orange, 0.58, 0.18), (0.01, z1, green, 0.15, 0.0015)):
        excess.axhline(tolerance, color=color, lw=1.1, ls="--")
        excess.plot([depth, depth], [1e-6, tolerance], color=color, lw=1, ls=":")
        excess.scatter([depth], [tolerance], color=color, s=40, zorder=4)
        excess.annotate(f"{100*tolerance:.0f}% excess at {depth:.3f} m", (depth, tolerance),
                        (text_x, text_y), color=color, fontsize=11,
                        arrowprops={"arrowstyle": "->", "color": color})
    excess.set(xlim=(0, 1.5), ylim=(1e-6, 20), xlabel="Underwater depth z (m)",
               ylabel=r"Fractional speed excess $(v-v_t)/v_t$", title="A tolerance makes ‘reached’ precise")
    excess.grid(alpha=0.16, which="major")
    fig.suptitle("Pebble settling under quadratic drag and buoyancy", fontsize=17, y=0.97)
    fig.text(0.5, 0.13, r"$v^2(z)=v_t^2+(u^2-v_t^2)e^{-2bz}$,  $b=5.25$ m$^{-1}$; exact equality $v=v_t$ is asymptotic.",
             ha="center", fontsize=13)
    fig.text(0.5, 0.06, "Model: 5 m air fall, no air drag or entry losses; constant water drag coefficient C = 0.7.",
             ha="center", fontsize=11, color="#596672")
    fig.text(0.5, 0.022, "Time to 5 m water depth: 4.43 s; total time including the air fall: 5.44 s.",
             ha="center", fontsize=11, color="#596672")
    fig.subplots_adjust(left=0.075, right=0.975, bottom=0.26, top=0.84, wspace=0.29)
    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()