codex/past-exam-of-the-mathematics-course-of-the-university-of-cambridge/2004/ia/paper-2-phase-portraits.py

past-exam-of-the-mathematics-course-of-the-university-of-cambridge/2004/ia/paper-2-phase-portraits.py
"""Original conservative phase portraits; Python 3.14, NumPy 2.3.5, Matplotlib 3.10.7.
Writes an opaque PNG basename in caller CWD and honors caller MPLCONFIGDIR.
"""
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

fig,axes=plt.subplots(1,3,figsize=(12,4.3),layout='constrained',facecolor='white')
configs=[('Pendulum: ẍ + cos x = 0',(np.pi/2-.9,5*np.pi/2+.9),(-2.8,2.8),None),('Quartic: λ = 1',(-2.1,1.65),(-2.8,2.8),1),('Quartic: λ = 5/2',(-2.95,.85),(-1.7,1.7),2.5)]
for ax,(title,xlim,vlim,lam) in zip(axes,configs):
    x=np.linspace(*xlim,800)
    v=np.linspace(*vlim,650)
    X,V=np.meshgrid(x,v)
    pot=lambda x: np.sin(x) if lam is None else x**4/4+lam*x**3/3+x*x/2
    force=lambda x:-np.cos(x) if lam is None else -x*(x*x+lam*x+1)
    h=V*V/2+pot(X)
    levels=[-.7,-.2,.4,.8,1.5,2.4] if lam is None else ([.12,.4,.85,1.65,2.7] if lam==1 else [-.6,-.35,-.08,.012,.025,.09,.28,.58])
    ax.contour(X,V,h,levels=levels,colors='#3876a2',linewidths=.85)
    ax.set(xlim=xlim,ylim=vlim,xlabel='x',ylabel='v = ẋ',title=title)
    ax.axhline(0,color='0.65',lw=.65)
    ax.grid(alpha=.13)
    xx,vv=np.meshgrid(np.linspace(*xlim,16),np.linspace(*vlim,15))
    ff=force(xx)
    nn=np.hypot(vv,ff)
    good=nn>1e-8
    ax.quiver(xx[good],vv[good],(vv/np.maximum(nn,1e-8))[good],(ff/np.maximum(nn,1e-8))[good],angles='xy',scale_units='xy',scale=8,color='0.45',width=.0028)
    if lam is None:
        saddles=np.array([np.pi/2,5*np.pi/2]);centers=[3*np.pi/2]
        # Exact saddle and center knots ensure zero separatrix velocity at saddles.
        sx=np.unique(np.r_[x,saddles,centers])
        speed=np.sqrt(np.maximum(2*(1-np.sin(sx)),0))
        for sign in [-1,1]:ax.plot(sx,sign*speed,color='#b84628',lw=1.6)
    elif lam==1:
        saddles=[];centers=[0]
    else:
        saddles=[-.5];centers=[-2,0]
        left,right=(-7-np.sqrt(70))/6,(-7+np.sqrt(70))/6
        hs=7/192
        for lo,hi in [(left,-.5),(-.5,right)]:
            sx=np.unique(np.r_[np.linspace(lo,hi,700),lo,hi])
            speed=np.sqrt(np.maximum(2*(hs-pot(sx)),0))
            speed[[0,-1]]=0  # Exact turning point and saddle values.
            for sign in [-1,1]:ax.plot(sx,sign*speed,color='#b84628',lw=1.6)
    ax.scatter(centers,np.zeros(len(centers)),s=38,c='#1668a7',zorder=6,label='Center')
    if len(saddles):ax.scatter(saddles,np.zeros(len(saddles)),s=40,c='#b84628',marker='x',zorder=6,label='Saddle')
    ax.legend(fontsize=8,loc='lower left')
fig.savefig('paper-2-phase-portraits.png',dpi=130,facecolor='white',transparent=False)
plt.close(fig)