"""Synthetic saturating feedback model, with equilibrium and fold calculations. Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7. Run from _media; write matching-basename opaque white PNG to CWD. """ from pathlib import Path import matplotlib.pyplot as plt import numpy as np def equilibria(u, beta): roots = np.roots([1, -(u + beta), 1, -u]) return sorted(r.real for r in roots if abs(r.imag) < 1e-8 and r.real >= 0) def main(): plt.rcParams.update({"font.size": 11, "figure.facecolor": "white"}) beta = 1.8 folds = sorted(z.real for z in np.roots([1, 0, 2, -2 * beta, 1]) if abs(z.imag) < 1e-8 and z.real > 0) x_on, x_off = folds u_on, u_off = [x - beta * x * x / (1 + x * x) for x in folds] fig, axes = plt.subplots(1, 2, figsize=(11, 5.7), dpi=110) fig.suptitle("SYNTHETIC POSITIVE FEEDBACK — no biological data", weight="bold", y=0.98) ax = axes[0] x = np.linspace(0, 1.8, 1201) u = 0.12 ax.plot(x, u + beta * x * x / (1 + x * x), color="#2468b4", lw=2, label="Production: u + βx²/(1+x²)") ax.plot(x, x, color="#334155", lw=2, label="Removal: x") for k, r in enumerate(equilibria(u, beta)): assert abs(u + beta * r * r / (1 + r * r) - r) < 1e-12 stable = 2 * beta * r / (1 + r * r) ** 2 < 1 ax.plot(r, r, "o", ms=8, mec="#334155", mfc="#187c64" if stable else "white", zorder=5) ax.text(r + 0.015, r - 0.15, ("stable" if stable else "unstable"), fontsize=10) ax.set(title="Same input, two stable equilibria\nu = 0.12, β = 1.8", xlabel="Activity x (normalized)", ylabel="Rate (normalized)", xlim=(0, 1.8), ylim=(0, 1.9)) ax.legend(loc="upper left", fontsize=9) ax = axes[1] low = np.linspace(0, x_on, 500) middle = np.linspace(x_on, x_off, 500) high = np.linspace(x_off, 1.9, 500) for xx, style, label in ((low, "-", "β = 1.8: stable branches"), (middle, "--", "β = 1.8: unstable branch"), (high, "-", None)): ax.plot(xx - beta * xx * xx / (1 + xx * xx), xx, style, color="#2468b4", lw=2.4, label=label) inputs = np.linspace(0, 0.3, 200) ax.plot(inputs, [equilibria(s, 1)[0] for s in inputs], color="#7c5f9e", lw=2, label="β = 1: one stable equilibrium") for s, xx, label in ((u_on, x_on, f"ON: {u_on:.4f}"), (u_off, x_off, f"OFF: {u_off:.4f}")): roots = equilibria(s + (1e-6 if label.startswith("ON") else -1e-6), beta) end = roots[-1] if label.startswith("ON") else roots[0] ax.annotate("", (s, end), (s, xx), arrowprops={"arrowstyle": "->", "color": "#ba4a29", "lw": 2}) ax.axvline(s, color="#94a3b8", linestyle=":", lw=1) ax.text(s + 0.006, 1.65 if label.startswith("ON") else 1.45, label, fontsize=10, color="#9b3c1d") ax.set(title="Equilibria under a slow input sweep\nArrows mark jumps at folds", xlabel="Sustained input u (normalized)", ylabel="Equilibrium activity x*", xlim=(0, 0.3), ylim=(0, 1.9)) ax.legend(loc="upper left", bbox_to_anchor=(0, -0.19), fontsize=9, frameon=False) for ax in axes: ax.grid(alpha=0.18) fig.subplots_adjust(left=0.07, right=0.98, bottom=0.25, top=0.82, wspace=0.29) fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), facecolor="white", transparent=False) plt.close(fig) if __name__ == "__main__": main()