Bifurcation-Aware Adaptive Compute Controller / experiment.py
Mechanism confirmed, baseline not beaten
1import json, math, os
2import numpy as np
3
4# Bifurcation-aware adaptive RK4 toy experiment.
5# The scalar normal form is dz/dt = mu + z^2, with passage measured from z=-1 to z=+1.
6
7def rhs(z, mu):
8 return mu + z*z
9
10def rk4_step(z, h, mu):
11 k1 = rhs(z, mu)
12 k2 = rhs(z + .5*h*k1, mu)
13 k3 = rhs(z + .5*h*k2, mu)
14 k4 = rhs(z + h*k3, mu)
15 return z + h*(k1 + 2*k2 + 2*k3 + k4)/6
16
17def exact_residence(mu):
18 # Integral dz/(mu+z^2), for positive mu, between -1 and +1.
19 q = math.sqrt(mu)
20 return 2.0*math.atan(1.0/q)/q
21
22def controller_h(mu, hmin=5e-4, hmax=5e-2, mu0=1e-2, delta=1e-12):
23 # Formula in the proposal, with mu_hat clipped to its positive ghost regime.
24 return min(hmax, max(hmin, hmax*math.sqrt((max(mu, 0.0)+delta)/mu0)))
25
26def passage(mu, mode, fixed_h=2e-3):
27 z, t, n = -1.0, 0.0, 0
28 while z < 1.0 and n < 100000000:
29 h = fixed_h if mode == 'fixed' else controller_h(mu)
30 # Do not step far beyond the event; this also makes errors comparable.
31 h = min(h, (1.0-z)/max(rhs(z, mu), 1e-30))
32 z = rk4_step(z, h, mu)
33 t += h; n += 1
34 return t, n, z
35
36def log_slope(x, y):
37 return float(np.polyfit(np.log(x), np.log(y), 1)[0])
38
39def slow_passage(eps):
40 # mu=eps*t. Start at mu=-0.1 on the attracting quasi-static branch,
41 # then measure time from the fold (t=0) until z crosses zero.
42 t = -0.1/eps
43 z = -math.sqrt(.1)
44 # A modest step relative to the universal eps^(1/3) time scale.
45 h = min(0.02, 0.03*eps**(-1/3))
46 n = 0
47 while z < 0.0 and n < 20000000:
48 # RK4 with time-varying mu.
49 def f(tt, zz): return eps*tt + zz*zz
50 k1=f(t,z); k2=f(t+h/2,z+h*k1/2); k3=f(t+h/2,z+h*k2/2); k4=f(t+h,z+h*k3)
51 zn=z+h*(k1+2*k2+2*k3+k4)/6
52 if zn >= 0.0:
53 # linear interpolation of the crossing, sufficient for scaling.
54 frac=(0.0-z)/(zn-z)
55 t=t+frac*h
56 break
57 z=zn; t += h; n += 1
58 return t, eps*t, n
59
60def main():
61 np.random.seed(0)
62 mus=np.logspace(-4, -0.5, 8)
63 residence=[]; fixed=[]; adaptive=[]
64 for mu in mus:
65 exact=exact_residence(mu)
66 tf,nf,_=passage(mu,'fixed')
67 ta,na,_=passage(mu,'adaptive')
68 residence.append(exact)
69 fixed.append({'time':tf,'n':nf,'relerr':abs(tf-exact)/exact})
70 adaptive.append({'time':ta,'n':na,'relerr':abs(ta-exact)/exact,'h':controller_h(mu)})
71 # Prediction 1: ghost residence has slope -1/2 at small positive mu.
72 small=mus[:5]
73 residence_slope=log_slope(small, np.array(residence[:5]))
74 # Prediction 2: controller h ~ sqrt(mu), hence controller allocations over the
75 # same physical ghost crossing scale approximately mu^-1 (T~mu^-1/2 and 1/h~mu^-1/2).
76 alloc_slope=log_slope(mus[:5], np.array([x['n'] for x in adaptive[:5]],float))
77 # Prediction 3: slow saddle-node passage time scales eps^-1/3.
78 eps=np.logspace(-4,-1,7)
79 slow=[slow_passage(e) for e in eps]
80 slow_times=np.array([x[0] for x in slow])
81 slow_slope=log_slope(eps, slow_times)
82 out={
83 'predictions':{
84 'ghost_residence_slope_pred':-0.5,'ghost_residence_slope_obs':residence_slope,
85 'controller_allocation_slope_pred':-1.0,'controller_allocation_slope_obs':alloc_slope,
86 'slow_delay_slope_pred':-1/3,'slow_delay_slope_obs':slow_slope},
87 'mu_sweep':[{'mu':float(m),'exact_T':float(residence[i]),'fixed':fixed[i], 'adaptive':adaptive[i],
88 'fixed_over_adaptive_n':fixed[i]['n']/max(adaptive[i]['n'],1)} for i,m in enumerate(mus)],
89 'slow_sweep':[{'epsilon':float(e),'delay_time':float(slow[i][0]),'mu_at_crossing':float(slow[i][1]),'steps':slow[i][2]} for i,e in enumerate(eps)],
90 'summary':{
91 'mean_fixed_relerr':float(np.mean([x['relerr'] for x in fixed])),
92 'mean_adaptive_relerr':float(np.mean([x['relerr'] for x in adaptive])),
93 'far_mu_eval_saving':float(1-adaptive[-1]['n']/fixed[-1]['n']),
94 'near_mu_eval_saving':float(1-adaptive[0]['n']/fixed[0]['n'])}}
95 with open('results.json','w') as f: json.dump(out,f,indent=2)
96 print(json.dumps(out,indent=2))
97
98if __name__=='__main__': main()