Composed Trusted Reachable Families for Recurrent Networks / trusted_reachable_rnn.py
Failed on benchmark
1import json, math, random
2from pathlib import Path
3import numpy as np
4
5
6def f(h, u, lam, q):
7 return lam * h + u + q * h * h
8
9
10def affine_rollout(lam, q, H, gamma):
11 # Nominal h*=u*=0, one scalar initial-state coordinate gamma.
12 hs = np.zeros(H + 1)
13 Rs = np.zeros(H + 1)
14 hs[0] = 0.0
15 Rs[0] = 1.0
16 for k in range(H):
17 hs[k + 1] = f(hs[k], 0.0, lam, q)
18 # A_k=df/dh at nominal = lam; C_k=1 but U_k=0.
19 Rs[k + 1] = lam * Rs[k]
20 pred = hs + Rs * gamma
21 true = np.zeros(H + 1)
22 true[0] = gamma
23 for k in range(H):
24 true[k + 1] = f(true[k], 0.0, lam, q)
25 return hs, Rs, pred, true
26
27
28def violation(lam, q, H, gamma):
29 hs, Rs, pred, true = affine_rollout(lam, q, H, gamma)
30 # v_k is one-step nonlinear defect evaluated on the affine family.
31 vals = []
32 for k in range(H):
33 vals.append(abs(f(pred[k], 0.0, lam, q) - pred[k + 1]))
34 return max(vals) if vals else 0.0
35
36
37def max_box_violation(lam, q, H, radius):
38 # In this scalar box, the endpoint is worst for this positive quadratic map.
39 return max(violation(lam, q, H, -radius), violation(lam, q, H, radius))
40
41
42def measured_horizon(lam, q, radius, eps, Hmax=80):
43 good = 0
44 for H in range(1, Hmax + 1):
45 if max_box_violation(lam, q, H, radius) <= eps * (1 + 1e-12):
46 good = H
47 else:
48 break
49 return good
50
51
52def predicted_horizon(lam, q, radius, eps):
53 # For lambda>1: q*r^2*lambda^(2(H-1)) <= eps.
54 if q * radius * radius <= eps:
55 return max(1, int(math.floor(1 + math.log(eps/(q*radius*radius)) / (2*math.log(lam)))))
56 return 0
57
58
59def radius_for_horizon(lam, q, H, eps):
60 # Exact one-step defect for the affine family at the last step.
61 return math.sqrt(eps / (q * lam ** (2 * (H - 1))))
62
63
64def main():
65 np.random.seed(7); random.seed(7)
66 lam, q, H, gamma = 1.25, 0.3, 6, 0.04
67 # Core math sanity: autodiff-free finite difference Jacobian and recurrence.
68 delta = 1e-6
69 numeric_A = (f(delta, 0, lam, q) - f(-delta, 0, lam, q)) / (2*delta)
70 _, Rs, _, _ = affine_rollout(lam, q, H, gamma)
71 jacobian_check = {"analytic_A": lam, "finite_difference_A": numeric_A,
72 "max_R_error": float(np.max(np.abs(Rs - lam**np.arange(H+1))))}
73
74 # Prediction 1: quadratic scaling with perturbation radius.
75 radii = np.array([0.01, 0.02, 0.04, 0.08])
76 vals = np.array([max_box_violation(lam, q, H, r) for r in radii])
77 log_slope = float(np.polyfit(np.log(radii), np.log(vals), 1)[0])
78
79 # Prediction 2: critical horizon follows the exponential boundary.
80 eps = 1e-3
81 hs = list(range(1, 13))
82 measured = [measured_horizon(lam, q, r, eps, 30) for r in [0.01, 0.02, 0.04]]
83 predicted = [predicted_horizon(lam, q, r, eps) for r in [0.01, 0.02, 0.04]]
84
85 # Prediction 3: trusted radius decays as lambda^-(H-1).
86 horizons = np.array([2, 4, 6, 8])
87 rr = np.array([radius_for_horizon(lam, q, int(h), eps) for h in horizons])
88 decay_slope = float(np.polyfit(horizons - 1, np.log(rr), 1)[0])
89 predicted_decay = -math.log(lam)
90
91 # Parameter sweep over lambda: stable dynamics should not show exponential growth.
92 lambda_sweep = []
93 for la in [0.8, 1.0, 1.1, 1.25, 1.5]:
94 v = max_box_violation(la, q, 10, 0.04)
95 lambda_sweep.append({"lambda": la, "H10_violation": v,
96 "critical_H": measured_horizon(la, q, 0.04, eps, 50)})
97
98 # Small monitor comparison: unmonitored affine prediction vs accepted trusted radius.
99 # Baseline uses fixed radius 0.04; monitor shrinks to the largest radius satisfying eps.
100 baseline_v = max_box_violation(lam, q, 10, 0.04)
101 monitored_r = radius_for_horizon(lam, q, 10, eps)
102 monitored_v = max_box_violation(lam, q, 10, monitored_r)
103
104 out = {
105 "jacobian_check": jacobian_check,
106 "prediction_1_quadratic_radius": {"radii": radii.tolist(), "violations": vals.tolist(), "observed_log_slope": log_slope, "predicted_slope": 2.0},
107 "prediction_2_horizon_boundary": {"radii": [0.01,0.02,0.04], "epsilon": eps, "measured_H": measured, "predicted_H": predicted, "formula": "q*r^2*lambda^(2(H-1)) <= epsilon"},
108 "prediction_3_radius_decay": {"H": horizons.tolist(), "radii": rr.tolist(), "observed_log_decay_per_step": decay_slope, "predicted": predicted_decay},
109 "lambda_sweep": lambda_sweep,
110 "monitor_comparison": {"baseline_radius": 0.04, "baseline_H10_violation": baseline_v, "monitored_radius": monitored_r, "monitored_H10_violation": monitored_v},
111 }
112 Path("results.json").write_text(json.dumps(out, indent=2))
113 print(json.dumps(out, indent=2))
114
115if __name__ == "__main__":
116 main()