Robust Instability Radius Monitor / rir_experiment.py
Mechanism failed
1import json, math, random
2from pathlib import Path
3import numpy as np
4
5
6def rho_hat_diagonal(g):
7 g = np.asarray(g, dtype=float)
8 return float(np.max(np.maximum(np.abs(g) - 1.0, 0.0)))
9
10
11def spectral_radius(a):
12 return float(np.max(np.abs(np.linalg.eigvals(np.asarray(a, dtype=float)))))
13
14
15def brute_radius_2x(a, step=0.01, upper=2.5):
16 """Approximate min ||d||inf for 2x2 diagonal perturbations."""
17 a = np.asarray(a, dtype=float)
18 for r in np.arange(0.0, upper + step / 2, step):
19 vals = np.arange(-r, r + step / 2, step) if r else np.array([0.0])
20 for d0 in vals:
21 for d1 in vals:
22 if spectral_radius(a + np.diag([d0, d1])) < 1.0 - 1e-8:
23 return float(r)
24 return float('nan')
25
26
27def math_checks():
28 # Exact diagonal prediction: each unstable channel costs |g_i|-1.
29 gs = np.array([-2.0, -1.2, -0.4, 0.7, 1.0, 1.3, 2.5])
30 exact = float(np.maximum(np.abs(gs) - 1, 0).max())
31 observed = rho_hat_diagonal(gs)
32 # Boundary prediction: x_t=g^t changes from decay to growth at |g|=1.
33 boundary = []
34 for g in np.linspace(0.5, 1.5, 21):
35 slope = math.log(abs(g))
36 boundary.append((float(g), float(slope)))
37 sign_change = min(boundary, key=lambda z: abs(z[1]))[0]
38 # Scaling prediction for a single unstable channel.
39 scaling = [(float(g), rho_hat_diagonal([g])) for g in [1.05, 1.2, 1.5, 2.0, 2.5]]
40 # Coupled 2x2 numerical definition check (the optimizer is a brute-force verifier).
41 mats = [np.array([[1.25, .20], [.10, .80]]), np.array([[-1.35, .15], [.05, .65]])]
42 coupled = []
43 for a in mats:
44 r = brute_radius_2x(a)
45 # coarse independent validation: returned point is feasible, previous grid shell is not.
46 feasible = spectral_radius(a + np.diag([-r, -r])) < 1.0 if np.isfinite(r) else False
47 coupled.append({'matrix': a.tolist(), 'grid_radius': r, 'feasible_at_uniform_shift': bool(feasible)})
48 return {'diagonal_exact': {'predicted': exact, 'observed': observed, 'abs_error': abs(exact-observed)},
49 'boundary': {'predicted': 1.0, 'observed_nearest_zero_log_slope': sign_change,
50 'max_boundary_error': abs(sign_change-1.0)},
51 'linear_scaling': scaling, 'coupled_checks': coupled}
52
53
54def run_torch_experiment(steps=500, seed=7):
55 try:
56 import torch
57 torch.manual_seed(seed); np.random.seed(seed); random.seed(seed)
58 device = 'cuda' if torch.cuda.is_available() else 'cpu'
59 # A scalar recurrent system makes rho_hat exact and isolates the regularizer.
60 # Task is to retain a constant input through T steps, requiring g near 1.
61 T, batch = 4, 64
62 x = torch.ones(batch, 1, device=device)
63 target = torch.ones(batch, 1, device=device)
64 def train(reg):
65 torch.manual_seed(seed)
66 g = torch.nn.Parameter(torch.tensor([1.2], device=device))
67 opt = torch.optim.Adam([g], lr=.015)
68 rec = []
69 for it in range(steps):
70 h = x
71 for _ in range(T): h = g * h
72 task = ((h-target)**2).mean()
73 radius = torch.relu(torch.abs(g)-1.0)
74 penalty = 20.0 * torch.relu(torch.tensor(.60, device=device)-radius)**2 if reg else 0*g
75 loss = task + penalty
76 opt.zero_grad(); loss.backward(); opt.step()
77 if it in (0, 49, 99, 199, 499):
78 rec.append({'step': it, 'g': float(g.detach().cpu()), 'task': float(task.detach().cpu()),
79 'rho_hat': float(radius.detach().cpu()), 'growth_abs_g_T': float(abs(g.detach().cpu())**T)})
80 return rec
81 return {'device': device, 'baseline': train(False), 'rir_penalty': train(True)}
82 except Exception as e:
83 return {'device': 'cpu-fallback', 'error': repr(e)}
84
85
86def main():
87 out = {'math_checks': math_checks(), 'training': run_torch_experiment()}
88 Path('results.json').write_text(json.dumps(out, indent=2))
89 print(json.dumps(out, indent=2))
90
91if __name__ == '__main__':
92 main()