Degree-Calibrated Stable Residual Flow / bench_degree_flow.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
6sys.path.insert(0, "/home/maxwelhelp/all/math2nn")
7from bench import get_dataset, train_model, sweep_baseline, make_report
8
9SEEDS = tuple(range(8))
10SWEEP_SEEDS = tuple(range(4))
11EPOCHS = 18
12BATCH = 128
13DT = 0.25
14HIDDEN = 64
15# The union is used on both sides, satisfying step-size/lr parity.
16GRID = [
17 {"lr": 0.0015, "dt": DT, "m": 0},
18 {"lr": 0.0030, "dt": DT, "m": 0},
19 {"lr": 0.0060, "dt": DT, "m": 0},
20]
21
22class ResidualRNN(nn.Module):
23 """Shared residual recurrent architecture; only radial mechanism differs."""
24 def __init__(self, idea=False, dt=DT, m=0, a=1.0):
25 super().__init__()
26 self.inp = nn.Linear(3, HIDDEN)
27 self.rec = nn.Linear(HIDDEN, HIDDEN)
28 self.head = nn.Linear(HIDDEN, 1)
29 self.idea, self.dt, self.m, self.a = idea, dt, m, a
30 self.last_states = None
31 self.last_fields = None
32
33 def field(self, z, x):
34 h = torch.tanh(self.rec(z) + self.inp(x))
35 if not self.idea:
36 return h
37 q = (z * z).sum(-1, keepdim=True)
38 r = 0.5 * q
39 # eps is only for numerical definition at the equilibrium.
40 tangent = h - z * (z * h).sum(-1, keepdim=True) / (q + 1e-8)
41 # Factor 1/2 gives z^T f = -a r^(m+1) for r=||z||^2/2.
42 return -0.5 * self.a * r.pow(self.m) * z + tangent
43
44 def forward(self, x):
45 seq = x.view(x.shape[0], -1, 3)
46 # Nonzero input-dependent initial state avoids the singular equilibrium
47 # while leaving the recurrent update itself as the intervention.
48 z = torch.tanh(self.inp(seq[:, 0]))
49 states, fields = [], []
50 for k in range(1, seq.shape[1]):
51 f = self.field(z, seq[:, k])
52 states.append(z)
53 fields.append(f)
54 z = z + self.dt * f
55 self.last_states = states
56 self.last_fields = fields
57 return self.head(z)
58
59def set_seed(seed):
60 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
61 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
62
63def ds(seed):
64 return get_dataset("dynamics", seed, n_train=1600, n_test=400)
65
66def train_side(seed, cfg, idea):
67 set_seed(seed)
68 net = ResidualRNN(idea=idea, dt=cfg["dt"], m=int(cfg.get("m", 0)))
69 _, metric, _ = train_model(net, ds(seed), epochs=EPOCHS, lr=cfg["lr"], batch=BATCH, log=lambda *_: None)
70 return float(metric)
71
72def base_factory(cfg):
73 return lambda seed: train_side(seed, cfg, False)
74
75def idea_factory(cfg):
76 return lambda seed: train_side(seed, cfg, True)
77
78def signature(cfg, seeds=SEEDS):
79 pred, obs, abs_err, norms = [], [], [], []
80 # Re-test the analytical prediction on trained benchmark models.
81 for seed in seeds:
82 set_seed(seed)
83 net = ResidualRNN(idea=True, dt=cfg["dt"], m=int(cfg.get("m", 0)))
84 net, _, _ = train_model(net, ds(seed), epochs=EPOCHS, lr=cfg["lr"], batch=BATCH, log=lambda *_: None)
85 net.eval()
86 with torch.no_grad():
87 _ = net(ds(seed)["xte"].to(next(net.parameters()).device))
88 for z, f in zip(net.last_states, net.last_fields):
89 r = 0.5 * (z*z).sum(-1, keepdim=True)
90 radial = (z*f).sum(-1, keepdim=True)
91 target = -net.a * r.pow(net.m + 1)
92 valid = r.squeeze(-1) > 1e-5
93 if valid.any():
94 pred.extend(target[valid].cpu().numpy().ravel().tolist())
95 obs.extend(radial[valid].cpu().numpy().ravel().tolist())
96 abs_err.extend((radial[valid]-target[valid]).abs().cpu().numpy().ravel().tolist())
97 norms.extend(z.norm(dim=-1).cpu().numpy().tolist())
98 p, o = np.asarray(pred), np.asarray(obs)
99 corr = float(np.corrcoef(p, o)[0,1]) if len(p)>2 else float("nan")
100 rel = float(np.mean(np.abs(o-p)/(np.abs(p)+1e-5))) if len(p) else float("nan")
101 # Quantitative confirmation is strict: near-zero radial residual and high agreement.
102 return {"quantity": "trained radial derivative vs -a*r^(m+1)",
103 "predicted_mean": float(p.mean()), "observed_mean": float(o.mean()),
104 "mean_abs_error": float(np.mean(abs_err)), "relative_abs_error": rel,
105 "correlation": corr, "n_samples": int(len(p)),
106 "hidden_norm_mean": float(np.mean(norms)),
107 "confirmed": bool(rel < 0.05 and corr > 0.99)}
108
109def main():
110 # Baseline sweep includes every lr used by idea; method knob dt is also shared.
111 base = sweep_baseline(base_factory, GRID, seeds=SWEEP_SEEDS)
112 best = base["best_cfg"]
113 idea_cfgs = [best, {"lr":0.0015,"dt":DT,"m":1}, {"lr":0.0060,"dt":DT,"m":1}]
114 idea_runs = []
115 for cfg in idea_cfgs:
116 r = {"cfg": cfg, "result": __import__('bench').evaluate(idea_factory(cfg), SEEDS)}
117 idea_runs.append(r)
118 chosen = min(idea_runs, key=lambda q:q["result"]["mean"])
119 report = make_report("dynamics", "residual_rnn_shared", base, chosen["result"],
120 {"mechanism_signature": signature(chosen["cfg"]),
121 "idea_sweep": idea_runs,
122 "custom_track": None,
123 "selection_note": "m=1 idea; baseline sweep and idea sweep use identical lr union and epochs"})
124 Path("bench_report.json").write_text(json.dumps(report, indent=2))
125 print(json.dumps(report, indent=2))
126
127if __name__ == "__main__": main()