codexpast-exam-of-the-mathematics-course-of-the-university-of-cambridge2001iiipaper-51-bogdanov-takens-portraits.py
past-exam-of-the-mathematics-course-of-the-university-of-cambridge/2001/iii/paper-51-bogdanov-takens-portraits.py
"""Original bifurcation and numerical phase diagrams.
Python 3.14, NumPy 2.3, Matplotlib 3.10; only basename PNG to caller CWD.
Respects caller MPLCONFIGDIR. Phase panels use the exact rescaled field
u'=v, v'=u²-sign(lambda)+0.1(beta+u)v. The repelling cycle is
computed by backward RK4 integration, rather than drawn as an ellipse.
"""
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
EPS=.1
def field(p,beta,sgn=1):
u,v=p
return np.array([v,u*u-sgn+EPS*(beta+u)*v])
def rk4(p,dt,beta,sgn=1):
k1=field(p,beta,sgn);k2=field(p+dt*k1/2,beta,sgn)
k3=field(p+dt*k2/2,beta,sgn);k4=field(p+dt*k3,beta,sgn)
return p+dt*(k1+2*k2+2*k3+k4)/6
def path(seed,dt,beta,sgn=1,steps=6000):
p=np.array(seed,dtype=float);pts=[p.copy()]
for _ in range(steps):
p=rk4(p,dt,beta,sgn)
if not np.all(np.isfinite(p)) or abs(p[0])>3.3 or abs(p[1])>3.5:break
pts.append(p.copy())
return np.array(pts)
def periodic_cycle(beta=.85):
pts=path([-.85,0],-.025,beta,steps=80000)
# Cross u=-1 with positive v: use interpolated sections to check convergence.
indices=np.flatnonzero((pts[:-1,0]>-1)&(pts[1:,0]<=-1)&(pts[1:,1]>0))
if len(indices)<10:raise RuntimeError('No converged periodic return')
values=[]
for i in indices[-10:]:
lo,hi=0.,1.
for _ in range(36):
mid=(lo+hi)/2
if rk4(pts[i],-.025*mid,beta)[0]>-1:lo=mid
else:hi=mid
values.append(rk4(pts[i],-.025*(lo+hi)/2,beta)[1])
if np.ptp(values)>2e-5:raise RuntimeError('Periodic section has not converged')
return pts[indices[-2]:indices[-1]+2],float(np.ptp(values)),float(np.mean(values))
if __name__=='__main__':
cycle,spread,cross=periodic_cycle()
fig=plt.figure(figsize=(10.2,10.0),dpi=130,facecolor='white')
grid=fig.add_gridspec(3,2,height_ratios=[.9,1,1],hspace=.64,wspace=.24)
ax=fig.add_subplot(grid[0,:]);lam=np.linspace(0,.00022,500);a=np.sqrt(lam)
ax.plot(lam,a,color='#a83339',lw=2,label='Hopf: μ = √λ')
ax.plot(lam,5*a/7,'--',color='#256daa',lw=2,label='Saddle loop: leading μ = (5/7)√λ')
ax.axvline(0,color='black',lw=1.7,label='Saddle-node')
ax.scatter([0],[0],color='black',s=30)
cases=[(-1,.8,'No equilibria'),(1,.5,'Stable focus, no nearby cycle'),(1,.85,'Stable focus + repelling cycle'),(1,1.3,'Unstable focus, no nearby cycle')]
for i,(sgn,beta,title) in enumerate(cases):
ax.scatter([sgn*1e-4],[beta*.01],s=24,color='black')
ax.annotate(str(i+1),(sgn*1e-4,beta*.01),xytext=(5,5),textcoords='offset points')
ax.set(xlim=(-.00014,.00023),ylim=(-.002,.018),xlabel='λ',ylabel='μ',title='Nearby bifurcation curves; global curve shown to leading order')
ax.ticklabel_format(style='sci',axis='x',scilimits=(0,0));ax.legend(loc='upper left',fontsize=8);ax.grid(alpha=.15)
uu=np.linspace(-2.65,1.65,120);vv=np.linspace(-2.4,2.4,110);U,V=np.meshgrid(uu,vv)
for i,(sgn,beta,title) in enumerate(cases):
panel=fig.add_subplot(grid[1+i//2,i%2])
panel.streamplot(uu,vv,V,U*U-sgn+EPS*(beta+U)*V,color='#a5b4b9',density=.85,linewidth=.65,arrowsize=.8)
if sgn==1:
panel.scatter([-1],[0],s=35,color='#224b6e' if beta<1 else '#a83339',zorder=5)
panel.scatter([1],[0],marker='x',s=55,color='black',zorder=5)
roots=np.linalg.eig(np.array([[0,1],[2,EPS*(beta+1)]]))
for eig,vec in zip(roots[0],roots[1].T):
for sign in [-1,1]:
points=path(np.array([1,0])+sign*.002*vec,.015 if eig>0 else -.015,beta,steps=1200)
panel.plot(points[:,0],points[:,1],color='#94653e',lw=1.15)
if i==2:
panel.plot(cycle[:,0],cycle[:,1],color='#aa2d68',lw=2)
panel.text(-2.5,1.95,'Repelling periodic orbit',fontsize=8,color='#aa2d68')
panel.tick_params(labelsize=9)
panel.set(xlim=(-2.65,1.65),ylim=(-2.4,2.4),xlabel='u = x/√|λ|',ylabel='v = y/|λ|^(3/4)',title=f'{i+1}. {title}\nβ = μ/√|λ| = {beta}, ε = 0.1')
for panel in fig.axes:
panel.title.set_fontsize(11);panel.xaxis.label.set_fontsize(9);panel.yaxis.label.set_fontsize(9)
fig.savefig('paper-51-bogdanov-takens-portraits.png',facecolor='white',transparent=False);plt.close(fig)
print(f'Backward RK4 periodic section spread={spread:.3g}, v crossing={cross:.8g}')