H-Infinity Disturbance-Attenuating Latent Observer / stage2_bench.py
Failed on benchmark
1import sys, json, random
2import numpy as np
3import torch
4import torch.nn as nn
5
6sys.path.insert(0, "/home/maxwelhelp/all/math2nn")
7from bench import get_dataset, make_model, train_model, evaluate, sweep_baseline, make_report
8
9SEEDS = tuple(range(8))
10LR_GRID = (1e-3, 3e-3, 6e-3)
11EPOCHS = 12
12NTRAIN, NTEST = 800, 300
13BATCH = 128
14LAMBDA = 0.01
15GAMMA = 0.50
16
17
18def seed_all(seed):
19 random.seed(seed)
20 np.random.seed(seed)
21 torch.manual_seed(seed)
22 if torch.cuda.is_available():
23 torch.cuda.manual_seed_all(seed)
24
25
26def baseline_fn(cfg):
27 def run(seed):
28 seed_all(seed)
29 ds = get_dataset("dynamics", seed=seed, n_train=NTRAIN, n_test=NTEST)
30 net = make_model("rnn_small", ds["input_shape"], ds["out_dim"])
31 _, metric, _ = train_model(net, ds, epochs=EPOCHS, lr=cfg["lr"],
32 batch=BATCH, log=lambda *_: None)
33 return float(metric)
34 return run
35
36
37def observer_train(seed, lr, return_signature=False):
38 """Train the identical GRU with an empirical bounded-real sensitivity penalty.
39
40 Input perturbations are the disturbance channel. For q equal to the scalar
41 prediction, ||dq/dx|| is the local induced gain. The penalty is the positive
42 part of gain^2-gamma^2, a differentiable finite-dimensional proxy for the
43 bounded-real inequality; the task MSE remains the primary objective.
44 """
45 seed_all(seed)
46 ds = get_dataset("dynamics", seed=seed, n_train=NTRAIN, n_test=NTEST)
47 net = make_model("rnn_small", ds["input_shape"], ds["out_dim"])
48 device = "cuda" if torch.cuda.is_available() else "cpu"
49 try:
50 net = net.to(device)
51 x = ds["xtr"].to(device)
52 y = ds["ytr"].to(device)
53 opt = torch.optim.Adam(net.parameters(), lr=lr)
54 lossf = nn.MSELoss()
55 for _ in range(EPOCHS):
56 net.train()
57 perm = torch.randperm(len(x), device=device)
58 for start in range(0, len(x), BATCH):
59 idx = perm[start:start+BATCH]
60 xb = x[idx].detach().requires_grad_(True)
61 pred = net(xb)
62 mse = lossf(pred, y[idx])
63 grad = torch.autograd.grad(pred.sum(), xb, create_graph=True)[0]
64 local_gain_sq = grad.reshape(len(idx), -1).pow(2).sum(1).mean()
65 penalty = torch.relu(local_gain_sq - GAMMA ** 2)
66 loss = mse + LAMBDA * penalty
67 opt.zero_grad(set_to_none=True)
68 loss.backward()
69 opt.step()
70 net.eval()
71 with torch.no_grad():
72 metric = float(((net(ds["xte"].to(device)) - ds["yte"].to(device)) ** 2).mean())
73 if return_signature:
74 # Measured on the trained model, not an analytical identity.
75 xt = ds["xte"][:64].to(device).detach().requires_grad_(True)
76 out = net(xt)
77 g = torch.autograd.grad(out.sum(), xt)[0]
78 gains = g.reshape(len(xt), -1).norm(dim=1).detach().cpu().numpy()
79 return metric, float(np.mean(gains)), float(np.quantile(gains, .95))
80 return metric
81 except RuntimeError:
82 if device == "cuda":
83 torch.cuda.empty_cache()
84 # Explicit CPU fallback with same seed and settings.
85 return observer_train_cpu(seed, lr, return_signature)
86 raise
87
88
89def observer_train_cpu(seed, lr, return_signature=False):
90 seed_all(seed)
91 ds = get_dataset("dynamics", seed=seed, n_train=NTRAIN, n_test=NTEST)
92 net = make_model("rnn_small", ds["input_shape"], ds["out_dim"])
93 opt = torch.optim.Adam(net.parameters(), lr=lr)
94 for _ in range(EPOCHS):
95 perm = torch.randperm(len(ds["xtr"]))
96 for start in range(0, len(perm), BATCH):
97 idx = perm[start:start+BATCH]
98 xb = ds["xtr"][idx].detach().requires_grad_(True)
99 pred = net(xb)
100 mse = ((pred - ds["ytr"][idx]) ** 2).mean()
101 grad = torch.autograd.grad(pred.sum(), xb, create_graph=True)[0]
102 gain2 = grad.reshape(len(idx), -1).pow(2).sum(1).mean()
103 loss = mse + LAMBDA * torch.relu(gain2 - GAMMA ** 2)
104 opt.zero_grad(set_to_none=True); loss.backward(); opt.step()
105 net.eval()
106 xt = ds["xte"][:64].detach().requires_grad_(True)
107 out = net(xt)
108 metric = float(((out - ds["yte"][:64]) ** 2).mean())
109 if return_signature:
110 g = torch.autograd.grad(out.sum(), xt)[0]
111 gains = g.reshape(len(xt), -1).norm(dim=1).detach().numpy()
112 return metric, float(gains.mean()), float(np.quantile(gains, .95))
113 return metric
114
115
116def main():
117 grid = [{"lr": x} for x in LR_GRID]
118 base = sweep_baseline(baseline_fn, grid, seeds=(0, 1, 2, 3))
119 idea_cfg = base["best_cfg"]
120 # Same union of learning rates as baseline; report best idea configuration.
121 idea_results = []
122 for lr in LR_GRID:
123 r = evaluate(lambda s, lr=lr: observer_train(s, lr), seeds=SEEDS)
124 idea_results.append((r, lr))
125 idea, idea_lr = min(idea_results, key=lambda z: z[0]["mean"])
126 sig = [observer_train(s, idea_lr, True) for s in SEEDS]
127 def baseline_gain(seed):
128 seed_all(seed)
129 ds = get_dataset("dynamics", seed=seed, n_train=NTRAIN, n_test=NTEST)
130 net = make_model("rnn_small", ds["input_shape"], ds["out_dim"])
131 net, metric, _ = train_model(net, ds, epochs=EPOCHS, lr=idea_lr,
132 batch=BATCH, log=lambda *_: None)
133 dev = next(net.parameters()).device
134 xt = ds["xte"][:64].to(dev).detach().requires_grad_(True)
135 out = net(xt)
136 g = torch.autograd.grad(out.sum(), xt)[0]
137 gains = g.reshape(len(xt), -1).norm(dim=1).detach().cpu().numpy()
138 return float(metric), float(gains.mean()), float(np.quantile(gains, .95))
139 base_beh = [baseline_gain(s) for s in SEEDS]
140 signature = {
141 "claim": "bounded-real attenuation reduces local input-disturbance gain",
142 "gamma": GAMMA,
143 "idea_lr": idea_lr,
144 "baseline_observed_mean_local_gain": float(np.mean([x[1] for x in base_beh])),
145 "idea_observed_mean_local_gain": float(np.mean([x[1] for x in sig])),
146 "baseline_observed_p95_local_gain": float(np.mean([x[2] for x in base_beh])),
147 "idea_observed_p95_local_gain": float(np.mean([x[2] for x in sig])),
148 "baseline_task_mse_at_same_lr": float(np.mean([x[0] for x in base_beh])),
149 "idea_task_mse_at_same_lr": float(np.mean([x[0] for x in sig])),
150 "confirmed": bool(np.mean([x[1] for x in sig]) < np.mean([x[1] for x in base_beh]) * 0.95)
151 }
152 report = make_report("dynamics", "rnn_small", base, idea,
153 {"mechanism_signature": signature,
154 "idea_sweep": [{"lr": lr, "mean": r["mean"]} for r, lr in idea_results],
155 "structural_match": "controlled damped pendulum; stability/control track"})
156 report["mechanism_signature"] = signature
157 with open("bench_report.json", "w") as f:
158 json.dump(report, f, indent=2)
159 print(json.dumps(report, indent=2))
160
161
162if __name__ == "__main__":
163 main()