codex/conical-pendulum-period.py

conical-pendulum-period.py
"""Plot ideal predictions for stipulated parameters, never measured data.

Tested with Python 3.14.4, matplotlib 3.10.7, and numpy 2.3.5.
Run from _media with MPLCONFIGDIR=/tmp/codex-wiki-matplotlib.
The matching PNG basename is written to the current working 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, "figure.facecolor": "white"})
    blue, orange, green = "#235d9f", "#c65b16", "#167146"
    length, gravity = 1.0, 9.81
    p0 = 2 * np.pi * np.sqrt(length / gravity)
    degrees = np.linspace(0, 80, 401)
    periods = p0 * np.sqrt(np.cos(np.deg2rad(degrees)))
    sample_degrees = np.array([15.0, 40.0, 60.0])
    sample_drop = length * np.cos(np.deg2rad(sample_degrees))
    sample_period = 2 * np.pi * np.sqrt(sample_drop / gravity)
    fig, axes = plt.subplots(1, 2, figsize=(10.4, 4.8))
    fig.subplots_adjust(left=0.08, right=0.97, bottom=0.19, top=0.74, wspace=0.30)
    fig.suptitle("Conical pendulum: period and a testable straight line", fontsize=17, y=0.96)
    fig.text(0.5, 0.87, r"Ideal predictions, not measured data: $\ell=1.00$ m, $g=9.81$ m/s$^2$", ha="center")
    for ax in axes:
        ax.spines[["top", "right"]].set_visible(False)
        ax.grid(alpha=0.20)
        ax.set_axisbelow(True)

    ax = axes[0]
    ax.set_title("Wider cone, shorter period", fontsize=13, pad=14)
    ax.plot(degrees, periods, color=blue, lw=2.5)
    ax.axhline(p0, color=green, ls="--", lw=1.6)
    ax.plot(0, p0, "o", mfc="white", mec=blue, ms=7, zorder=5)
    ax.scatter(sample_degrees, sample_period, color=orange, s=35, zorder=5)
    ax.text(8, p0 + 0.035, r"Small-angle limit $P_0=2.006$ s", color=green, fontsize=11)
    ax.text(26, 1.15, r"$P=P_0\sqrt{\cos\theta}$", color=blue, fontsize=14)
    ax.annotate("40°: 1.756 s", xy=(40, sample_period[1]), xytext=(47, 1.90), fontsize=11,
                arrowprops={"arrowstyle": "->", "color": orange}, color=orange)
    ax.set(xlim=(-1, 81), ylim=(0.72, 2.17), xlabel=r"Angle from downward vertical $\theta$ (degrees)", ylabel="Revolution period P (s)")
    ax.set_xticks([0, 20, 40, 60, 80])

    ax = axes[1]
    drops = np.linspace(0, length, 401)
    ax.plot(drops, 4 * np.pi**2 * drops / gravity, color=blue, lw=2.5)
    ax.scatter(sample_drop, sample_period**2, color=orange, s=35, zorder=5)
    ax.plot(length, p0**2, "o", mfc="white", mec=blue, ms=7, zorder=5)
    for angle, drop, period in zip(sample_degrees, sample_drop, sample_period):
        ax.annotate(f"{angle:.0f}°", (drop, period**2), xytext=(-9, 9), textcoords="offset points", ha="right", color=orange, fontsize=11)
    ax.text(0.06, 3.3, r"$P^2=\frac{4\pi^2}{g}h$", color=blue, fontsize=16)
    ax.text(0.07, 2.63, "Slope = 4.024 s²/m", color=blue, fontsize=11)
    ax.set_title("Measure height and timing independently", fontsize=13, pad=14)
    ax.set(xlim=(0, 1.06), ylim=(0, 4.5), xlabel=r"Pivot-to-orbit vertical drop $h$ (m)", ylabel="Squared period P² (s²)")
    fig.text(0.5, 0.055, "Orange points: chosen example angles. Open circles: zero-radius limit, not a revolution at rest.", ha="center", fontsize=11)
    fig.savefig(Path(__file__).with_suffix(".png").name, dpi=110, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()