codexduffing-homoclinic-loops.py
duffing-homoclinic-loops.py
"""Draw the exact separatrices of x'' = x - x**3.
Build: make _media/duffing-homoclinic-loops.png
Python 3.14.4, NumPy 2.3.5 and Matplotlib 3.10.7.
Writes an opaque same-basename PNG to the working directory.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
def loop(t):
x = np.sqrt(2) / np.cosh(t)
return x, -x * np.tanh(t)
def main():
fig, ax = plt.subplots(figsize=(8, 5), dpi=110, facecolor="white")
x = np.linspace(-1.85, 1.85, 501)
v = np.linspace(-1.15, 1.15, 401)
xx, vv = np.meshgrid(x, v)
energy = vv**2 / 2 - xx**2 / 2 + xx**4 / 4
cs = ax.contour(xx, vv, energy, levels=[-.20, -.08, .14],
colors="#94a3b8", linewidths=1.0, linestyles="solid")
ax.clabel(cs, fmt=lambda h: f"H={h:.2f}", fontsize=9)
t = np.linspace(-9, 9, 1501)
lx, lv = loop(t)
for sign in [-1, 1]:
ax.plot(sign * lx, sign * lv, color="#c75b20", lw=2.5)
for start in [-1.6, .4]:
a = np.array(loop(start)) * sign
b = np.array(loop(start + .24)) * sign
ax.annotate("", xy=b, xytext=a,
arrowprops=dict(arrowstyle="->", color="#c75b20", lw=2))
ax.plot(0, 0, "x", color="#111827", ms=10, mew=2)
ax.annotate("saddle", xy=(0, 0), xytext=(0, .37), ha="center",
arrowprops=dict(arrowstyle="->", color="#111827"))
ax.plot([-1, 1], [0, 0], "o", color="#2563a0", ms=6)
ax.text(-1, -.14, "center", ha="center", color="#2563a0")
ax.text(1, -.14, "center", ha="center", color="#2563a0")
ax.set(xlim=(-1.85, 1.85), ylim=(-1.15, 1.15), xlabel=r"$x$",
ylabel=r"$v=\dot x$", title=r"$\ddot x=x-x^3$: two homoclinic loops at $H=0$")
ax.set_aspect("equal")
ax.grid(alpha=.15)
fig.tight_layout()
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), facecolor="white")
plt.close(fig)
if __name__ == "__main__":
main()