import json import math from pathlib import Path import numpy as np SEED = 1448 rng = np.random.default_rng(SEED) def softmax(x): x = np.asarray(x, dtype=float) z = x - np.max(x, axis=-1, keepdims=True) e = np.exp(z) return e / e.sum(axis=-1, keepdims=True) def stl_softmin(values, tau=0.15): values = np.asarray(values, dtype=float) m = np.min(values, axis=-1) return m - tau * np.log(np.exp(-(values - m[..., None]) / tau).sum(axis=-1)) def transition(A, rho, beta): # Rows are source modes j and columns are destination modes i. return softmax(A + beta * np.asarray(rho)[None, :]) def posterior(pi, A, rho, beta, loglik): T = transition(A, rho, beta) pred = pi @ T q = pred * np.exp(loglik - np.max(loglik)) return q / q.sum() def expert_step(x, mode, dt=0.1): # x = position x/y and velocity x/y; four simple dynamical regimes. p = x[:2].copy(); v = x[2:].copy() if mode == 0: # constant velocity a = np.array([0., 0.]) elif mode == 1: # x acceleration a = np.array([0.7, 0.]) elif mode == 2: # y acceleration, safety-critical near the floor a = np.array([0., -0.55]) else: # smooth turn a = np.array([-0.35 * v[1], 0.35 * v[0]]) vn = v + dt * a return np.r_[p + dt * vn, vn] def rollout(x, mode, H=50): out = [] z = x.copy() for _ in range(H): z = expert_step(z, mode) out.append(z.copy()) return np.asarray(out) def robustness(x, mode, H=12): tr = rollout(x, mode, H) # G(position_y >= -0.5 AND speed <= 2.7), with smooth temporal min. pred1 = tr[:, 1] + 0.5 pred2 = 2.7 - np.linalg.norm(tr[:, 2:], axis=1) atomic = np.minimum(pred1, pred2) return float(stl_softmin(atomic, tau=0.08)) def math_checks(): # Prediction 1: log(T_i/T_j) is affine in beta with slope rho_i-rho_j. Arow = np.array([0.4, -0.3, 0.1, -0.2]) rho = np.array([0.20, -0.75, 0.55, -0.10]) i, j = 2, 1 betas = np.linspace(0., 4., 9) logs = [] for b in betas: q = transition(Arow[None, :], rho, b)[0] logs.append(np.log(q[i] / q[j])) slope, intercept = np.polyfit(betas, logs, 1) expected_slope = rho[i] - rho[j] max_identity_err = float(np.max(np.abs(np.asarray(logs) - (Arow[i] - Arow[j] + betas * expected_slope)))) # Prediction 2: destination i overtakes j at beta*Delta-rho = logit gap. gap = 1.35 delta = 0.90 predicted_boundary = gap / delta # A two-mode row has equal probabilities exactly when the log odds is zero. bgrid = np.linspace(0., 3., 3001) odds = -gap + bgrid * delta observed_boundary = float(bgrid[np.argmin(np.abs(odds))]) # Prediction 3: beta=0 removes all robustness dependence, exactly. A = np.array([[0.8, -0.1, 0.2, -0.4], [-0.2, 0.7, 0.1, -0.3], [0.1, -0.2, 0.9, -0.1], [-0.3, -0.1, 0.2, 0.6]]) r1 = np.array([0.2, -0.3, 0.8, -0.1]) r2 = np.array([-2., 1.3, -0.5, 0.7]) t1 = transition(A, r1, 0.) t2 = transition(A, r2, 0.) beta0_err = float(np.max(np.abs(t1 - t2))) # Sweep unrelated robustness vectors: at beta=0 every one must give the # same transition, directly testing the predicted vanishing effect. beta0_sweep = [] for scale in [0.0, 0.5, 1.0, 3.0, 10.0]: rr = rng.normal(size=4) * scale beta0_sweep.append(float(np.max(np.abs(transition(A, r1, 0.) - transition(A, rr, 0.))))) return { "odds_slope_observed": float(slope), "odds_slope_predicted": float(expected_slope), "odds_max_abs_identity_error": max_identity_err, "boundary_beta_observed": observed_boundary, "boundary_beta_predicted": predicted_boundary, "boundary_abs_error": abs(observed_boundary - predicted_boundary), "beta0_max_transition_difference": beta0_err, "beta0_sweep_scales": [0.0, 0.5, 1.0, 3.0, 10.0], "beta0_sweep_max_errors": beta0_sweep, "predictions_confirmed": bool(abs(slope-expected_slope) < 1e-10 and max_identity_err < 1e-10 and abs(observed_boundary-predicted_boundary) < 0.002 and beta0_err < 1e-12) } def mini_experiment(): # Same fixed initial state and mode-2 trajectories for both routers. # Mode 2 is the true downward-acceleration regime; observation noise makes # the one-step likelihood intentionally ambiguous near the safety boundary. A = np.array([[2.0, -0.5, -0.5, -0.5], [-0.5, 1.8, -0.5, -0.5], [-0.5, -0.5, 1.8, -0.5], [-0.5, -0.5, -0.5, 1.8]]) x0 = np.array([0.0, 0.12, 0.0, -1.05]) Hobs = 8 noise = 0.16 n = 160 one = {"baseline": [], "stl": []} long = {"baseline": [], "stl": []} selected = {"baseline": [], "stl": []} true = [] for _ in range(n): # Small episode-to-episode perturbation, with all methods seeing it. x = x0 + rng.normal(0, 0.025, 4) y = expert_step(x, 2) + rng.normal(0, noise, 4) true.append(y) loglik = np.array([-np.sum((y-expert_step(x,m))**2)/(2*noise**2) for m in range(4)]) pi = np.ones(4) / 4 base = posterior(pi, A, np.zeros(4), 0., loglik) rho = np.array([robustness(x, m, Hobs) for m in range(4)]) safe = posterior(pi, A, rho, 2.8, loglik) for name, q in [("baseline", base), ("stl", safe)]: pred = sum(q[m] * expert_step(x, m) for m in range(4)) one[name].append(np.mean((pred-y)**2)) # Open-loop rollout from the posterior mixture by blending each # deterministic expert trajectory; report terminal 50-step MSE. target = rollout(y, 2, 50) predtr = sum(q[m] * rollout(x, m, 50) for m in range(4)) long[name].append(np.mean((predtr-target)**2)) selected[name].append(int(np.argmax(q))) # Parameter sweep on the same fixed episodes is intentionally reported: # beta=0 must equal baseline, while larger beta changes mode selection. beta_sweep = {} for b in [0.0, 0.5, 1.0, 2.8, 5.0]: errs = [] picks = [] for _ in range(n): x = x0 + rng.normal(0, 0.025, 4) y = expert_step(x, 2) + rng.normal(0, noise, 4) ll = np.array([-np.sum((y-expert_step(x,m))**2)/(2*noise**2) for m in range(4)]) rr = np.array([robustness(x, m, Hobs) for m in range(4)]) q = posterior(np.ones(4)/4, A, rr, b, ll) pred = sum(q[m] * expert_step(x,m) for m in range(4)) errs.append(np.mean((pred-y)**2)); picks.append(int(np.argmax(q))) beta_sweep[str(b)] = {"one_step_mse": float(np.mean(errs)), "mode2_selection_rate": float(np.mean(np.asarray(picks)==2))} return { "n_episodes": n, "one_step_mse": {k: float(np.mean(v)) for k,v in one.items()}, "rollout_50_mse": {k: float(np.mean(v)) for k,v in long.items()}, "mode2_selection_rate": {k: float(np.mean(np.asarray(v)==2)) for k,v in selected.items()}, "beta_sweep_same_protocol": beta_sweep, "robustness_beta": 2.8, "note": "Synthetic fixed expert dynamics; no learned parameters or training loop." } def main(): result = {"seed": SEED, "math_checks": math_checks(), "mini_experiment": mini_experiment()} Path("results.json").write_text(json.dumps(result, indent=2)) print(json.dumps(result, indent=2)) if __name__ == "__main__": main()