Delay-Shape Master-Stability Coupling / bench_delay_shape.py
Failed on benchmark
1import os, sys, json, math, 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, train_model, sweep_baseline, make_report
8
9SEEDS = tuple(range(8))
10SWEEP_SEEDS = (0, 1, 2, 3)
11NMOD, H, DELAY = 8, 16, 2
12SIGMA = 0.42
13
14
15def seed_all(seed):
16 random.seed(seed)
17 np.random.seed(seed)
18 torch.manual_seed(seed)
19 if torch.cuda.is_available():
20 torch.cuda.manual_seed_all(seed)
21
22
23def symmetric_ring(n=NMOD):
24 w = np.zeros((n, n), dtype=np.float32)
25 for i in range(n):
26 w[i, (i - 1) % n] = 0.5
27 w[i, (i + 1) % n] = 0.5
28 return w
29
30
31def directed_heterogeneous(n=NMOD, seed=17):
32 rng = np.random.default_rng(seed)
33 return rng.dirichlet(0.22 * np.ones(n), size=n).astype(np.float32)
34
35
36def transverse_radius(w):
37 vals = np.linalg.eigvals(w)
38 j = int(np.argmin(np.abs(vals - 1.0)))
39 return float(np.max(np.abs(np.delete(vals, j))))
40
41
42def companion_radius(a, alpha, delay):
43 c = np.zeros((delay + 1, delay + 1), dtype=complex if np.iscomplexobj(alpha) else float)
44 c[0, 0] = a
45 c[0, -1] = alpha
46 c[1:, :-1] = np.eye(delay)
47 return float(np.max(np.abs(np.linalg.eigvals(c))))
48
49
50def sanity_check():
51 # Constant-Jacobian modal recurrence: empirical exponent should equal log spectral radius.
52 a, alpha, d = 0.72, 0.15, 2
53 c = np.zeros((d + 1, d + 1))
54 c[0, 0], c[0, -1] = a, alpha
55 c[1:, :-1] = np.eye(d)
56 rng = np.random.default_rng(0)
57 v = rng.normal(size=d + 1); v /= np.linalg.norm(v)
58 logs = []
59 for _ in range(2500):
60 v = c @ v
61 z = np.linalg.norm(v)
62 logs.append(math.log(max(z, 1e-300)))
63 v /= max(z, 1e-300)
64 empirical = float(np.mean(logs[300:]))
65 predicted = math.log(companion_radius(a, alpha, d))
66 return {'predicted_log_rho': predicted, 'measured_exponent': empirical,
67 'absolute_error': abs(predicted - empirical),
68 'passed': abs(predicted - empirical) < 1e-5}
69
70
71class CoupledRNN(nn.Module):
72 def __init__(self, coupling, sigma=SIGMA, delay=DELAY, hidden=H):
73 super().__init__()
74 self.hidden = hidden
75 self.n = coupling.shape[0]
76 self.sigma = sigma
77 self.delay = delay
78 self.inp = nn.Linear(3, hidden)
79 self.cell = nn.GRUCell(hidden, hidden)
80 self.head = nn.Linear(hidden, 1)
81 self.register_buffer('coupling', torch.tensor(coupling))
82 self.last_disagreement = None
83
84 def forward(self, x):
85 b = x.shape[0]
86 seq = x.view(b, -1, 3)
87 hs = torch.zeros(b, self.n, self.hidden, device=x.device, dtype=x.dtype)
88 history = [hs.clone() for _ in range(self.delay + 1)]
89 disagreements = []
90 for t in range(seq.shape[1]):
91 z = self.inp(seq[:, t]).unsqueeze(1).expand(-1, self.n, -1)
92 local = self.cell(z.reshape(b * self.n, -1), hs.reshape(b * self.n, -1)).view(b, self.n, self.hidden)
93 delayed = history[0]
94 # Row-stochastic coupling preserves the synchronized mode; only topology differs.
95 hs = local + self.sigma * torch.einsum('ij,bjh->bih', self.coupling, delayed)
96 history = history[1:] + [hs.clone()]
97 disagreements.append((hs - hs.mean(dim=1, keepdim=True)).pow(2).mean())
98 self.last_disagreement = float(torch.stack(disagreements).detach().cpu()[-1])
99 return self.head(hs.mean(dim=1))
100
101
102def train_one(track, seed, cfg, topology):
103 seed_all(seed)
104 ds = get_dataset(track, seed, n_train=400, n_test=400)
105 model = CoupledRNN(topology, sigma=cfg['sigma'], delay=cfg['delay'])
106 _, metric, history = train_model(model, ds, epochs=cfg['epochs'], lr=cfg['lr'], batch=128)
107 return float(metric), model
108
109
110def evaluate_config(cfg, topology, seeds=SEEDS, keep_models=False):
111 vals, models = [], []
112 for s in seeds:
113 metric, model = train_one('dynamics', s, cfg, topology)
114 vals.append(metric)
115 if keep_models: models.append(model)
116 out = {'mean': float(np.mean(vals)), 'std': float(np.std(vals)),
117 'per_seed': [float(v) for v in vals], 'n': len(vals)}
118 if keep_models: out['models'] = models
119 return out
120
121
122def main():
123 sanity = sanity_check()
124 base_w = symmetric_ring()
125 idea_w = directed_heterogeneous()
126 # Shared union: every idea lr is also evaluated by baseline; topology is the method knob.
127 grid = [
128 {'lr': 0.001, 'epochs': 10, 'sigma': 0.42, 'delay': 2},
129 {'lr': 0.003, 'epochs': 10, 'sigma': 0.42, 'delay': 2},
130 {'lr': 0.006, 'epochs': 10, 'sigma': 0.42, 'delay': 2},
131 ]
132 def make_base(cfg):
133 return lambda seed: train_one('dynamics', seed, cfg, base_w)[0]
134 base = sweep_baseline(make_base, grid, seeds=SWEEP_SEEDS)
135 idea_candidates = [base['best_cfg'], grid[0], grid[2]]
136 idea_candidates = list({tuple(sorted(c.items())): c for c in idea_candidates}.values())
137 idea_runs = []
138 for cfg in idea_candidates:
139 r = evaluate_config(cfg, idea_w, SEEDS)
140 idea_runs.append({'cfg': cfg, 'result': r})
141 best = min(idea_runs, key=lambda z: z['result']['mean'])
142 idea = best['result']
143 # Re-test the trained systems' observed disagreement, not an analytical toy graph.
144 base_full = evaluate_config(base['best_cfg'], base_w, SEEDS, keep_models=True)
145 idea_full = evaluate_config(best['cfg'], idea_w, SEEDS, keep_models=True)
146 base_dis = float(np.mean([m.last_disagreement for m in base_full['models']]))
147 idea_dis = float(np.mean([m.last_disagreement for m in idea_full['models']]))
148 sig = {
149 'predicted_transverse_radius_baseline': transverse_radius(base_w),
150 'predicted_transverse_radius_idea': transverse_radius(idea_w),
151 'observed_final_disagreement_baseline': base_dis,
152 'observed_final_disagreement_idea': idea_dis,
153 'prediction': 'smaller transverse spectral radius should yield smaller trained hidden disagreement',
154 'confirmed': bool(idea_dis < base_dis)
155 }
156 # Remove model objects before JSON serialization and use the canonical report schema.
157 base_clean = dict(base); base_clean['full'] = base_full.copy(); base_clean['full'].pop('models', None)
158 report = make_report('dynamics', 'rnn_small', base_clean, idea, {
159 'math_sanity': sanity, 'trained_model_signature': sig,
160 'topology': {'baseline': 'symmetric reciprocal ring', 'idea': 'directed heterogeneous row-stochastic'}
161 })
162 report['idea_sweep'] = [{'cfg': z['cfg'], 'mean': z['result']['mean']} for z in idea_runs]
163 report['math_sanity'] = sanity
164 with open('bench_report.json', 'w') as f: json.dump(report, f, indent=2)
165 print(json.dumps(report, indent=2))
166
167
168if __name__ == '__main__':
169 main()