Basin-Aware Hysteresis Guard / basin_guard_experiment.py
Beats tuned baseline
1import json
2import math
3import numpy as np
4
5SEED = 2771
6UC = 2.0 / (3.0 * math.sqrt(3.0))
7
8def F(x, u):
9 return x - x**3 + u
10
11def jac(x):
12 return 1.0 - 3.0*x*x
13
14def equilibria(u):
15 roots = np.roots([1.0, 0.0, -1.0, -u])
16 return sorted(float(r.real) for r in roots if abs(r.imag) < 1e-8)
17
18def integrate_many(x0, u, T=35.0, damping=1.0):
19 """Vectorized fixed-step RK4 integration of the scalar flow."""
20 x = np.asarray(x0, dtype=float).copy()
21 n = max(1, int(math.ceil(T / 0.04)))
22 dt = T / n
23 for _ in range(n):
24 k1 = damping * F(x, u)
25 k2 = damping * F(x + 0.5*dt*k1, u)
26 k3 = damping * F(x + 0.5*dt*k2, u)
27 k4 = damping * F(x + dt*k3, u)
28 x += dt*(k1 + 2*k2 + 2*k3 + k4)/6.0
29 return x
30
31def integrate(x0, u, T=35.0, damping=1.0):
32 return float(integrate_many(np.array([x0]), u, T, damping)[0])
33
34def desired_eq(u):
35 return max(equilibria(u))
36
37def estimate_saddle_node():
38 us = np.linspace(0.0, 0.45, 9001)
39 counts = np.array([len(equilibria(u)) for u in us])
40 return float(us[np.where(counts >= 3)[0][-1]])
41
42def main():
43 numeric_uc = estimate_saddle_node()
44 u_vals = [0.0, 0.10, 0.20, 0.35, 0.38]
45 stability = []
46 for u in u_vals:
47 roots = equilibria(u)
48 xp = max(roots)
49 stability.append({"u": u, "roots": roots, "desired_jacobian": jac(xp),
50 "locally_stable": jac(xp) < 0, "coexisting": len(roots) == 3})
51
52 # At u=0, x_ref=1 and separatrix=0. Thus recovery is exactly the event
53 # 1 + eps*z > 0 (up to numerical integration tolerance).
54 eps_values = [0.10, 0.30, 0.50, 0.70, 0.90, 1.10]
55 basin = []
56 for eps in eps_values:
57 rng = np.random.default_rng(SEED + int(eps * 1000))
58 z = rng.normal(size=4000)
59 xref = desired_eq(0.0)
60 final = integrate_many(xref + eps*z, 0.0)
61 basin.append({"epsilon": eps,
62 "B_hat": float(np.mean(np.abs(final-xref) < .08)),
63 "B_predicted_gaussian": float(0.5*(1+math.erf(1/(eps*math.sqrt(2))))),
64 "separatrix": 0.0, "reference": xref})
65
66 # Finite-horizon comparison. The intervention is deliberately tested after
67 # local stability is restored. The guard estimates basin recovery with probes
68 # and retains damping when recovery is not demonstrated.
69 def trial(mode, n=300):
70 rng = np.random.default_rng(SEED + (1 if mode == 'guard' else 2))
71 xref = desired_eq(0.0)
72 failures = releases = 0
73 for _ in range(n):
74 x = xref + 0.95 * rng.normal()
75 if mode == 'threshold':
76 c = 1.0 # local Jacobian is safely negative, so threshold-only releases
77 releases += 1
78 else:
79 probes = xref + 0.95*rng.normal(size=80)
80 probe_final = integrate_many(probes, 0.0, T=6.0, damping=0.35)
81 B = np.mean(np.abs(probe_final-xref) < .15)
82 c = 1.0 if jac(xref) <= -0.5 and B >= .95 else 0.35
83 releases += int(c == 1.0)
84 y = integrate(x, 0.0, T=6.0, damping=c)
85 failures += int(abs(y-xref) >= .15)
86 return {"failure_rate": failures/n, "release_rate": releases/n}
87
88 result = {
89 "seed": SEED,
90 "model": "dx/dt = x - x^3 + u (desired positive equilibrium)",
91 "predictions": {
92 "saddle_node": {"analytic_uc": UC, "numeric_last_three_root_u": numeric_uc,
93 "absolute_error": abs(UC-numeric_uc)},
94 "local_vs_global": "desired Jacobian remains negative throughout the three-root bistable interval; local stability does not imply global recovery",
95 "basin_transition": "at u=0, separatrix is x=0 and desired equilibrium is x=1; recovery probability is Phi(1/epsilon)"
96 },
97 "stability_sweep": stability,
98 "basin_sweep": basin,
99 "finite_horizon_comparison": {"threshold_only": trial("threshold"), "basin_guard": trial("guard")}
100 }
101 with open("results.json", "w") as f:
102 json.dump(result, f, indent=2)
103 print(json.dumps(result, indent=2))
104
105if __name__ == '__main__':
106 main()