Conservative Parallel-Edge Decomposition / bench_stage2.py
Mechanism confirmed, baseline not beaten
1import json, random, sys
2from pathlib import Path
3import numpy as np
4import torch
5from torch import nn
6
7sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
8from bench import get_dataset, train_model, sweep_baseline, make_report
9
10SEEDS = tuple(range(8))
11# Union of all learning rates is used by both baseline and idea.
12GRID = [
13 {'lr': 0.0015, 'epochs': 24},
14 {'lr': 0.0030, 'epochs': 24},
15 {'lr': 0.0060, 'epochs': 24},
16]
17B = torch.tensor([[-1.0], [1.0]], dtype=torch.float32)
18
19class EdgeTemporalBase(nn.Module):
20 """Matched small dynamics forecaster; only edge exchange differs."""
21 def __init__(self, idea=False, channels=2, hidden=32):
22 super().__init__()
23 self.idea, self.channels = idea, channels
24 self.temporal = nn.GRU(3, hidden, batch_first=True)
25 # Shared post-message state readout on both systems.
26 self.mix = nn.Linear(hidden + 2, hidden)
27 if idea:
28 self.channel_mlps = nn.ModuleList([
29 nn.Sequential(nn.Linear(3, 12), nn.Tanh(), nn.Linear(12, 1))
30 for _ in range(channels)
31 ])
32 else:
33 self.edge = nn.Sequential(nn.Linear(3, 24), nn.Tanh(), nn.Linear(24, 2))
34 self.head = nn.Linear(hidden, 1)
35 self._last_exchange = None
36
37 def forward(self, x):
38 seq = x.view(x.shape[0], 8, 3)
39 _, h = self.temporal(seq)
40 z = seq[:, -1, :] # endpoint/driver features of the final observed step
41 if self.idea:
42 flows = torch.cat([m(z) for m in self.channel_mlps], dim=1)
43 flow = flows.sum(dim=1, keepdim=True)
44 exchange = torch.einsum('nm,bm->bn', B.to(x.device), flow)
45 else:
46 exchange = self.edge(z)
47 self._last_exchange = exchange
48 fused = torch.tanh(self.mix(torch.cat([h[-1], exchange], dim=1)))
49 return self.head(fused)
50
51def seed_all(seed):
52 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
53 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
54
55def make_ds(seed):
56 return get_dataset('dynamics', seed, n_train=400, n_test=200)
57
58def train_one(seed, cfg, idea):
59 seed_all(seed)
60 ds = make_ds(seed)
61 model = EdgeTemporalBase(idea=idea)
62 _, metric, _ = train_model(model, ds, epochs=int(cfg['epochs']), lr=float(cfg['lr']), batch=128, log=lambda *_: None)
63 return float(metric) if metric is not None else float('nan')
64
65def make_fn(idea, cfg):
66 return lambda seed: train_one(seed, cfg, idea)
67
68def signature(cfg):
69 # Re-test the predicted identity on trained models, not an analytic toy.
70 rows = []
71 for seed in SEEDS:
72 seed_all(seed); ds = make_ds(seed)
73 model = EdgeTemporalBase(idea=True)
74 model, _, _ = train_model(model, ds, epochs=int(cfg['epochs']), lr=float(cfg['lr']), batch=128, log=lambda *_: None)
75 with torch.no_grad():
76 # Signature is evaluated on CPU to avoid shared-GPU/cuDNN allocation failures.
77 model = model.cpu()
78 x = ds['xte'][:128].cpu()
79 q = model(x)
80 ex = model._last_exchange
81 residual = ex.sum(dim=1).abs().mean().item()
82 rows.append({'seed': seed, 'mean_abs_internal_residual': residual,
83 'mean_abs_predicted_output': q.abs().mean().item()})
84 vals = [r['mean_abs_internal_residual'] for r in rows]
85 max_res = max(vals)
86 # In float32, <=1e-6 is an honest machine-scale conservation tolerance.
87 return {'quantity': 'trained-model mean absolute 1^T B P',
88 'predicted': 'zero internal net exchange', 'observed_max': max_res,
89 'observed_mean': float(np.mean(vals)), 'per_seed': rows,
90 'confirmed': bool(max_res <= 1e-6)}
91
92def main():
93 print('baseline sweep')
94 base = sweep_baseline(lambda cfg: make_fn(False, cfg), GRID, seeds=SEEDS)
95 best = base['best_cfg']
96 # Idea is evaluated at best baseline config and two nearby settings; the
97 # union is exactly GRID, and baseline sweep covered every setting.
98 idea = {'per_seed': [], 'configs': []}
99 for cfg in GRID:
100 r = __import__('bench').evaluate(make_fn(True, cfg), SEEDS)
101 idea['configs'].append({'cfg': cfg, 'result': r})
102 best_idea = min(idea['configs'], key=lambda z: z['result']['mean'])
103 idea_res = best_idea['result']; idea_res['selected_cfg'] = best_idea['cfg']
104 rep = make_report('dynamics', 'rnn_small', base, idea_res,
105 extra=signature(best_idea['cfg']))
106 rep['protocol'] = {'paired_seeds': list(SEEDS), 'grid': GRID,
107 'baseline_best_cfg': best, 'idea_grid_results': idea['configs']}
108 Path('bench_report.json').write_text(json.dumps(rep, indent=2))
109 print(json.dumps(rep, indent=2))
110
111if __name__ == '__main__': main()