codexpendulum-period-scaling.py
pendulum-period-scaling.py
"""Generate pendulum-period-scaling.png in cwd.
Tested with Python 3.14.4, numpy 2.3.5, matplotlib 3.10.7.
Small-angle periods are analytic. Finite-amplitude periods use Gaussian
quadrature of the exact ideal-pendulum elliptic integral, not simulated data.
"""
from pathlib import Path
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
def period_ratio(theta):
nodes,weights=np.polynomial.legendre.leggauss(256)
phi=(nodes+1)*np.pi/4
k=np.sin(np.asarray(theta)[...,None]/2)
integral=np.sum(weights*(np.pi/4)/np.sqrt(1-k*k*np.sin(phi)**2),axis=-1)
return 2*integral/np.pi
def main():
g=9.81
length=np.linspace(.1,4.2,400)
degrees=np.linspace(0,150,400)
ratio=period_ratio(np.deg2rad(degrees))
assert np.isclose(period_ratio(0),1,atol=1e-14)
assert np.isclose(period_ratio(np.pi/2),1.1803405990160962,atol=1e-12)
fig,axes=plt.subplots(1,2,figsize=(10.5,4.9),dpi=120,facecolor='white')
axes[0].plot(length,2*np.pi*np.sqrt(length/g),color='#2878ad',lw=2.3)
for ell in (1,4):
t=2*np.pi*np.sqrt(ell/g)
axes[0].scatter(ell,t,c='#c05a2a',s=50,zorder=4)
axes[0].annotate(f'{ell} m: {t:.3f} s',(ell,t),xytext=(-5,-25) if ell==4 else (10,9),
textcoords='offset points',ha='right' if ell==4 else 'left',fontsize=11)
axes[0].set(xlim=(0,4.4),ylim=(0,4.7),xlabel='Length ℓ (m)',ylabel='Small-angle period (s)',
title=r'Length scaling: $T_0=2\pi\sqrt{\ell/g}$')
axes[1].plot(degrees,ratio,color='#24805e',lw=2.3,label='Exact ideal-pendulum period ratio')
axes[1].axhline(1,color='#2878ad',lw=1.4,ls='--',label='Small-angle approximation')
for deg in (10,60,90,120):
r=float(period_ratio(np.deg2rad(deg)))
axes[1].scatter(deg,r,s=30,c='#c05a2a',zorder=4)
axes[1].annotate(f'{deg}°: {r:.4f}',(deg,r),xytext=(5,8),textcoords='offset points',fontsize=10)
axes[1].set(xlim=(0,155),ylim=(.96,1.8),xlabel='Initial amplitude θ₀ (degrees)',ylabel=r'$T/T_0$',
title='Amplitude reveals the approximation limit')
axes[1].legend(loc='upper left',fontsize=9)
for ax in axes:
ax.set_facecolor('white');ax.grid(alpha=.16)
ax.spines['top'].set_visible(False);ax.spines['right'].set_visible(False)
fig.suptitle('One ideal model predicts scaling and its small-angle correction',fontsize=15)
fig.text(.5,.026,'Assume a point mass, massless fixed-length support, no drag, and '+r'$g=9.81\ \mathrm{m/s^2}$'+'.\n'
'Right: exact periods from 256-point quadrature; these curves are not experimental measurements.',ha='center',fontsize=9)
fig.tight_layout(rect=(0,.085,1,.925))
fig.savefig(Path.cwd()/'pendulum-period-scaling.png',facecolor='white',transparent=False)
plt.close(fig)
for deg in (10,60,90,120):
print(f'Amplitude {deg} degrees: T/T0={float(period_ratio(np.deg2rad(deg))):.10f}')
print(f'Lengths 1m and4m: {2*np.pi/np.sqrt(g):.10f}s, {4*np.pi/np.sqrt(g):.10f}s')
if __name__=='__main__':
main()