codexpast-exam-of-the-mathematics-course-of-the-university-of-cambridge2008iipaper-1-phase-planes.py
past-exam-of-the-mathematics-course-of-the-university-of-cambridge/2008/ii/paper-1-phase-planes.py
"""Phase portraits and stable-manifold basin; output only to caller CWD."""
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.path import Path as PolygonPath
def rhs(v,k):
x,y=v
return np.array([y,-x+x*x-k*y])
def trace(v,k,dt,steps=18000):
out=[v.copy()]
for _ in range(steps):
a=rhs(v,k);b=rhs(v+.5*dt*a,k);c=rhs(v+.5*dt*b,k);d=rhs(v+dt*c,k)
w=v+dt*(a+2*b+2*c+d)/6
if w[0]>4.5:
frac=(4.5-v[0])/(w[0]-v[0]);out.append(v+frac*(w-v));break
out.append(w.copy());v=w
if abs(v[0])>12 or abs(v[1])>18:break
return np.array(out)
fig,axs=plt.subplots(1,2,figsize=(11,5.2),dpi=110)
fig.patch.set_facecolor('white')
x=np.linspace(-1.25,2.55,220);y=np.linspace(-2.7,2.7,220);xx,yy=np.meshgrid(x,y)
for ax,k in zip(axs,[0,.1]):
ax.set_facecolor('white');ax.streamplot(x,y,yy,-xx+xx*xx-k*yy,density=.65,color='#b5b5b5',linewidth=.5,arrowsize=.75)
ax.plot(0,0,'o',color='#1565c0');ax.plot(1,0,'o',color='#c62828')
ax.axhline(0,color='black',lw=.5);ax.axvline(0,color='black',lw=.5)
ax.set(xlim=(x[0],x[-1]),ylim=(y[0],y[-1]),xlabel='x',ylabel='y',title=f'k = {k:g}')
ax.annotate('saddle',(1,0),xytext=(9,11),textcoords='offset points')
# Conservative separatrices, exact energy level 1/6.
a=np.linspace(-.5,1,650);z=(1-a)*np.sqrt(np.maximum((2*a+1)/3,0))
for sign in [-1,1]:axs[0].plot(a,sign*z,color='#c62828',lw=2)
a=np.linspace(1,2.55,300);z=(a-1)*np.sqrt((2*a+1)/3)
for sign in [-1,1]:axs[0].plot(a,sign*z,color='#c62828',lw=2)
energy=yy*yy/2+xx*xx/2-xx**3/3
axs[0].contour(xx,yy,energy,levels=[.025,.07,.12,.155],colors='#1565c0',linewidths=.7)
axs[0].annotate('centre',(0,0),xytext=(-52,-20),textcoords='offset points')
# Numerical saddle manifolds of damped system, using eigenvector starts.
k=.1;ls=(-k-np.sqrt(k*k+4))/2;lu=(-k+np.sqrt(k*k+4))/2;eps=1e-5
left=trace(np.array([1-eps,-ls*eps]),k,-.004)
right=trace(np.array([1+eps,ls*eps]),k,-.004)
poly=np.vstack([left[::-1],right])
assert PolygonPath(poly).contains_point((0,0))
assert not PolygonPath(poly).contains_point((2,0))
inside=PolygonPath(poly).contains_points(np.c_[xx.ravel(),yy.ravel()]).reshape(xx.shape)
axs[1].contourf(xx,yy,inside.astype(int),levels=[.5,1.5],colors=['#dbeafe'],alpha=.85,zorder=0)
for v in [left,right]:axs[1].plot(v[:,0],v[:,1],color='#c62828',lw=2)
for sign in [-1,1]:
v=trace(np.array([1+sign*eps,sign*lu*eps]),k,.004,steps=17000)
axs[1].plot(v[:,0],v[:,1],color='#ef6c00',lw=1.4,ls='--')
axs[1].annotate('stable focus',(0,0),xytext=(-76,17),textcoords='offset points')
axs[1].annotate('Basin of the origin',(.5,-.3),xytext=(-.7,-1.5),fontsize=9,color='#1565c0',arrowprops={'arrowstyle':'->','color':'#1565c0','lw':.8})
fig.suptitle('Quadratic oscillator: saddle separatrices and basin (shaded)',fontsize=12)
fig.tight_layout()
fig.savefig(Path.cwd()/'paper-1-phase-planes.png',facecolor='white',transparent=False)
plt.close(fig)