Dual recoverability gate for policy switching / dual_gate_experiment.py

Mechanism failed

Raw ⬇ ZIP
  1import json
  2from pathlib import Path
  3import numpy as np
  4from sklearn.linear_model import LogisticRegression
  5
  6SEED = 3048
  7
  8
  9def primary_action(x):
 10    # Aggressive controller: fast near the goal, but saturation makes it brittle
 11    # from boundary states under disturbances.
 12    return (-0.85 * x if abs(x) < 0.85 else 0.75 * np.sign(x))
 13
 14
 15def fallback_action(x):
 16    # Conservative controller: slower, with smaller action excursions.
 17    return np.clip(-0.42 * x, -0.55, 0.55)
 18
 19
 20def rollout(x0, policy, noise_std, horizon=12, rng=None):
 21    rng = np.random.default_rng() if rng is None else rng
 22    x = float(x0)
 23    reached = False
 24    failed = False
 25    for _ in range(horizon):
 26        x = x + float(policy(x)) + rng.normal(0.0, noise_std)
 27        if abs(x) >= 2.0:
 28            failed = True
 29            break
 30        if abs(x) <= 0.16:
 31            reached = True
 32            break
 33    return reached and not failed, failed, x
 34
 35
 36def features(xs):
 37    xs = np.asarray(xs)
 38    return np.column_stack([np.abs(xs), xs * xs])
 39
 40
 41def make_critic_data(n, noise, policy, seed):
 42    rng = np.random.default_rng(seed)
 43    xs = rng.uniform(-1.95, 1.95, size=n)
 44    successes = np.array([rollout(x, policy, noise, rng=rng)[0] for x in xs], dtype=int)
 45    return features(xs), successes
 46
 47
 48def train_critics():
 49    # P estimates primary-policy success probability.
 50    xp, yp = make_critic_data(8000, 0.42, primary_action, SEED)
 51    # V is failure risk under the conservative fallback, so lower is safer.
 52    xv, yv_success = make_critic_data(8000, 0.62, fallback_action, SEED + 1)
 53    yv_failure = 1 - yv_success
 54    p = LogisticRegression(C=10.0, solver="lbfgs", random_state=SEED).fit(xp, yp)
 55    v = LogisticRegression(C=10.0, solver="lbfgs", random_state=SEED + 1).fit(xv, yv_failure)
 56    return p, v
 57
 58
 59def gate(p_hat, v_hat, eta_p=0.55, eta_r=0.20):
 60    # Execute primary iff both paper conditions pass.
 61    return (p_hat >= eta_p) & (v_hat <= eta_r)
 62
 63
 64def evaluate(mode, p, v, n=5000, noise=0.48, seed=SEED + 20):
 65    rng = np.random.default_rng(seed)
 66    xs = np.clip(rng.normal(0.0, 1.45, n), -1.95, 1.95)
 67    p_hat = p.predict_proba(features(xs))[:, 1]
 68    v_hat = v.predict_proba(features(xs))[:, 1]
 69    if mode == "primary":
 70        use_primary = np.ones(n, dtype=bool)
 71    elif mode == "p_gate":
 72        use_primary = p_hat >= 0.55
 73    elif mode == "dual_gate":
 74        use_primary = gate(p_hat, v_hat)
 75    else:
 76        raise ValueError(mode)
 77    success = np.zeros(n, dtype=bool)
 78    failed = np.zeros(n, dtype=bool)
 79    for i, x in enumerate(xs):
 80        pol = primary_action if use_primary[i] else fallback_action
 81        success[i], failed[i], _ = rollout(x, pol, noise, rng=rng)
 82    return {
 83        "catastrophic_failure_rate": float(failed.mean()),
 84        "goal_success_rate": float(success.mean()),
 85        "primary_coverage": float(use_primary.mean()),
 86        "mean_p_selected": float(p_hat[use_primary].mean()) if use_primary.any() else 0.0,
 87        "mean_v_selected": float(v_hat[use_primary].mean()) if use_primary.any() else 0.0,
 88    }
 89
 90
 91def verify_math():
 92    rng = np.random.default_rng(SEED)
 93    p, v = rng.random(10000), rng.random(10000)
 94    a = gate(p, v, 0.55, 0.20)
 95    direct = (p >= 0.55) & (v <= 0.20)
 96    tighter = gate(p, v, 0.70, 0.10)
 97    return {"formula_exact": bool(np.array_equal(a, direct)),
 98            "stricter_threshold_subset": bool(np.all(~tighter | a)),
 99            "selected_fraction": float(a.mean())}
100
101
102def main():
103    math = verify_math()
104    p, v = train_critics()
105    results = {m: evaluate(m, p, v) for m in ("primary", "p_gate", "dual_gate")}
106    out = {"math_check": math, "results": results,
107           "config": {"seed": SEED, "train_noise_p": 0.42, "train_noise_v": 0.62,
108                      "eval_noise": 0.48, "eta_p": 0.55, "eta_r": 0.20}}
109    Path("results.json").write_text(json.dumps(out, indent=2))
110    print(json.dumps(out, indent=2))
111
112
113if __name__ == "__main__":
114    main()