Turnpike-Calibrated Short-Window Training / run_experiment.py
Mechanism failed
1import json
2from pathlib import Path
3import numpy as np
4import torch
5
6SEED = 7
7np.random.seed(SEED)
8torch.manual_seed(SEED)
9
10
11def toy_check():
12 a = 0.8
13 ns = np.array([2, 4, 8, 16, 32, 64, 128, 256])
14 cumulative = np.array([(1-a**int(n))/(1-a) for n in ns])
15 mean_gap = cumulative / ns
16 slope = float(np.polyfit(np.log(ns[2:]), np.log(mean_gap[2:]), 1)[0])
17 return {'a': a, 'N': ns.tolist(), 'cumulative_gap': cumulative.tolist(),
18 'mean_gap': mean_gap.tolist(), 'loglog_slope': slope,
19 'theoretical_C': 1/(1-a)}
20
21
22def make_data(T=240, a=.82, b=.55, noise=.025):
23 rng = np.random.default_rng(SEED)
24 u = rng.normal(size=T).astype(np.float32)
25 x = np.zeros(T+1, dtype=np.float32); y = np.zeros(T, dtype=np.float32)
26 x[0] = .7
27 for k in range(T):
28 y[k] = x[k] + noise*rng.normal(); x[k+1] = a*x[k] + b*u[k]
29 return torch.tensor(u), torch.tensor(y)
30
31
32def rollout(theta, x0, u):
33 a, b = theta[0], theta[1]; x, out = x0, []
34 for uk in u:
35 out.append(x); x = a*x + b*uk
36 return torch.stack(out)
37
38
39def estimate_gap(theta, u, y, x0, horizon, inner_steps=20):
40 z = torch.nn.Parameter(x0.detach().clone())
41 inner = torch.optim.Adam([z], lr=.08)
42 uu, yy = u[:horizon], y[:horizon]
43 for _ in range(inner_steps):
44 inner.zero_grad(); loss = ((rollout(theta.detach(), z, uu)-yy)**2).mean()
45 loss.backward(); inner.step()
46 with torch.no_grad():
47 fixed, free = rollout(theta, x0, uu), rollout(theta, z, uu)
48 return (fixed-free).abs().sum().item(), ((free-yy)**2).mean().item(), float(z)
49
50
51def fit(method, u, y, steps=600, base_horizon=40, eps=.05, every=25):
52 theta = torch.nn.Parameter(torch.tensor([.35, .25])); opt = torch.optim.Adam([theta], lr=.018)
53 fixed_x0 = torch.tensor(0.0); chosen, chat = base_horizon, 0.0; hist = []
54 for step in range(steps):
55 if method == 'calibrated' and step % every == 0:
56 c, _, _ = estimate_gap(theta, u, y, fixed_x0, base_horizon)
57 chat = .8*chat + .2*c
58 chosen = min(base_horizon, max(4, int(np.ceil(chat/eps))))
59 n = base_horizon if method == 'full' else (8 if method == 'fixed_8' else chosen)
60 start = (3*step) % (len(u)-n+1)
61 loss = ((rollout(theta, fixed_x0, u[start:start+n])-y[start:start+n])**2).mean()
62 opt.zero_grad(); loss.backward(); torch.nn.utils.clip_grad_norm_([theta], 5.0); opt.step()
63 if step % 50 == 0: hist.append([step, float(loss), int(n), float(chat)])
64 with torch.no_grad(): mse = ((rollout(theta, fixed_x0, u)-y)**2).mean().item()
65 gap, free_loss, free_x0 = estimate_gap(theta, u, y, fixed_x0, base_horizon)
66 return {'mse': mse, 'theta': theta.detach().numpy().tolist(), 'mean_fixed_free_gap': gap/base_horizon,
67 'free_loss': free_loss, 'free_x0': free_x0, 'selected_horizon': int(chosen), 'history': hist}
68
69
70def empirical_sweep(theta, u, y):
71 ns = [2, 4, 8, 16, 32, 40]
72 gaps = [estimate_gap(theta, u, y, torch.tensor(0.0), n)[0]/n for n in ns]
73 slope = float(np.polyfit(np.log(ns[1:]), np.log(gaps[1:]), 1)[0])
74 return {'N': ns, 'mean_gap': gaps, 'loglog_slope': slope}
75
76
77def main():
78 u, y = make_data()
79 full = fit('full', u, y)
80 fixed = fit('fixed_8', u, y)
81 calibrated = fit('calibrated', u, y, eps=.05)
82 relaxed = fit('calibrated', u, y, eps=.20)
83 out = {'toy_check': toy_check(), 'training': {'full_40': full, 'fixed_8': fixed,
84 'calibrated_eps_.05': calibrated, 'calibrated_eps_.20': relaxed},
85 'empirical_gap_sweep_full_model': empirical_sweep(torch.tensor(full['theta']), u, y)}
86 Path('results.json').write_text(json.dumps(out, indent=2)); print(json.dumps(out, indent=2))
87
88if __name__ == '__main__': main()