Transverse Synchrony Training / transverse_sync_experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json
  2import math
  3import random
  4from pathlib import Path
  5
  6import numpy as np
  7import torch
  8
  9SEED = 2466
 10random.seed(SEED)
 11np.random.seed(SEED)
 12torch.manual_seed(SEED)
 13A = 1.15
 14T = 60
 15
 16
 17def rollout(k, e0=1.0, a=A, T=T):
 18    rng = np.random.default_rng(SEED)
 19    v = rng.normal(0.0, 0.35, size=T)
 20    q, z = 0.0, e0
 21    errors = [z - q]
 22    for t in range(T):
 23        u = k * (q - z)
 24        q = a * q + v[t]
 25        z = a * z + u + v[t]
 26        errors.append(z - q)
 27    return np.asarray(errors)
 28
 29
 30def fit_slope(errors, burn=3):
 31    # Fit only the numerically resolved exponential segment. Exact synchronization
 32    # can otherwise turn roundoff into a spurious positive slope.
 33    vals = np.abs(errors[burn:])
 34    peak = float(np.max(vals))
 35    keep = vals > max(peak * 1e-12, 1e-14)
 36    vals = vals[keep]
 37    x = np.arange(len(errors))[burn:][keep].astype(float)
 38    if len(vals) < 2:
 39        return 0.0
 40    return float(np.polyfit(x, np.log(vals), 1)[0])
 41
 42
 43def analytic_lambda(k, a=A):
 44    m = abs(a - k)
 45    return -math.inf if m == 0 else math.log(m)
 46
 47
 48def numerical_checks():
 49    # Prediction 1: transverse stability boundary is k*=a-1 for positive gains.
 50    gains = np.linspace(-0.8, 2.8, 37)
 51    rows = []
 52    for k in gains:
 53        errors = rollout(float(k))
 54        measured = fit_slope(errors)
 55        predicted = analytic_lambda(float(k))
 56        rows.append({"k": float(k), "predicted_lambda": predicted,
 57                     "measured_slope": measured,
 58                     "stable_predicted": bool(abs(A-k) < 1),
 59                     "stable_observed": bool(measured < 0)})
 60
 61    # Prediction 2: measured log-residual slope equals transverse exponent.
 62    slope_errors = [abs(r["measured_slope"] - r["predicted_lambda"])
 63                    for r in rows if np.isfinite(r["predicted_lambda"]) and abs(A-r["k"]) > 1e-6]
 64    max_slope_error = max(slope_errors)
 65
 66    # Prediction 3: close to the boundary, decay time scales as 1/|lambda|.
 67    near_gains = [0.35, 0.45, 0.55, 0.65, 0.75, 0.85, 0.95, 1.05, 1.15, 1.25]
 68    scaling = []
 69    for k in near_gains:
 70        lam = analytic_lambda(k)
 71        slope = fit_slope(rollout(k))
 72        scaling.append({"k": k, "abs_lambda": abs(lam),
 73                        "measured_abs_slope": abs(slope),
 74                        "time_constant_pred": 1.0/abs(lam),
 75                        "time_constant_obs": (1.0/abs(slope) if abs(slope) > 1e-15 else float("inf"))})
 76
 77    # Prediction 3: the convergence margin is linear in coupling gain,
 78    # lambda(k)=log|a-k|, with slope -1/(a-k) locally.
 79    scaling_gains = [0.20, 0.30, 0.40, 0.50]
 80    gain_scaling = [{"k": k, "predicted_multiplier": A-k,
 81                     "observed_multiplier": float(abs(rollout(k)[1] / rollout(k)[0]))}
 82                    for k in scaling_gains]
 83
 84    # Locate the observed sign transition by linear interpolation between sweep points.
 85    crossing = None
 86    for x, y in zip(rows[:-1], rows[1:]):
 87        if x["measured_slope"] * y["measured_slope"] <= 0:
 88            crossing = x["k"] + (0.0-x["measured_slope"]) * (y["k"]-x["k"]) / (y["measured_slope"]-x["measured_slope"])
 89            break
 90    stable_agreement = sum(r["stable_predicted"] == r["stable_observed"] for r in rows) / len(rows)
 91    return {"analytic_boundary": A - 1.0, "observed_boundary": crossing,
 92            "boundary_error": abs(crossing-(A-1.0)) if crossing is not None else None,
 93            "classification_agreement": stable_agreement,
 94            "max_abs_slope_error": max_slope_error,
 95            "slope_rows": rows,
 96            "scaling_rows": scaling, "gain_scaling": gain_scaling}
 97
 98
 99def train_controller(sync_weight, steps=500, batch=64, horizon=25):
100    # Same task for both methods: make plant track reference under shared forcing.
101    torch.manual_seed(SEED)
102    k = torch.nn.Parameter(torch.tensor(0.0))
103    opt = torch.optim.Adam([k], lr=0.03)
104    rng = torch.Generator().manual_seed(SEED)
105    losses = []
106    for _ in range(steps):
107        v = torch.randn(batch, horizon, generator=rng) * 0.35
108        q = torch.zeros(batch)
109        z = torch.randn(batch, generator=rng) * 1.5
110        total = torch.zeros(())
111        for t in range(horizon):
112            u = k * (q-z)
113            q = A*q + v[:, t]
114            z = A*z + u + v[:, t]
115            total = total + (z-q).square().mean()
116        # task and synchrony coincide here; sync_weight tests explicit residual shaping.
117        loss = total / horizon * (1.0 + sync_weight)
118        opt.zero_grad(); loss.backward(); opt.step()
119        losses.append(float(loss.detach()))
120    with torch.no_grad():
121        test = []
122        for e0 in [-2.0, -1.0, 1.0, 2.0]:
123            test.append(float(np.mean(rollout(float(k), e0=e0)[-10:]**2)))
124    return {"final_k": float(k.detach()), "train_loss_last": losses[-1],
125            "test_tail_mse": float(np.mean(test)), "loss_curve": losses}
126
127
128def main():
129    checks = numerical_checks()
130    baseline = train_controller(0.0)
131    idea = train_controller(2.0)
132    out = {"seed": SEED, "a": A, "horizon": T, "checks": checks,
133           "training": {"baseline_task_only": baseline,
134                         "idea_task_plus_synchrony": idea}}
135    Path("results.json").write_text(json.dumps(out, indent=2))
136    print(json.dumps({"analytic_boundary": checks["analytic_boundary"],
137                      "observed_boundary": checks["observed_boundary"],
138                      "boundary_error": checks["boundary_error"],
139                      "classification_agreement": checks["classification_agreement"],
140                      "max_abs_slope_error": checks["max_abs_slope_error"],
141                      "baseline": {k:v for k,v in baseline.items() if k != "loss_curve"},
142                      "idea": {k:v for k,v in idea.items() if k != "loss_curve"}}, indent=2))
143
144
145if __name__ == "__main__":
146    main()