Closure-Decorrelation Memory Scheduler / closure_scheduler_experiment.py

Mechanism failed

Raw ⬇ ZIP
  1import json, math
  2from pathlib import Path
  3import numpy as np
  4
  5SEED = 1729
  6EPS = 0.10
  7PERSISTENCE = 3
  8RIDGE = 1e-4
  9CANDIDATES = [1, 2, 4, 8, 16, 32]
 10
 11
 12def closure_rho(z, max_lag=100):
 13    """Ensemble/time normalized autocorrelation, avoiding sequence concatenation."""
 14    z = np.asarray(z, float)
 15    z = z - np.mean(z)
 16    den = np.mean(z * z)
 17    return np.array([1.0 if k == 0 else np.mean(z[:, :-k] * z[:, k:]) / den
 18                     for k in range(max_lag)])
 19
 20
 21def first_persistent_crossing(rho, eps=EPS, persistence=PERSISTENCE):
 22    for k in range(1, len(rho) - persistence + 1):
 23        if np.all(np.abs(rho[k:k + persistence]) <= eps):
 24            return k
 25    return len(rho) - persistence
 26
 27
 28def make_data(a, nseq=240, length=240, noise=0.65, seed=SEED):
 29    rng = np.random.default_rng(seed)
 30    z = rng.normal(size=(nseq, length))
 31    for t in range(1, length):
 32        z[:, t] = a * z[:, t - 1] + math.sqrt(1 - a * a) * z[:, t]
 33    # x is a resolved signal with unresolved closure injection z observed noisily.
 34    x = z + noise * rng.normal(size=z.shape)
 35    return x, z
 36
 37
 38def fit_history_predictor(x, m, frac=.65):
 39    cut = int(len(x) * frac)
 40
 41    def xy(arr):
 42        X, Y = [], []
 43        for row in arr:
 44            for t in range(m - 1, len(row) - 1):
 45                X.append(row[t - m + 1:t + 1][::-1])
 46                Y.append(row[t + 1])
 47        return np.asarray(X), np.asarray(Y)
 48
 49    X, Y = xy(x[:cut])
 50    Xt, Yt = xy(x[cut:])
 51    w = np.linalg.solve(X.T @ X + RIDGE * np.eye(m), X.T @ Y)
 52    return w, float(np.mean((Xt @ w - Yt) ** 2))
 53
 54
 55def rollout_error(x, w, m, horizon=30):
 56    vals = []
 57    for row in x:
 58        t0 = len(row) - horizon - 1
 59        hist = list(row[t0 - m + 1:t0 + 1])
 60        pred = []
 61        for _ in range(horizon):
 62            y = float(np.dot(w, np.asarray(hist[-m:][::-1])))
 63            pred.append(y)
 64            hist.append(y)
 65        vals.append(np.mean((np.asarray(pred) - row[t0 + 1:t0 + 1 + horizon]) ** 2))
 66    return float(np.mean(vals))
 67
 68
 69def theoretical_crossing(a):
 70    return int(math.ceil(math.log(EPS) / math.log(abs(a))))
 71
 72
 73def theoretical_tau(a):
 74    # Discrete analogue of integral |rho| for rho(k)=a^k, including k=0.
 75    return 1.0 / (1.0 - abs(a))
 76
 77
 78def run():
 79    rows = []
 80    for a in (.2, .5, .7, .8, .9):
 81        x, z = make_data(a)
 82        rho = closure_rho(z)
 83        observed = first_persistent_crossing(rho)
 84        pred = theoretical_crossing(a)
 85        tau_obs = float(np.sum(np.abs(rho)))
 86        tau_pred = theoretical_tau(a)
 87        results = []
 88        for m in CANDIDATES:
 89            w, one = fit_history_predictor(x, m)
 90            results.append({'m': m, 'one_step': one, 'rollout': rollout_error(x, w, m)})
 91        scheduled = max(1, min(32, observed))
 92        ws, one = fit_history_predictor(x, scheduled)
 93        best = min(results, key=lambda q: q['one_step'])
 94        # Smallest window retaining at least 95% of the improvement from m=1 to m=32.
 95        e1, emax = results[0]['one_step'], results[-1]['one_step']
 96        target = e1 - .95 * (e1 - emax)
 97        elbow = next(q['m'] for q in results if q['one_step'] <= target)
 98        rows.append({'a': a, 'predicted_crossing': pred, 'observed_crossing': observed,
 99                     'predicted_tau': tau_pred, 'observed_tau': tau_obs,
100                     'scheduled_m': scheduled, 'elbow_m_95pct': elbow, 'best_m': best['m'],
101                     'results': results, 'scheduled_one_step': one,
102                     'scheduled_rollout': rollout_error(x, ws, scheduled)})
103
104    cross_err = [abs(q['observed_crossing'] - q['predicted_crossing']) for q in rows]
105    tau_rel_err = [abs(q['observed_tau'] - q['predicted_tau']) / q['predicted_tau'] for q in rows]
106    # Compare adaptive scheduler to Markov baseline and full 32-token model.
107    baseline = float(np.mean([q['results'][0]['one_step'] for q in rows]))
108    idea = float(np.mean([q['scheduled_one_step'] for q in rows]))
109    full = float(np.mean([q['results'][-1]['one_step'] for q in rows]))
110    summary = {
111        'crossing_mae_steps': float(np.mean(cross_err)),
112        'crossing_max_error_steps': int(max(cross_err)),
113        'tau_relative_mae': float(np.mean(tau_rel_err)),
114        'mean_baseline_m1_loss': baseline,
115        'mean_scheduler_loss': idea,
116        'mean_full_m32_loss': full,
117        'scheduler_vs_m1_percent': 100 * (baseline - idea) / baseline,
118        'scheduler_vs_m32_percent': 100 * (idea - full) / full,
119        'mean_scheduler_m': float(np.mean([q['scheduled_m'] for q in rows])),
120        'mean_elbow_m': float(np.mean([q['elbow_m_95pct'] for q in rows]))
121    }
122    out = {'config': {'epsilon': EPS, 'persistence': PERSISTENCE, 'seed': SEED,
123                      'candidates': CANDIDATES}, 'summary': summary, 'rows': rows}
124    Path('results.json').write_text(json.dumps(out, indent=2))
125    print(json.dumps(out, indent=2))
126
127
128if __name__ == '__main__':
129    run()