Routh-Hurwitz Gain-Capped Optimizer / routh_experiment.py
Mechanism confirmed, baseline not beaten
1import json
2import random
3from pathlib import Path
4import numpy as np
5
6
7def cubic_check(r=0.2, L=1.0, omega=1.0, rho=0.8):
8 a1 = 2*r/L
9 a2 = (r/L)**2 + omega**2
10 kappa = 1.5*omega/L
11 boundary = a1*a2/kappa
12 rows = []
13 for frac in [0.2, 0.8, 0.99, 1.0, 1.01, 1.5]:
14 g = frac*boundary
15 roots = np.roots([1., a1, a2, kappa*g])
16 rows.append({'fraction': frac, 'chi': float(kappa*g/(a1*a2)),
17 'max_real_root': float(np.max(roots.real)),
18 'roots': [[float(z.real), float(z.imag)] for z in roots]})
19 return {'a1': a1, 'a2': a2, 'kappa': kappa,
20 'boundary_g': boundary, 'cap_g': rho*boundary, 'rows': rows,
21 'crossing_observed': bool(rows[2]['max_real_root'] < 0 and rows[4]['max_real_root'] > 0)}
22
23
24def run_momentum(lr, momentum, H, steps=250, cap=False, rho=0.8):
25 # Two-dimensional quadratic, with one deliberately high-curvature direction.
26 x = np.array([1., 1.])
27 v = np.zeros(2)
28 losses, gains, chis = [], [], []
29 # Use the cubic coefficients as the specified local stability model.
30 r, L, omega = .2, 1., 1.
31 a1, a2, kappa = 2*r/L, (r/L)**2 + omega**2, 1.5*omega/L
32 gmax = rho*a1*a2/kappa
33 effective_lr = min(lr, gmax/max(H)) if cap else lr
34 for _ in range(steps):
35 grad = H*x
36 g = effective_lr*max(H)
37 chi = kappa*g/(a1*a2)
38 v = momentum*v + grad
39 x = x - effective_lr*v
40 losses.append(0.5*float(x@(H*x))); gains.append(g); chis.append(chi)
41 if not np.all(np.isfinite(x)) or losses[-1] > 1e100:
42 break
43 tail = losses[-50:]
44 return {'final_loss': losses[-1] if losses else float('inf'),
45 'min_loss': min(losses) if losses else float('inf'),
46 'loss_std_tail': float(np.std(tail)) if tail else float('inf'),
47 'steps': len(losses), 'effective_lr': effective_lr,
48 'max_chi': max(chis) if chis else float('inf'),
49 'diverged': len(losses) < steps or not np.isfinite(losses[-1])}
50
51
52def main():
53 random.seed(7); np.random.seed(7)
54 check = cubic_check()
55 # Sweep a range around the ordinary quadratic momentum stability boundary.
56 H = np.array([1., 20.])
57 settings = []
58 for lr in [.12, .2, .4, .8, 1.2]:
59 settings.append({'lr': lr,
60 'baseline': run_momentum(lr, .9, H, cap=False),
61 'routh_cap': run_momentum(lr, .9, H, cap=True)})
62 out = {'cubic_check': check, 'quadratic_experiment': settings}
63 Path('results.json').write_text(json.dumps(out, indent=2))
64 print(json.dumps(out, indent=2))
65
66if __name__ == '__main__':
67 main()