Fourier-Tumble Oscillatory Memory / stage2_bench.py
Mechanism confirmed, baseline not beaten
1import json, math, 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, make_model, train_model, evaluate, sweep_baseline, make_report
9
10SEEDS = tuple(range(8))
11SWEEP_SEEDS = (0, 1, 2, 3)
12GRID = [
13 {'lr': 1e-3, 'weight_decay': 0.0},
14 {'lr': 3e-3, 'weight_decay': 0.0},
15 {'lr': 1e-2, 'weight_decay': 0.0},
16]
17EPOCHS = 18
18BATCH = 128
19
20
21def seed_all(seed):
22 random.seed(seed)
23 np.random.seed(seed)
24 torch.manual_seed(seed)
25 if torch.cuda.is_available():
26 try:
27 torch.cuda.manual_seed_all(seed)
28 except Exception:
29 pass
30
31
32class FourierTumbleRNN(nn.Module):
33 """Drop-in recurrent replacement for bench rnn_small.
34
35 The official dynamics track supplies flattened (8,3) sequences. Each of
36 32 two-dimensional channels has its own softmax circular distribution;
37 its first Fourier coefficient induces exact damped-rotation dynamics.
38 """
39 def __init__(self, input_dim=3, hidden=64, bins=24):
40 super().__init__()
41 assert hidden % 2 == 0
42 self.hidden = hidden
43 self.channels = hidden // 2
44 self.bins = bins
45 self.inp = nn.Linear(input_dim, hidden)
46 self.bias_in = nn.Linear(input_dim, hidden, bias=False)
47 self.head = nn.Linear(hidden, 1)
48 self.logits = nn.Parameter(torch.zeros(self.channels, bins))
49 self.log_alpha = nn.Parameter(torch.full((self.channels,), math.log(0.25)))
50 self.register_buffer('angles', torch.linspace(-math.pi, math.pi, bins + 1)[:-1])
51
52 def induced(self):
53 q = torch.softmax(self.logits, dim=-1)
54 pi = (q * torch.exp(1j * self.angles)).sum(dim=-1)
55 alpha = torch.nn.functional.softplus(self.log_alpha) + 1e-5
56 gamma = alpha * (1.0 - pi.real)
57 omega = alpha * pi.imag
58 rho = torch.exp(-gamma)
59 c, s = torch.cos(omega), torch.sin(omega)
60 A = torch.zeros(self.channels, 2, 2, device=self.logits.device)
61 A[:, 0, 0] = rho * c
62 A[:, 0, 1] = -rho * s
63 A[:, 1, 0] = rho * s
64 A[:, 1, 1] = rho * c
65 return A, pi, gamma, omega, rho
66
67 def forward(self, x, return_states=False):
68 seq = x.view(x.shape[0], -1, 3)
69 h = torch.zeros(x.shape[0], self.hidden, device=x.device)
70 states = []
71 A, _, _, _, _ = self.induced()
72 for t in range(seq.shape[1]):
73 u = self.inp(seq[:, t]) + self.bias_in(seq[:, t])
74 hp = h.view(x.shape[0], self.channels, 2)
75 hp = torch.einsum('cij,bcj->bci', A, hp)
76 h = (hp + u.view(x.shape[0], self.channels, 2)).reshape(x.shape[0], self.hidden)
77 states.append(h)
78 out = self.head(h)
79 if return_states:
80 return out, torch.stack(states, dim=1)
81 return out
82
83
84def run(kind, cfg, seed, capture=False):
85 seed_all(seed)
86 ds = get_dataset('dynamics', seed=seed, n_train=400, n_test=200)
87 if kind == 'baseline':
88 model = make_model('rnn_small', ds['input_shape'], ds['out_dim'])
89 else:
90 model = FourierTumbleRNN(input_dim=3, hidden=64, bins=24)
91 model, metric, hist = train_model(
92 model, ds, epochs=EPOCHS, lr=float(cfg['lr']), batch=BATCH,
93 weight_decay=float(cfg.get('weight_decay', 0.0)), log=lambda *a, **k: None)
94 if model is None or metric is None:
95 return (float('nan'), None, ds) if capture else float('nan')
96 return (float(metric), model, ds) if capture else float(metric)
97
98
99def sanity_check():
100 seed_all(2703)
101 m = FourierTumbleRNN()
102 A, pi, gamma, omega, rho = m.induced()
103 p = pi.detach().cpu().numpy()
104 n = torch.linalg.matrix_norm(A, ord=2, dim=(1, 2)).detach().cpu().numpy()
105 return {
106 'predictions': {'abs_Pi_le_1': True, 'Jacobian_norm_le_1': True,
107 'strict_contraction_when_gamma_positive': True},
108 'max_abs_Pi': float(np.max(np.abs(p))),
109 'max_jacobian_norm': float(np.max(n)),
110 'min_gamma': float(gamma.min().detach().cpu()),
111 'all_abs_Pi_le_1': bool(np.max(np.abs(p)) <= 1.0 + 1e-6),
112 'all_jacobian_norms_le_1': bool(np.max(n) <= 1.0 + 1e-6),
113 }
114
115
116def mechanism_signature(cfg):
117 rows = []
118 for seed in SEEDS:
119 metric, model, ds = run('idea', cfg, seed, capture=True)
120 if model is None:
121 continue
122 model.eval()
123 with torch.no_grad():
124 A, pi, gamma, omega, rho = model.induced()
125 # Empirical homogeneous response of the trained transition: use
126 # random states and measure ||A h||/||h|| over many samples.
127 h = torch.randn(512, model.channels, 2, device=A.device)
128 nxt = torch.einsum('cij,bcj->bci', A, h)
129 ratios = torch.linalg.vector_norm(nxt, dim=-1) / torch.linalg.vector_norm(h, dim=-1).clamp_min(1e-8)
130 observed = float(ratios.max().cpu())
131 predicted = float(torch.linalg.matrix_norm(A, ord=2, dim=(1, 2)).max().cpu())
132 abs_pi = float(torch.abs(pi).max().cpu())
133 gmin = float(gamma.min().cpu())
134 rows.append({'seed': seed, 'metric': metric,
135 'predicted_norm': predicted, 'observed_max_ratio': observed,
136 'max_abs_pi': abs_pi, 'min_gamma': gmin})
137 max_obs = max(r['observed_max_ratio'] for r in rows)
138 max_pred = max(r['predicted_norm'] for r in rows)
139 max_pi = max(r['max_abs_pi'] for r in rows)
140 return {
141 'prediction': 'trained Fourier transition has ||rho R||_2=rho<=1 and |Pi_1|<=1',
142 'trained_model_samples': len(rows),
143 'predicted_max_norm': max_pred,
144 'observed_max_homogeneous_ratio': max_obs,
145 'observed_max_abs_pi': max_pi,
146 'within_tolerance': bool(max_obs <= 1.0001 and max_pi <= 1.0001),
147 'confirmed': bool(max_obs <= 1.0001 and max_pi <= 1.0001),
148 'per_seed': rows,
149 }
150
151
152def main():
153 math_sanity = sanity_check()
154 baseline = sweep_baseline(
155 lambda cfg: (lambda seed: run('baseline', cfg, seed)),
156 GRID, seeds=SWEEP_SEEDS)
157 idea_trials = []
158 for cfg in GRID:
159 result = evaluate(lambda seed, cfg=cfg: run('idea', cfg, seed), seeds=SEEDS)
160 idea_trials.append({'cfg': cfg, 'result': result})
161 best_idea = min(idea_trials, key=lambda z: z['result']['mean'])
162 idea_cfg, idea_result = best_idea['cfg'], best_idea['result']
163 signature = mechanism_signature(idea_cfg)
164 report = make_report(
165 'dynamics', 'rnn_small', baseline, idea_result,
166 extra={
167 'track_match': 'stability/control and recurrent memory -> official dynamics track',
168 'math_sanity': math_sanity,
169 'idea_sweep': idea_trials,
170 'selected_idea_cfg': idea_cfg,
171 'mechanism_signature': signature,
172 })
173 Path('bench_report.json').write_text(json.dumps(report, indent=2))
174 print(json.dumps(report, indent=2))
175
176
177if __name__ == '__main__':
178 main()