Certified Temporal Budget for Neural Control / experiment.py
Mechanism confirmed, baseline not beaten
1import json
2import math
3from pathlib import Path
4import numpy as np
5
6SEED = 2734
7rng = np.random.default_rng(SEED)
8
9# 1-D specialization of a 2-D obstacle margin: Phi(x)=distance to obstacle.
10# The plant moves toward the obstacle with constant speed v, so dPhi/dt=-v exactly.
11def certificate_sweep():
12 gamma_true = 1.0
13 gamma_est = 1.25 # conservative bound
14 delta = 0.05
15 distances = np.linspace(0.15, 2.0, 80)
16 latencies = np.linspace(0.0, 2.0, 81)
17 permitted = []
18 unsafe_permitted = []
19 for d in distances:
20 for L in latencies:
21 contract_permits = L <= d / gamma_est - delta
22 physically_safe = L <= d / gamma_true + 1e-12
23 permitted.append(contract_permits)
24 unsafe_permitted.append(contract_permits and not physically_safe)
25 # The implication is checked over a dense grid, including the boundary.
26 conservative_violations = int(np.sum(unsafe_permitted))
27
28 # With an intentionally underestimated gamma, the observed permit/fail
29 # transition is compared to the exact physical boundary d/gamma_true-delta.
30 under_gamma = 0.75
31 transitions = []
32 for d in distances:
33 ls = np.linspace(0, 2.5, 10001)
34 permits = ls <= d / under_gamma - delta
35 unsafe = ls > d / gamma_true
36 bad = np.where(permits & unsafe)[0]
37 transitions.append(float(ls[bad[0]]) if len(bad) else float("nan"))
38 valid = np.isfinite(transitions)
39 observed_underestimate_boundary = float(np.nanmedian(np.array(transitions)[valid]))
40 # For this sweep the onset is expected near the smallest boundary where
41 # under-estimation starts to allow an unsafe handoff: d/gamma_true.
42 expected_underestimate_boundary = float(np.median(distances[distances / under_gamma - delta > distances / gamma_true]))
43 return {
44 "grid_points": len(distances) * len(latencies),
45 "conservative_contract_violations": conservative_violations,
46 "conservative_violation_rate": conservative_violations / len(permitted),
47 "underestimated_gamma": under_gamma,
48 "underestimate_unsafe_onset_median_L": observed_underestimate_boundary,
49 "expected_physical_boundary_median_L": expected_underestimate_boundary,
50 }
51
52def derivative_verification():
53 # Phi(t)=phi0-v*t and gamma_true=v: finite differences satisfy
54 # dPhi/dt=-gamma_true, hence Phi(t)-Phi(0)+gamma_true*t == 0.
55 phi0, gamma_true = 1.7, 1.3
56 ts = np.linspace(0.0, 1.0, 1001)
57 phi = phi0 - gamma_true * ts
58 finite_slopes = np.diff(phi) / np.diff(ts)
59 residual = phi - (phi0 - gamma_true * ts)
60 return {"max_abs_slope_error": float(np.max(np.abs(finite_slopes + gamma_true))),
61 "max_contract_residual": float(np.max(np.abs(residual))),
62 "predicted_unit_rate": 1.0,
63 "observed_normalized_slope": float(np.max(-finite_slopes / gamma_true))}
64
65def gamma_boundary_sweep():
66 # Prediction: for a fixed latency L and margin delta, permission changes at
67 # gamma_est = d/(L+delta). Conservative gamma has no unsafe permissions;
68 # underestimated gamma creates unsafe permissions.
69 d = 1.0
70 L = 1.05
71 delta = 0.05
72 gamma_true = 1.0
73 gammas = np.linspace(0.4, 1.8, 141)
74 unsafe = []
75 permitted = []
76 for g in gammas:
77 p = L <= d / g - delta
78 permitted.append(p)
79 unsafe.append(p and L > d / gamma_true)
80 transition = gammas[np.where(np.array(permitted) == False)[0][0]]
81 predicted = d / (L + delta)
82 return {
83 "predicted_gamma_transition": predicted,
84 "observed_gamma_transition_grid": float(transition),
85 "transition_abs_error": abs(float(transition) - predicted),
86 "unsafe_permissions_at_conservative_gamma": int(sum(unsafe[i] for i,g in enumerate(gammas) if g >= gamma_true)),
87 "unsafe_permissions_at_underestimated_gamma": int(sum(unsafe[i] for i,g in enumerate(gammas) if g < gamma_true)),
88 "predicted_unsafe_onset_gamma": predicted,
89 "observed_first_unsafe_gamma": float(gammas[np.where(np.array(unsafe))[0][-1]]) if np.any(unsafe) else None,
90 }
91
92def evaluation_scaling():
93 # Prediction: a constant-distance contract with usable interval
94 # B=d/gamma-delta-L has evaluation rate 1/B, and rate ratios scale as
95 # the inverse certified budget.
96 d, delta, L, horizon = 1.0, 0.05, 0.05, 100.0
97 gammas = np.array([0.5, 1.0, 1.5, 2.0])
98 rows = []
99 for g in gammas:
100 B = d / g - delta - L
101 observed = math.ceil(horizon / B)
102 predicted = horizon / B
103 rows.append({"gamma": float(g), "budget": B, "observed_evals": observed,
104 "predicted_evals": predicted, "rate_error": abs(observed-predicted)/predicted})
105 # Relative scaling is the important quantity and is not affected by ceil.
106 return {"rows": rows, "rate_ratio_observed_gamma2_over_gamma1": rows[3]["observed_evals"] / rows[1]["observed_evals"],
107 "rate_ratio_predicted_gamma2_over_gamma1": rows[3]["predicted_evals"] / rows[1]["predicted_evals"]}
108
109def scheduler_comparison():
110 # Same finite approach episodes, random initial distance and speed. A
111 # handoff is unsafe if latency exceeds the distance remaining at launch.
112 n = 3000
113 d0 = rng.uniform(0.5, 2.0, n)
114 v = rng.uniform(0.5, 1.5, n)
115 L = rng.uniform(0.0, 0.8, n)
116 delta = 0.05
117 gamma = 1.6 # safely above the sampled vmax
118 # ordinary periodic inference: fixed interval, no certificate fallback
119 periodic_h = 0.50
120 periodic_unsafe = L > np.maximum(d0 - v * periodic_h, 0) / v
121 # contract: only launch when the certified remaining interval covers latency;
122 # otherwise fallback/urgent refresh is used, so unsafe handoffs are rejected.
123 budget = d0 / gamma - delta
124 contract_launch = L <= budget
125 contract_unsafe = contract_launch & (L > d0 / v)
126 # Naive event trigger uses a fixed distance threshold, not a rate certificate.
127 naive_launch = d0 > 0.25
128 naive_unsafe = naive_launch & (L > d0 / v)
129 return {
130 "episodes": n,
131 "periodic_unsafe_handoff_rate": float(periodic_unsafe.mean()),
132 "naive_event_unsafe_handoff_rate": float(naive_unsafe.mean()),
133 "contract_unsafe_handoff_rate": float(contract_unsafe.mean()),
134 "contract_launch_fraction": float(contract_launch.mean()),
135 "contract_fallback_fraction": float((~contract_launch).mean()),
136 "periodic_evaluations_per_episode": 1.0,
137 "contract_evaluations_per_episode": float(contract_launch.mean()),
138 }
139
140def main():
141 out = {"seed": SEED, "derivative_verification": derivative_verification(),
142 "math_verification": certificate_sweep(),
143 "gamma_boundary_sweep": gamma_boundary_sweep(),
144 "evaluation_scaling": evaluation_scaling(),
145 "scheduler_comparison": scheduler_comparison()}
146 Path("results.json").write_text(json.dumps(out, indent=2))
147 print(json.dumps(out, indent=2))
148
149if __name__ == "__main__":
150 main()