Drift-Balanced Adaptive Constraint Multiplier / stage2_bench.py
Mechanism confirmed, baseline not beaten
1import sys, json, random
2from pathlib import Path
3import numpy as np
4import torch
5import torch.nn as nn
6
7sys.path.insert(0, "/home/maxwelhelp/all/math2nn")
8from bench import get_dataset, make_model, evaluate, sweep_baseline, make_report
9
10TRACK = "dynamics"
11MODEL = "rnn_small"
12EPOCHS = 18
13BATCH = 64
14# Shared union: every idea learning rate is also present in baseline grid.
15LRS = [1e-3, 3e-3, 6e-3]
16PENALTIES = [0.0, 0.03, 0.1, 0.3, 1.0]
17ALPHAS = [0.02, 0.08, 0.20]
18TAU = 0.0
19V_CAP = 1.0
20LAMBDA_MAX = 5.0
21
22
23def seed_all(seed):
24 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
25 if torch.cuda.is_available():
26 torch.cuda.manual_seed_all(seed)
27
28
29def violation(pred):
30 """Terminal feasibility violation: excess absolute terminal angle over 1 rad."""
31 return torch.relu(pred.abs() - V_CAP)
32
33
34def projected_update(lam, vbar, alpha, tau=TAU, lam_max=LAMBDA_MAX):
35 return float(np.clip(lam + alpha * (vbar - tau), 0.0, lam_max))
36
37
38def train_one(seed, lr, mode, penalty=0.0, alpha=0.08, collect=False):
39 seed_all(seed)
40 ds = get_dataset(TRACK, seed, n_train=400, n_test=200)
41 net = make_model(MODEL, ds["input_shape"], ds["out_dim"])
42 use_cuda = torch.cuda.is_available()
43 device = torch.device("cuda" if use_cuda else "cpu")
44 try:
45 net = net.to(device)
46 x, y = ds["xtr"].to(device), ds["ytr"].to(device)
47 opt = torch.optim.Adam(net.parameters(), lr=lr)
48 mse = nn.MSELoss()
49 lam = 0.0
50 lambdas, vbars, drifts, predicted_drifts = [], [], [], []
51 for _ in range(EPOCHS):
52 net.train()
53 order = torch.randperm(len(x), device=device)
54 for start in range(0, len(x), BATCH):
55 idx = order[start:start+BATCH]
56 pred = net(x[idx])
57 vbar = float(violation(pred).detach().mean().cpu())
58 old = lam
59 if mode == "adaptive":
60 lam = projected_update(lam, vbar, alpha)
61 coeff = lam
62 else:
63 coeff = penalty
64 # Minimize prediction loss plus fixed/adaptive terminal constraint.
65 loss = mse(pred, y[idx]) + coeff * violation(pred).mean()
66 opt.zero_grad(set_to_none=True); loss.backward(); opt.step()
67 if mode == "adaptive":
68 lambdas.append(lam); vbars.append(vbar)
69 drifts.append(lam - old)
70 predicted_drifts.append(alpha * (vbar - TAU))
71 net.eval()
72 with torch.no_grad():
73 pred = net(ds["xte"].to(device))
74 metric = float(((pred - ds["yte"].to(device)) ** 2).mean().cpu())
75 test_v = float(violation(pred).mean().cpu())
76 if collect:
77 return metric, {"model": net, "test_violation": test_v,
78 "lambda": np.asarray(lambdas), "vbar": np.asarray(vbars),
79 "drift": np.asarray(drifts), "predicted_drift": np.asarray(predicted_drifts)}
80 return metric
81 except RuntimeError:
82 # Explicit CPU fallback for a shared/fragile CUDA slot.
83 if device.type == "cuda":
84 torch.cuda.empty_cache()
85 seed_all(seed)
86 old = torch.cuda.is_available
87 torch.cuda.is_available = lambda: False
88 try: return train_one(seed, lr, mode, penalty, alpha, collect)
89 finally: torch.cuda.is_available = old
90 raise
91
92
93def math_check():
94 rng = np.random.default_rng(2346)
95 err = 0.0
96 for _ in range(10000):
97 z = rng.uniform(-3, 8); cap = rng.uniform(.1, 8)
98 v = rng.uniform(-2, 3); a = rng.uniform(0, 2); tau = rng.uniform(-1, 1)
99 err = max(err, abs(projected_update(z, v, a, tau, cap) - min(cap, max(0., z+a*(v-tau)))))
100 # Direct interior identity, independently of the neural experiment.
101 lam, vb, a = 0.7, 0.4, 0.2
102 nxt = projected_update(lam, vb, a, 0., 5.)
103 return {"max_projection_error": err,
104 "interior_drift_error": abs((nxt-lam)-a*vb),
105 "constant_positive_drift_cap_steps": int(np.ceil(5.0/(.2*.4)))}
106
107
108def main():
109 print("math_check", json.dumps(math_check()))
110 # Baseline method is fixed terminal penalty; sweep both decisive penalty and lr.
111 grid = [{"lr": lr, "penalty": p} for lr in LRS for p in PENALTIES]
112 base = sweep_baseline(lambda c: lambda s: train_one(s, c["lr"], "fixed", c["penalty"]), grid)
113 best_lr = base["best_cfg"]["lr"]
114 # Idea uses baseline-best lr plus nearby lr values; all are in baseline union.
115 idea_grid = [{"lr": lr, "alpha": a} for lr in LRS for a in ALPHAS]
116 idea_scores = []
117 for cfg in idea_grid:
118 r = evaluate(lambda s, c=cfg: train_one(s, c["lr"], "adaptive", alpha=c["alpha"]),
119 seeds=(0,1,2,3))
120 idea_scores.append((r["mean"], cfg))
121 best_idea_cfg = min(idea_scores, key=lambda z: z[0])[1]
122 idea = evaluate(lambda s: train_one(s, best_idea_cfg["lr"], "adaptive", alpha=best_idea_cfg["alpha"]),
123 seeds=tuple(range(8)))
124 # Re-test trained models on all paired seeds to measure the mechanism signature.
125 sig_rows = []
126 for s in range(8):
127 _, h = train_one(s, best_idea_cfg["lr"], "adaptive", alpha=best_idea_cfg["alpha"], collect=True)
128 d = h["drift"]; pd = h["predicted_drift"]
129 sig_rows.append({"seed": s, "mean_violation": float(np.mean(h["vbar"][-20:])),
130 "mean_lambda": float(np.mean(h["lambda"][-20:])),
131 "drift_prediction_mae": float(np.mean(np.abs(d-pd))),
132 "drift_observed": float(np.mean(d)),
133 "drift_predicted": float(np.mean(pd)),
134 "cap_fraction": float(np.mean(h["lambda"] >= LAMBDA_MAX-1e-7))})
135 sig = {"formula": "delta_lambda = alpha*(vbar-tau) away from projection",
136 "per_seed": sig_rows,
137 "observed_mean_drift": float(np.mean([r["drift_observed"] for r in sig_rows])),
138 "predicted_mean_drift": float(np.mean([r["drift_predicted"] for r in sig_rows])),
139 "mean_drift_prediction_mae": float(np.mean([r["drift_prediction_mae"] for r in sig_rows])),
140 "confirmed": bool(np.mean([r["drift_prediction_mae"] for r in sig_rows]) < 1e-7)}
141 report = make_report(TRACK, MODEL, base, idea,
142 {"mechanism_signature": sig, "selected_idea_cfg": best_idea_cfg,
143 "math_check": math_check(),
144 "track_justification": "Dynamics is structurally matched: controlled pendulum rollout and terminal feasibility."})
145 Path("bench_report.json").write_text(json.dumps(report, indent=2))
146 print(json.dumps(report, indent=2))
147
148if __name__ == "__main__": main()