Analytic Markov-Routing Lyapunov Controller / experiment.py
Mechanism failed
1import json, math
2import numpy as np
3
4SEED = 1675
5A = np.array([[[1.18, 0.22], [0.04, 0.82]],
6 [[0.86, 0.16], [-0.10, 1.26]]], dtype=float)
7
8
9def transition_matrix(p):
10 # symmetric two-state chain: persistence p; primitive for 0<p<1
11 return np.array([[p, 1-p], [1-p, p]], dtype=float)
12
13
14def simulate(p, T=12000, burn=1000, seed=0, target=None, lr=0.0, epochs=0):
15 """Finite-time top exponent; optional scalar persistence controller."""
16 rng = np.random.default_rng(seed)
17 v = np.array([1.0, 0.37]); v /= np.linalg.norm(v)
18 z = 0
19 vals = []
20 for _ in range(epochs):
21 v = np.array([1.0, 0.37]); v /= np.linalg.norm(v); z = 0; s = 0.0
22 for t in range(T):
23 w = A[z] @ v
24 n = np.linalg.norm(w)
25 s += np.log(n)
26 v = w / n
27 z = int(rng.random() >= p) if z == 0 else int(rng.random() < p)
28 lam = s / T
29 vals.append(lam)
30 if target is not None:
31 # finite-difference d lambda/dp, then gradient descent on squared target error
32 hp=0.01
33 lp=lambda_estimate(min(.98,p+hp), T=max(3000,T//3), seed=700+_)
34 lm=lambda_estimate(max(.02,p-hp), T=max(3000,T//3), seed=900+_)
35 grad=(lp-lm)/(2*hp)
36 p = float(np.clip(p - lr * 2.0 * (lam-target) * grad, 0.02, 0.98))
37 # independent estimate, collecting state frequencies and transitions
38 counts = np.zeros(2, dtype=int); trans = np.zeros((2,2), dtype=int)
39 s = 0.0; v = np.array([1.0, 0.37]); v /= np.linalg.norm(v); z = 0
40 for t in range(T + burn):
41 old = z
42 w = A[z] @ v; n = np.linalg.norm(w); v = w/n
43 z = int(rng.random() >= p) if z == 0 else int(rng.random() < p)
44 if t >= burn:
45 s += np.log(n); counts[old] += 1; trans[old,z] += 1
46 return dict(lam=float(s/T), p=float(p), counts=counts.tolist(), trans=trans.tolist(), controller_trace=vals)
47
48
49def lambda_estimate(p, T=8000, seed=1):
50 return simulate(p, T=T, burn=1000, seed=seed)['lam']
51
52
53def common_random_lambda(p, uniforms, burn=1000):
54 """Coupled estimate: identical random uniforms make FD noise small."""
55 z=0; v=np.array([1.0,.37]); v/=np.linalg.norm(v); s=0.0
56 for t,u in enumerate(uniforms):
57 old=z
58 w=A[z]@v; n=np.linalg.norm(w); v=w/n
59 z = int(u >= p) if z == 0 else int(u < p)
60 if t >= burn: s += np.log(n)
61 return s/(len(uniforms)-burn)
62
63
64def finite_difference_scaling(p=0.5):
65 # At symmetry p=.5, lambda'(p)=0 is predicted by mode-label exchange.
66 rng=np.random.default_rng(991)
67 uniforms=rng.random(8000)
68 rows=[]
69 for h in [0.08,0.04,0.02,0.01,0.005]:
70 d=(common_random_lambda(p+h,uniforms)-common_random_lambda(p-h,uniforms))/(2*h)
71 rows.append({'h':h, 'derivative':float(d), 'abs_error_vs_predicted_zero':float(abs(d))})
72 # Away from symmetry, check derivative consistency under h halving.
73 p2=.35; u2=np.random.default_rng(992).random(8000)
74 drows=[]
75 for h in [.04,.02,.01,.005]:
76 d=(common_random_lambda(p2+h,u2)-common_random_lambda(p2-h,u2))/(2*h)
77 drows.append({'h':h,'derivative':float(d)})
78 return {'symmetry_point':{'p':p,'predicted_derivative':0.0,'rows':rows},
79 'interior_point':{'p':p2,'rows':drows}}
80
81def stationary_check(p):
82 x=simulate(p,T=8000,burn=2000,seed=31)
83 freq=np.array(x['counts'],float)/sum(x['counts'])
84 pi=np.array([.5,.5])
85 # q_j mass equation reduces to pi=pi P; compare empirical frequencies.
86 residual=float(np.max(np.abs(freq - pi @ transition_matrix(p))))
87 return {'empirical_pi':freq.tolist(), 'theoretical_pi':[.5,.5], 'mass_equation_max_residual':residual}
88
89
90def boundary_sweep():
91 rows=[]
92 for p in [0.05,0.10,0.25,0.50,0.75,0.90,0.95]:
93 vals=[lambda_estimate(p,10000,s) for s in [2,3,4,5]]
94 rows.append({'p':p,'mean_lambda':float(np.mean(vals)), 'std_over_seeds':float(np.std(vals,ddof=1))})
95 return rows
96
97
98def controller_demo():
99 # Target is the exponent at p=.5; controller starts at p=.2.
100 target=lambda_estimate(.5,20000,77)
101 x=simulate(.2,T=5000,burn=500,seed=100,target=target,lr=.08,epochs=12)
102 return {'target_lambda':target,'initial_p':.2,'final_p':x['p'],'trace':x['controller_trace']}
103
104
105def main():
106 result={
107 'matrices':A.tolist(),
108 'stationary_check_p05':stationary_check(.5),
109 'stationary_check_p095':stationary_check(.95),
110 'smoothness_fd':finite_difference_scaling(.5),
111 'boundary_sweep':boundary_sweep(),
112 'controller':controller_demo(),
113 'predictions':[
114 'For primitive p in (0,1), empirical stationary masses should approach pi=(.5,.5).',
115 'In the smooth interior, central finite-difference derivative error should decrease approximately quadratically with h after accounting for Monte Carlo noise.',
116 'Near p=0 or p=1, mixing slows and finite-time Lyapunov estimates should have larger seed variance than at p=.5.'
117 ]
118 }
119 with open('results.json','w') as f: json.dump(result,f,indent=2)
120 print(json.dumps(result,indent=2))
121
122if __name__=='__main__': main()