GQL Safe Residual Layer / gql_experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json
  2import numpy as np
  3
  4EPS = 1e-6
  5
  6
  7def admissibility(U, eps=EPS):
  8    U = np.asarray(U)
  9    D = U[..., 0]
 10    m = U[..., 1:-1]
 11    E = U[..., -1]
 12    return (D >= eps) & (E - np.sqrt(D * D + np.sum(m * m, axis=-1)) >= eps)
 13
 14
 15def g(U, s):
 16    U = np.asarray(U)
 17    s = np.asarray(s)
 18    # Supports a shared s=(d,) or batched s=(N,d).
 19    if s.ndim == 1:
 20        return U[..., -1] - s[0] * U[..., 0] - np.sum(s[1:] * U[..., 1:-1], axis=-1)
 21    return U[..., -1] - s[..., 0] * U[..., 0] - np.sum(s[..., 1:] * U[..., 1:-1], axis=-1)
 22
 23
 24def limit_residual(U0, U1, eps=EPS, iterations=60):
 25    """Exact scalar residual limiter via bisection on the relativistic margin.
 26
 27    The admissible set is convex, so with valid U0 and invalid U1 there is one
 28    boundary crossing on [0,1]. This is a robust reference implementation of
 29    the scalar limiter; theta is detached because this NumPy MVP has no grads.
 30    """
 31    U0 = np.asarray(U0, dtype=float)
 32    U1 = np.asarray(U1, dtype=float)
 33    delta = U1 - U0
 34    def margin(t):
 35        V = U0 + t[..., None] * delta
 36        return V[..., -1] - np.sqrt(np.sum(V[..., :-1] * V[..., :-1], axis=-1))
 37    theta = np.ones(U0.shape[:-1])
 38    bad = (U1[..., 0] < eps) | (margin(np.ones(U0.shape[:-1])) < eps)
 39    lo = np.zeros_like(theta)
 40    hi = np.ones_like(theta)
 41    # Only bad rows need a root; valid rows remain at theta=1.
 42    for _ in range(iterations):
 43        mid = 0.5 * (lo + hi)
 44        ok = margin(mid) >= eps
 45        lo = np.where(ok, mid, lo)
 46        hi = np.where(ok, hi, mid)
 47    theta = np.where(bad, lo, theta)
 48    # Density is linear and can be the active constraint independently.
 49    dbad = U1[..., 0] < eps
 50    td = np.clip((U0[..., 0] - eps) / np.maximum(U0[..., 0] - U1[..., 0], 1e-30), 0., 1.)
 51    theta = np.where(dbad, np.minimum(theta, td), theta)
 52    # Move infinitesimally inside both closed constraints to avoid roundoff.
 53    theta = np.where(theta < 1.0, np.maximum(0.0, theta * (1.0 - 1e-12)), theta)
 54    V = U0 + theta[..., None] * delta
 55    # A direct safety correction handles ulp-scale endpoint errors.
 56    V[..., 0] = np.maximum(V[..., 0], eps)
 57    return V, theta
 58
 59def component_clip(U, eps=EPS):
 60    """Control: independently clip density and energy, leaving momentum unchanged."""
 61    V = np.asarray(U, dtype=float).copy()
 62    V[..., 0] = np.maximum(V[..., 0], eps)
 63    V[..., -1] = np.maximum(V[..., -1], eps)
 64    return V
 65
 66
 67def main(seed=1234):
 68    rng = np.random.default_rng(seed)
 69
 70    # Verify min_{||s||<=1} g_s(U) = E - sqrt(D^2+||m||^2).
 71    x = rng.normal(size=(10000, 3))
 72    x /= np.linalg.norm(x, axis=1, keepdims=True)
 73    U = rng.uniform(0.1, 3.0, size=(10000, 4))
 74    U[:, 1:3] = rng.normal(0.0, 1.0, size=(10000, 2))
 75    exact = U[:, -1] - np.linalg.norm(U[:, :-1], axis=1)
 76    sampled = np.min(np.stack([g(U, x[i]) for i in range(len(x))], axis=1), axis=1)
 77    # The sampled minimum is only an approximation; test the analytic worst normal.
 78    w = U[:, :-1]
 79    sstar = w / np.linalg.norm(w, axis=1, keepdims=True)
 80    analytic = np.array([g(U[i], sstar[i]) for i in range(len(U))])
 81    identity_abs_err = float(np.max(np.abs(exact - analytic)))
 82    sampled_gap_mean = float(np.mean(sampled - exact))
 83
 84    # Valid baselines, then deliberately corrupt learned candidates.
 85    n = 20000
 86    U0 = rng.uniform(0.2, 2.0, size=(n, 4))
 87    U0[:, 1:3] = rng.normal(0.0, 0.7, size=(n, 2))
 88    U0[:, -1] = np.linalg.norm(U0[:, :-1], axis=1) + rng.uniform(0.05, 1.5, size=n)
 89    # Errors are large enough to produce both density and coupled energy/momentum violations.
 90    U1 = U0 + rng.normal(0.0, 0.9, size=U0.shape)
 91    U1[:, 0] -= rng.uniform(0.0, 1.3, size=n)
 92    U1[:, -1] -= rng.uniform(0.0, 0.9, size=n)
 93
 94    safe, theta = limit_residual(U0, U1)
 95    clipped = component_clip(U1)
 96    valid0 = admissibility(U0).mean()
 97    valid1 = admissibility(U1).mean()
 98    valid_safe = admissibility(safe).mean()
 99    valid_clip = admissibility(clipped).mean()
100    # Retention is the fraction of the learned residual preserved.
101    dnorm = np.linalg.norm(U1 - U0, axis=1)
102    retained = np.linalg.norm(safe - U0, axis=1) / np.maximum(dnorm, 1e-12)
103    clip_retained = np.linalg.norm(clipped - U0, axis=1) / np.maximum(dnorm, 1e-12)
104    result = {
105        "seed": seed,
106        "identity_max_abs_error": identity_abs_err,
107        "random_sphere_mean_gap_vs_exact": sampled_gap_mean,
108        "baseline_valid_rate": float(valid0),
109        "candidate_valid_rate": float(valid1),
110        "gql_safe_valid_rate": float(valid_safe),
111        "component_clip_valid_rate": float(valid_clip),
112        "gql_mean_theta": float(theta.mean()),
113        "gql_fraction_limited": float(np.mean(theta < 1.0 - 1e-12)),
114        "gql_mean_residual_retention": float(retained.mean()),
115        "component_clip_mean_residual_retention": float(clip_retained.mean()),
116        "gql_min_margin": float(np.min(safe[:, -1] - np.linalg.norm(safe[:, :-1], axis=1))),
117        "component_clip_min_margin": float(np.min(clipped[:, -1] - np.linalg.norm(clipped[:, :-1], axis=1))),
118    }
119    print(json.dumps(result, indent=2, sort_keys=True))
120    with open("gql_results.json", "w") as f:
121        json.dump(result, f, indent=2, sort_keys=True)
122
123
124if __name__ == "__main__":
125    main()
126