codexpast-exam-of-the-mathematics-course-of-the-university-of-cambridge2018iiipaper-316-secular-rings.py
past-exam-of-the-mathematics-course-of-the-university-of-cambridge/2018/iii/paper-316-secular-rings.py
#!/usr/bin/env python3
"""Original planetary-dynamics figure; outputs its same-basename PNG to CWD.
Tested Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7+dfsg1.
Dependencies match the repository pyproject; no external visual assets.
"""
from pathlib import Path
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Arc
plt.rcParams.update({'font.size':10,'axes.spines.top':False,'axes.spines.right':False})
blue='#22577a'; orange='#c66a24'; green='#387c58'; gray='#666666'
def finish(fig):
fig.savefig(Path.cwd()/Path(__file__).with_suffix('.png').name,dpi=100,facecolor='white',transparent=False)
plt.close(fig)
fig,axes=plt.subplots(1,3,figsize=(11.4,4.4),dpi=100)
theta=np.linspace(0,2*np.pi,600)
ax=axes[0];zf=.14;ep=.075;ang=1.05;z=zf+ep*np.exp(1j*ang)
ax.plot(zf+ep*np.cos(theta),ep*np.sin(theta),color=blue)
ax.annotate('',(zf,0),(0,0),arrowprops={'arrowstyle':'->','color':green,'lw':2})
ax.annotate('',(z.real,z.imag),(zf,0),arrowprops={'arrowstyle':'->','color':orange,'lw':2})
ax.plot(z.real,z.imag,'o',color=blue);ax.text(.055,-.018,r'$z_f$',color=green);ax.text(.18,.025,r'$z_p$',color=orange)
ax.text(z.real+.005,z.imag,r'$z$');ax.set(xlim=(-.035,.25),ylim=(-.11,.11),xlabel='Re z',ylabel='Im z',title='Proper + forced eccentricity');ax.axhline(0,color=gray,lw=.5);ax.axvline(0,color=gray,lw=.5);ax.set_aspect('equal')
ax=axes[1];ef=.12;ep=.065
outer=np.c_[np.cos(theta)*(1+ep)-ef,np.sin(theta)*(1+ep)]
inner=np.c_[np.cos(theta)*(1-ep)-ef,np.sin(theta)*(1-ep)]
ax.fill(np.r_[outer[:,0],inner[::-1,0]],np.r_[outer[:,1],inner[::-1,1]],color='#d4e2ec')
for phi in np.linspace(0,2*np.pi,12,endpoint=False):
center=np.array([-ef-ep*np.cos(phi),-ep*np.sin(phi)])
ax.plot(np.cos(theta)+center[0],np.sin(theta)+center[1],color=blue,alpha=.27,lw=.8)
ax.plot(-ef,0,'+',color=green,ms=8);ax.plot(0,0,'*',color='#bb921d',ms=11)
ax.set(title='Common a; random proper phases');ax.text(-.17,-1.25,'Constant width 2 a e_p',ha='center',fontsize=9)
ax=axes[2]
for a in np.linspace(.92,1.08,9):ax.plot(a*(np.cos(theta)-ef),a*np.sin(theta),color=orange,alpha=.65,lw=1)
ax.plot(0,0,'*',color='#bb921d',ms=11);ax.set(title='Range of a; constant forced e')
ax.text(-.17,-1.25,'Wider at apocentre (left)',ha='center',fontsize=9)
for ax in axes[1:]:
ax.set_aspect('equal');ax.set(xlim=(-1.48,1.3),ylim=(-1.37,1.3),xlabel='x / a',ylabel='y / a')
fig.suptitle('First-order secular geometry; forced pericentre points right',fontsize=11)
fig.tight_layout(rect=(0,0,1,.94));finish(fig)