codexhopf-criticality-radius-branches.py
hopf-criticality-radius-branches.py
"""Compute cubic Hopf radius branches and drift; output PNG basename to CWD.
Run from _media with MPLCONFIGDIR=/tmp/codex-wiki-matplotlib.
Tested with Python 3.14.4, NumPy 2.3.5, and Matplotlib 3.10.7.
Dimensionless cubic models only: no measured data or global prediction.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.lines import Line2D
def main():
plt.rcParams.update({"font.size": 10, "axes.spines.top": False,
"axes.spines.right": False, "figure.facecolor": "white",
"axes.facecolor": "white"})
fig, axes = plt.subplots(2, 2, figsize=(9.0, 6.0))
fig.subplots_adjust(left=0.085, right=0.975, bottom=0.17, top=0.79,
wspace=0.27, hspace=0.64)
stable, unstable = "#187154", "#bb4937"
fig.suptitle("The cubic sign selects the side and stability of the small cycle", y=0.975, fontsize=13)
fig.text(0.5, 0.92, "Radius branches above; computed radial drift below. Dimensionless models, not measured data.",
ha="center", fontsize=9)
handles = [Line2D([0], [0], color=stable, lw=2.5, label="attracting branch"),
Line2D([0], [0], color=unstable, lw=2.5, ls="--", label="repelling branch")]
fig.legend(handles=handles, loc="upper center", bbox_to_anchor=(0.5, 0.9), ncol=2,
frameon=False, fontsize=9)
side = np.linspace(0, 0.4, 500)
r = np.linspace(0, 0.7, 500)
for col, sign in enumerate((-1, 1)):
ax = axes[0, col]
ax.axvline(0, color="#bbc3c9", lw=1, zorder=0)
ax.plot((-0.4, 0), (0, 0), color=stable, lw=3)
ax.plot((0, 0.4), (0, 0), color=unstable, ls="--", lw=3)
branch_mu = side if sign < 0 else -side
ax.plot(branch_mu, np.sqrt(side), color=stable if sign < 0 else unstable,
ls="-" if sign < 0 else "--", lw=2.5)
for mu in (-0.2, 0.2):
for rad in (0.13, 0.34, 0.57):
drift = mu * rad + sign * rad**3
direction = 1 if drift > 0 else -1
ax.annotate("", xy=(mu, rad + direction * 0.045),
xytext=(mu, rad - direction * 0.045),
arrowprops={"arrowstyle": "->", "color": "#59616a", "lw": 1.1})
ax.plot(0, 0, "o", ms=4.5, color="#333333", clip_on=False, zorder=5)
ax.set(xlim=(-0.42, 0.42), ylim=(-0.04, 0.70), xlabel=r"parameter $\mu$", ylabel=r"radius $r$")
ax.set_title(("Supercritical: " + r"$\dot r=\mu r-r^3$") if sign < 0
else ("Subcritical: " + r"$\dot r=\mu r+r^3$"), fontsize=11)
ax.text(0.96 if sign < 0 else 0.04, 0.94,
r"$r_*=\sqrt{\mu}$" if sign < 0 else r"$r_*=\sqrt{-\mu}$",
transform=ax.transAxes, ha="right" if sign < 0 else "left", fontsize=11)
ax = axes[1, col]
ax.axhline(0, color="#bbc3c9", lw=1)
for mu, color in ((-0.2, "#5365af"), (0.2, "#c37822")):
ax.plot(r, mu * r + sign * r**3, color=color, lw=2, label=rf"$\mu={mu:g}$")
if -mu / sign > 0:
ax.plot(np.sqrt(-mu / sign), 0, "o", ms=6, mew=1.5,
mec=color, mfc=color if sign < 0 else "white", zorder=5)
ax.set(xlim=(0, 0.70), ylim=(-0.51, 0.51), xlabel=r"radius $r$", ylabel=r"radial drift $\dot r$")
ax.set_yticks((-0.4, 0, 0.4))
ax.set_title("Positive drift grows the radius; negative drift shrinks it", fontsize=9, pad=8)
ax.legend(frameon=False, fontsize=9, loc="lower left" if sign < 0 else "upper left")
fig.text(0.5, 0.075, "Upper arrows show radial motion at fixed parameter. The zero-radius branch is an equilibrium.",
ha="center", fontsize=9)
fig.text(0.5, 0.035, "Outward subcritical drift leaves the local neighbourhood; the cubic model supplies no distant attractor.",
ha="center", fontsize=9)
fig.savefig(Path(__file__).stem + ".png", dpi=120, facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()