codex/past-exam-of-the-mathematics-course-of-the-university-of-cambridge/2005/ia/paper-4-cyclotron-paths.py

past-exam-of-the-mathematics-course-of-the-university-of-cambridge/2005/ia/paper-4-cyclotron-paths.py
"""Original cyclotron-path sketch; Python 3.14, NumPy 2.3.5, Matplotlib 3.10.7.

Outputs paper-4-cyclotron-paths.png to caller CWD; honors MPLCONFIGDIR.
Nondimensional example: initial velocity 1 along x, signed cyclotron rate 1.
"""
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 2, figsize=(7.8, 4.1), facecolor='white')
for ax, damping, title in zip(axes, [0, .09], ['No drag: a circle', 'Weak drag: a logarithmic spiral']):
    t = np.linspace(0, 2*np.pi if damping == 0 else 60, 3500)
    centre = 1/(damping+1j)
    z = centre*(1-np.exp(-(damping+1j)*t))
    ax.plot(z.real, z.imag, color='#1864ab', lw=1.6)
    ax.scatter([0], [0], color='#d95f02', s=30, zorder=5)
    ax.scatter([centre.real], [centre.imag], color='#2b8a3e', s=45, marker='x', zorder=5)
    ax.annotate('Start', xy=(0, 0), xytext=(-.55, .12), fontsize=9,
                color='#d95f02', arrowprops=dict(arrowstyle='->', color='#d95f02'))
    ax.text(centre.real+.08, centre.imag+.08, 'Centre' if damping==0 else 'Limiting point',
            fontsize=9, color='#2b8a3e')
    j = 500 if damping == 0 else 100
    ax.annotate('', xy=(z[j+35].real,z[j+35].imag),
                xytext=(z[j].real,z[j].imag),
                arrowprops=dict(arrowstyle='->', color='#1864ab', lw=1.7))
    ax.set(xlim=(-1.15, 1.2), ylim=(-2.15, .35), aspect='equal', xlabel='$x$', ylabel='$y$', title=title)
    ax.spines[['top', 'right']].set_visible(False)
fig.suptitle('Charged-particle paths for positive $qB$', fontsize=12)
fig.tight_layout()
fig.savefig('paper-4-cyclotron-paths.png', dpi=120, facecolor='white', transparent=False)
plt.close(fig)