Carrier-Probed Hidden-State Training / carrier_probe.py

Failed on benchmark

Raw ⬇ ZIP
  1import json
  2from pathlib import Path
  3import numpy as np
  4from scipy.optimize import least_squares
  5
  6DT = 0.02
  7H = 250
  8Q_TRUE = 1.0
  9
 10
 11def carrier(kind="sin", omega=2.0, phase=0.0):
 12    t = np.arange(H) * DT
 13    if kind == "sin":
 14        return np.sin(omega * t + phase)
 15    if kind == "binary":
 16        return np.sign(np.sin(omega * t + phase))
 17    raise ValueError(kind)
 18
 19
 20def rollout(a, m, q=Q_TRUE, u=None, z0=None):
 21    if u is None:
 22        u = np.zeros(H)
 23    if z0 is None:
 24        z0 = np.zeros(2)
 25    z = np.zeros((H + 1, 2))
 26    z[0] = z0
 27    for t in range(H):
 28        inp = u[t] + a * m[t]
 29        z[t + 1, 0] = z[t, 0] + DT * (-z[t, 0] + q * z[t, 1] ** 2 + inp)
 30        z[t + 1, 1] = z[t, 1] + DT * (-0.1 * z[t, 1] + inp)
 31    return z
 32
 33
 34def gramians(a, m, q=Q_TRUE, delta=1e-12):
 35    z = rollout(a, m, q)
 36    # C=[1,0], and A is the exact Jacobian of one Euler step.
 37    Phi = np.eye(2)
 38    Wo = np.zeros((2, 2))
 39    # Reachability is computed by propagating each input impulse forward.
 40    Wr = np.zeros((2, 2))
 41    for t in range(H):
 42        A = np.array([[1 - DT, 2 * DT * q * z[t, 1]],
 43                      [0, 1 - 0.1 * DT]])
 44        B = np.array([DT, DT])
 45        C = np.array([1.0, 0.0])
 46        Wo += (Phi.T @ np.outer(C, C) @ Phi) * DT
 47        Psi = np.eye(2)
 48        for j in range(t + 1, H):
 49            Aj = np.array([[1 - DT, 2 * DT * q * z[j, 1]],
 50                           [0, 1 - 0.1 * DT]])
 51            Psi = Aj @ Psi
 52        v = Psi @ B
 53        Wr += np.outer(v, v) * DT
 54        Phi = A @ Phi
 55    ev_o = np.linalg.eigvalsh(Wo)
 56    ev_r = np.linalg.eigvalsh(Wr)
 57    return Wo, Wr, ev_o, ev_r
 58
 59
 60def fit_q(observations, m, a):
 61    # Fit q from observed z1 only, with known input and known initial state.
 62    def residual(x):
 63        pred = rollout(a, m, float(x[0]))[:, 0]
 64        return (pred - observations) / 0.01
 65    return float(least_squares(residual, [0.2], bounds=(-3, 3)).x[0])
 66
 67
 68def main():
 69    rng = np.random.default_rng(123)
 70    m = carrier("sin", omega=2.0)
 71    amplitudes = np.array([0.0, 0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.6])
 72    rows = []
 73    for a in amplitudes:
 74        Wo, Wr, eo, er = gramians(a, m)
 75        rows.append({"a": float(a), "lambda_min_Wo": float(eo[0]),
 76                     "lambda_min_Wr": float(er[0]),
 77                     "logdet_Wo": float(np.linalg.slogdet(Wo + 1e-12*np.eye(2))[1])})
 78
 79    # Prediction 1: passive hidden observability is zero (up to numerical precision).
 80    passive = rows[0]["lambda_min_Wo"]
 81    # Prediction 2: small-amplitude hidden observability is quadratic in a.
 82    small = np.array([r for r in rows if 0.01 <= r["a"] <= 0.08])
 83    coef = float(np.polyfit(np.log(small[:, 0]) if False else np.log([r["a"] for r in small]),
 84                            np.log([r["lambda_min_Wo"] for r in small]), 1)[0])
 85    ratios = [r["lambda_min_Wo"] / (r["a"] ** 2) for r in small]
 86    # Prediction 3: a practical threshold is where the hidden eigenvalue exceeds 100x passive.
 87    threshold = next((r["a"] for r in rows if r["lambda_min_Wo"] > max(100*passive, 1e-8)), None)
 88
 89    # Secondary task check: estimate q from noisy observed trajectories.
 90    noise = rng.normal(0, 0.01, H + 1)
 91    passive_obs = rollout(0.0, m)[:, 0] + noise
 92    probed_a = 0.16
 93    probed_obs = rollout(probed_a, m)[:, 0] + rng.normal(0, 0.01, H + 1)
 94    q_passive = fit_q(passive_obs, m, 0.0)
 95    q_probed = fit_q(probed_obs, m, probed_a)
 96    result = {"dt": DT, "horizon": H*DT, "rows": rows,
 97              "predictions": {
 98                  "passive_lambda_hidden_expected": 0.0,
 99                  "passive_lambda_observed": passive,
100                  "quadratic_exponent_expected": 2.0,
101                  "quadratic_exponent_observed": coef,
102                  "quadratic_ratio_mean": float(np.mean(ratios)),
103                  "quadratic_ratio_cv": float(np.std(ratios)/np.mean(ratios)),
104                  "threshold_rule": "lambda_min(Wo) > max(100*passive, 1e-8)",
105                  "threshold_observed_a": threshold
106              },
107              "fit_q": {"true": Q_TRUE, "passive": q_passive, "probed": q_probed}}
108    Path("results.json").write_text(json.dumps(result, indent=2))
109    print(json.dumps(result, indent=2))
110
111
112if __name__ == "__main__":
113    main()