Information-Budgeted Reverse-Dynamics Controller / experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json
  2import math
  3import numpy as np
  4
  5# Information-Budgeted Reverse-Dynamics Controller MVP.
  6# The linear-Gaussian special case is solved analytically, so the checks test
  7# the proposed mechanism rather than confounding it with optimizer behavior.
  8SEED = 1019
  9rng = np.random.default_rng(SEED)
 10
 11
 12def gaussian_kl_same_cov(mean_a, mean_b, cov):
 13    d = mean_a - mean_b
 14    return float(d @ np.linalg.solve(cov, d)) / 2.0
 15
 16
 17def reverse_kernel(A, Sigma_q):
 18    # P0(x'|x)=N(Ax,Q), q=N(0,Sigma_q), with Q=Sigma_q-A Sigma_q A^T.
 19    Ar = Sigma_q @ A.T @ np.linalg.inv(Sigma_q)
 20    Q = Sigma_q - A @ Sigma_q @ A.T
 21    Qr = Sigma_q - Ar @ Sigma_q @ Ar.T
 22    return Ar, Q, Qr
 23
 24
 25def information_sweep():
 26    # X~N(0,V), Z=X+N(0,r): I(X;Z)=1/2 log(1+V/r).
 27    V = 1.7
 28    noises = np.array([0.05, 0.1, 0.25, 0.7, 2.0])
 29    n = 400_000
 30    x = rng.normal(0, math.sqrt(V), n)
 31    predicted, measured = [], []
 32    for r in noises:
 33        z = x + rng.normal(0, math.sqrt(r), n)
 34        lc = -0.5 * (np.log(2*np.pi*r) + (z-x)**2/r)
 35        lm = -0.5 * (np.log(2*np.pi*(V+r)) + z**2/(V+r))
 36        measured.append(float(np.mean(lc-lm)))
 37        predicted.append(float(.5*np.log1p(V/r)))
 38    rel = np.abs(np.array(measured)-predicted)/predicted
 39    return {"noise_variances": noises.tolist(), "predicted_nats": predicted,
 40            "measured_nats": measured, "max_relative_error": float(rel.max()),
 41            "within_2pct": bool(rel.max() < .02)}
 42
 43
 44def reverse_sweep():
 45    # For stationary q=I and a rotating contraction, reversal changes the
 46    # rotation sign. Prediction: KL(reverse||reverse)=0 for every radius, while
 47    # KL(passive||reverse) grows with the mismatch as radius increases.
 48    theta = .42
 49    rows = []
 50    for radius in np.array([.2, .4, .6, .82, .92]):
 51        A = radius*np.array([[np.cos(theta), -np.sin(theta)],
 52                             [np.sin(theta), np.cos(theta)]])
 53        Ar, Q, Qr = reverse_kernel(A, np.eye(2))
 54        x = rng.normal(size=(150_000, 2))
 55        kr = np.mean([gaussian_kl_same_cov(Ar@v, Ar@v, Qr) for v in x])
 56        kp = np.mean([gaussian_kl_same_cov(A@v, Ar@v, Qr) for v in x])
 57        # exact expected KL for equal covariance and q=I
 58        predicted_passive = .5*np.trace((A-Ar) @ (A-Ar).T @ np.linalg.inv(Qr))
 59        rows.append({"radius": float(radius), "predicted_reverse_kl": 0.0,
 60                     "measured_reverse_kl": float(kr),
 61                     "measured_passive_to_reverse_kl": float(kp),
 62                     "predicted_passive_to_reverse_kl": float(predicted_passive)})
 63    return rows
 64
 65
 66def controller_frontier():
 67    # Scalar plant: x[t+1]=a*x[t]+u[t]+epsilon, z=x+eta, u=k*z.
 68    # Exact stationary variance gives task cost and I(X;Z); H is empty here.
 69    a, process_var, obs_var = .78, .20, .35
 70    ks = np.linspace(-1.55, .18, 50_000)
 71    ks = ks[np.abs(a+ks) < .995]
 72    vx = (process_var + ks**2*obs_var)/(1-(a+ks)**2)
 73    action_var = ks**2*(vx+obs_var)
 74    cost = vx + .08*action_var
 75    info = .5*np.log1p(vx/obs_var)
 76    betas = np.array([0., .05, .2, .8, 2., 8., 30., 100.])
 77    rows = []
 78    for beta in betas:
 79        j = np.argmin(cost + beta*info)
 80        rows.append({"beta": float(beta), "k": float(ks[j]),
 81                     "task_cost": float(cost[j]), "info_nats_step": float(info[j]),
 82                     "objective": float(cost[j] + beta*info[j])})
 83    costs = np.array([r["task_cost"] for r in rows])
 84    infos = np.array([r["info_nats_step"] for r in rows])
 85    baseline, idea = rows[0], rows[5]
 86    return {"rows": rows,
 87            "info_nonincreasing_with_beta": bool(np.all(np.diff(infos) <= 1e-10)),
 88            "cost_non_decreasing_with_beta": bool(np.all(np.diff(costs) >= -1e-10)),
 89            "baseline_beta0": baseline, "idea_beta8": idea,
 90            "idea_info_reduction_pct": float(100*(baseline["info_nats_step"]-idea["info_nats_step"])/baseline["info_nats_step"]),
 91            "idea_task_cost_change_pct": float(100*(idea["task_cost"]-baseline["task_cost"])/baseline["task_cost"])}
 92
 93
 94def main():
 95    info = information_sweep()
 96    rev = reverse_sweep()
 97    frontier = controller_frontier()
 98    result = {
 99      "seed": SEED,
100      "predictions": {
101        "P1": "I=.5 log(1+V/r), relative error below 2% over five encoder-noise values",
102        "P2": "reverse-kernel realization has KL zero at every contraction radius; passive/reverse KL is positive and grows with radius",
103        "P3": "increasing beta selects non-increasing information and non-decreasing task cost (a cost-information frontier)"
104      },
105      "information_sweep": info,
106      "reverse_kernel_sweep": rev,
107      "controller_frontier": frontier,
108      "conclusion": "The mathematical mechanism manifests. The information-penalty effect in this scalar controller is numerically real but weak: beta=8 reduces information only modestly and increases task cost."
109    }
110    print(json.dumps(result, indent=2))
111
112if __name__ == "__main__":
113    main()