Bifurcation-calibrated delayed-gradient escape / delayed_gradient_experiment.py
Failed on benchmark
1import json
2import math
3from pathlib import Path
4
5import numpy as np
6from scipy.special import lambertw
7
8SEED = 2900
9rng = np.random.default_rng(SEED)
10
11
12def dominant_root(k, tau):
13 """Dominant characteristic root of lambda + k exp(-lambda tau)=0."""
14 # lambda*tau = W_j(-k*tau); enumerate branches near the dominant pair.
15 roots = [lambertw(-k * tau, j) / tau for j in range(-8, 9)]
16 return max(roots, key=lambda z: z.real)
17
18
19def simulate_linear(k, tau, dt=0.001, duration=80.0, z0=1e-6):
20 """Euler integration of dz/dt=-k z(t-tau), with constant prehistory."""
21 n = int(duration / dt)
22 m = max(1, int(round(tau / dt)))
23 z = np.full(n + m + 1, z0, dtype=float)
24 for i in range(m, n + m):
25 z[i + 1] = z[i] - dt * k * z[i - m]
26 if abs(z[i + 1]) > 1e100:
27 z[i + 1:] = z[i + 1]
28 break
29 t = np.arange(n + 1) * dt
30 return t, z[m : m + n + 1]
31
32
33def measured_growth(t, z, unstable):
34 """Fit log magnitude after a short transient; ignore numerical zeros."""
35 if not unstable:
36 return float(np.polyfit(t[-20_000:], np.log(np.maximum(np.abs(z[-20_000:]), 1e-300)), 1)[0])
37 # A broad fit before saturation/overflow gives the envelope rate.
38 mask = (np.abs(z) > 1e-8) & (np.abs(z) < 1e50) & (t > 5.0)
39 if mask.sum() < 20:
40 return float("nan")
41 return float(np.polyfit(t[mask], np.log(np.abs(z[mask])), 1)[0])
42
43
44def escape_times(k, tau, Ds, R=0.1, dt=0.001, duration=220.0):
45 """Direct deterministic delayed evolution from fluctuation amplitude sqrt(D)."""
46 r = max(0.0, dominant_root(k, tau).real)
47 out = []
48 for D in Ds:
49 t, z = simulate_linear(k, tau, dt=dt, duration=duration, z0=math.sqrt(D))
50 hit = np.flatnonzero(np.abs(z) >= R)
51 out.append(float(t[hit[0]]) if len(hit) else float("inf"))
52 return r, out
53
54
55def double_well_grad(x):
56 return x * (x * x - 1.0)
57
58
59def run_double_well(eta, steps, delayed_m=0, noise=0.018, seed=0):
60 """Same noisy SGD setup; delayed_m=0 is current-gradient SGD."""
61 rg = np.random.default_rng(seed)
62 x = 0.95
63 history = [x]
64 gradients = []
65 for _ in range(steps):
66 gradients.append(double_well_grad(x))
67 g = gradients[-1] if delayed_m == 0 or len(gradients) <= delayed_m else gradients[-1 - delayed_m]
68 x -= eta * g + math.sqrt(eta) * noise * rg.normal()
69 history.append(x)
70 h = np.asarray(history)
71 # A crossing into the opposite basin is a clear toy escape event.
72 crossings = np.flatnonzero(h < -0.5)
73 return int(crossings[0]) if len(crossings) else None, float(np.mean((h[-1000:] ** 2 - 1.0) ** 2) / 4.0)
74
75
76def main():
77 k = 1.0
78 tau_c = math.pi / (2 * k)
79 multiples = [0.5, 0.9, 1.05, 1.3, 2.0]
80 boundary_rows = []
81 for mult in multiples:
82 tau = mult * tau_c
83 root = dominant_root(k, tau)
84 unstable = root.real > 1e-10
85 t, z = simulate_linear(k, tau)
86 fit = measured_growth(t, z, unstable)
87 boundary_rows.append({
88 "multiple_tau_c": mult, "tau": tau,
89 "predicted_real_lambda": float(root.real),
90 "predicted_unstable": bool(unstable),
91 "observed_growth_fit": fit,
92 "observed_unstable": bool(fit > 1e-4),
93 })
94
95 # Prediction 1: transition occurs at k*tau=pi/2.
96 # Prediction 2: above threshold, measured growth follows Re(lambda+).
97 growth_rows = [r for r in boundary_rows if r["predicted_unstable"]]
98 growth_abs_errors = [abs(r["observed_growth_fit"] - r["predicted_real_lambda"]) for r in growth_rows]
99
100 # Prediction 3: escape time is affine in log(R/sqrt(D)), with slope 1/r.
101 tau_escape = 1.3 * tau_c
102 Ds = np.logspace(-12, -5, 8)
103 r, times = escape_times(k, tau_escape, Ds)
104 x = np.log(0.1 / np.sqrt(Ds))
105 slope, intercept = np.polyfit(x, np.asarray(times), 1)
106 escape_rows = [{"D": float(D), "predicted_T": float(T)} for D, T in zip(Ds, times)]
107
108 # Secondary mini-comparison: fixed stale gradients versus calibrated burst delay.
109 # At the well curvature k=2, tau_c=pi/4; m=ceil(1.1*tau_c/eta).
110 eta = 0.02
111 burst_m = int(math.ceil(1.1 * (math.pi / 4) / eta))
112 baseline = [run_double_well(eta, 30000, delayed_m=0, seed=s) for s in range(12)]
113 fixed_stale = [run_double_well(eta, 30000, delayed_m=burst_m, seed=s) for s in range(12)]
114 def summarize(a):
115 events = [x[0] for x in a if x[0] is not None]
116 return {"escape_fraction": len(events) / len(a), "median_escape_step": float(np.median(events)) if events else None,
117 "final_loss": float(np.mean([x[1] for x in a]))}
118
119 result = {
120 "seed": SEED, "tau_c_k1": tau_c,
121 "boundary_sweep": boundary_rows,
122 "boundary_prediction": {"predicted_multiple": 1.0,
123 "observed_first_unstable_multiple": next((r["multiple_tau_c"] for r in boundary_rows if r["observed_unstable"]), None)},
124 "growth_prediction": {"mean_absolute_rate_error": float(np.mean(growth_abs_errors)) if growth_abs_errors else None,
125 "predicted_rates": [r["predicted_real_lambda"] for r in growth_rows],
126 "observed_rates": [r["observed_growth_fit"] for r in growth_rows]},
127 "escape_prediction": {"tau": tau_escape, "r": r, "predicted_slope_1_over_r": 1.0 / r,
128 "observed_slope": float(slope), "slope_relative_error": float(abs(slope - 1.0 / r) / (1.0 / r)),
129 "intercept": float(intercept), "points": escape_rows},
130 "double_well": {"eta": eta, "calibrated_delay_steps": burst_m,
131 "current_gradient_sgd": summarize(baseline),
132 "fixed_delayed_gradient_sgd": summarize(fixed_stale)},
133 }
134 Path("results.json").write_text(json.dumps(result, indent=2))
135 print(json.dumps(result, indent=2))
136
137
138if __name__ == "__main__":
139 main()