scipy.integrate.solve_ivp() and matplotlib.
Module 1: Low-Dimensional Dynamics
This page is optional in Session 8. It applies the CDIMA pipeline to a second oscillator. Session 9 returns to it in Relaxation Oscillations.
The Van der Pol oscillator, introduced by van der Pol for electrical circuits with triodes (vanderpol1926relaxation?), is a prototypical nonlinear system that exhibits self-sustained oscillations. For a wide range of initial conditions, trajectories converge to a stable limit cycle.
In the form we will use in this course, the system is written as (Strogatz 2024, chap. 7.5):
\[\begin{aligned} \dot x &= \mu\,(y - f(x)) \\ \dot y &= -\frac{x}{\mu} \end{aligned} \tag{1}\]
where \(\mu>0\) controls the strength of the nonlinearity (and the fast-slow character for large \(\mu\)), and
\[ f(x) = \frac{x^3}{3} - x. \tag{2}\]
The nullclines are especially simple in this model:
\[ \dot x = 0 \quad \Longrightarrow \quad y = \frac{x^3}{3} - x, \qquad \dot y = 0 \quad \Longrightarrow \quad x = 0. \]
They intersect at the origin, so \((0,0)\) is the equilibrium point. For the parameter regime used in this session, the main long-run object of interest is not the equilibrium itself, but the attracting limit cycle around it.
scipy.integrate.solve_ivp() and matplotlib.
Complete the Van der Pol vector field Equation 1.
def van_der_pol(t, xy, mu=1.0):
x, y = xy
dxdt = # TODO
dydt = # TODO
return np.array([dxdt, dydt])def van_der_pol(t, xy, mu=1.0):
x, y = xy
fx = x**3 / 3 - x
dxdt = mu * (y - fx)
dydt = -x / mu
return np.array([dxdt, dydt])Follow the same pipeline as in CDIMA: Phase Plane: define the right-hand side, solve the initial value problem, compute nullclines, and then animate the resulting trajectory.
For every \(\mu > 0\) the Van der Pol system has a unique, stable limit cycle; this follows from Liénard’s theorem (Strogatz 2024, sec. 7.4).
Plot this limit cycle as an \(x(t)\) against \(t\) plot for \(\mu = 3\) and the initial condition \((2, -2)\), and verify that it matches the phase plane trajectories. Figure 2 shows the expected result.
sol = solve_ivp(van_der_pol, [0, 40], (2, -2), args=(3,), t_eval=np.linspace(0, 40, 4000))
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(sol.t, sol.y[0])
ax.set_xlabel("Time (t)")
ax.set_ylabel("x(t)")
plt.show()The maximum of \(x(t)\) is close to 2, the largest \(|x|\) on the closed orbit in Figure 1.
scipy.integrate.solve_ivp() and matplotlib.
The amplitude and period of the limit cycle depend on \(\mu\).
This is the characteristic relaxation-oscillation picture.
Play with the parameter \(\mu\) (try 0.5, 1, 3 and 10) to see how it changes the shape of the limit cycle, its amplitude, its period and the transient dynamics.
The amplitude of \(x\) stays close to 2 for every \(\mu\). The period grows with \(\mu\): about 6.4 for \(\mu = 0.5\) and 19 for \(\mu = 10\). For large \(\mu\) it approaches \((3 - 2\ln 2)\mu\) (Strogatz 2024, sec. 7.5). Small \(\mu\) gives a nearly sinusoidal oscillation; large \(\mu\) gives slow drifts along the cubic nullcline and fast jumps between its branches.
for mu_test in [0.5, 1, 3, 10]:
t_end = 40 + 30 * mu_test
s = solve_ivp(van_der_pol, (0, t_end), [2, -2], args=(mu_test,),
t_eval=np.linspace(0, t_end, 200001), rtol=1e-9, atol=1e-11)
late = s.t > t_end / 2
x_late, t_late = s.y[0][late], s.t[late]
up = np.where((x_late[:-1] < 0) & (x_late[1:] >= 0))[0]
print(f"mu = {mu_test}: amplitude {x_late.max():.2f}, period {np.diff(t_late[up]).mean():.2f}, "
f"(3 - 2 ln 2) mu = {(3 - 2 * np.log(2)) * mu_test:.2f}")mu = 0.5: amplitude 2.00, period 6.38, (3 - 2 ln 2) mu = 0.81
mu = 1: amplitude 2.01, period 6.66, (3 - 2 ln 2) mu = 1.61
mu = 3: amplitude 2.02, period 8.86, (3 - 2 ln 2) mu = 4.84
mu = 10: amplitude 2.01, period 19.08, (3 - 2 ln 2) mu = 16.14