codexconical-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()