Nonlinear Noise-Tightening Drift / stage2_bench.py
Failed on benchmark
1import sys, json, random
2import numpy as np
3import torch
4import torch.nn as nn
5sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
6from bench import get_dataset, train_model, make_report, sweep_baseline
7
8# Explicit state-space recurrent predictor: cubic term is the sole intervention.
9LAMBDA = 0.981
10DT = 0.05
11BETAS = (0.05, 0.15, 0.30)
12LRS = (1e-3, 3e-3, 1e-2)
13EPOCHS = 12
14BATCH = 128
15
16class DriftGRU(nn.Module):
17 def __init__(self, beta):
18 super().__init__()
19 self.beta = float(beta)
20 self.gru = nn.GRU(3, 32, batch_first=True)
21 self.resid = nn.Sequential(nn.Linear(32, 32), nn.Tanh(), nn.Linear(32, 1))
22 def forward(self, x):
23 seq = x.view(x.shape[0], 8, 3)
24 _, h = self.gru(seq)
25 correction = self.resid(h[-1]).squeeze(-1)
26 theta, omega, u = seq[:, -1, 0], seq[:, -1, 1], seq[:, -1, 2]
27 # One physical dt update; learned residual remains trainable on both sides.
28 accel = -LAMBDA * theta - self.beta * theta.pow(3) - 0.15 * omega + 2.0 * u + correction
29 return (theta + DT * (omega + DT * accel)).unsqueeze(1)
30
31def seed_all(seed):
32 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
33 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
34
35def run(beta, lr, seed, ntr=1200, nte=400):
36 seed_all(seed)
37 d = get_dataset('dynamics', seed, n_train=ntr, n_test=nte)
38 net, metric, hist = train_model(DriftGRU(beta), d, epochs=EPOCHS, lr=lr, batch=BATCH, log=lambda *_: None)
39 return float(metric), net, d
40
41def base_factory(cfg):
42 return lambda seed: run(0.0, cfg['lr'], seed)[0]
43
44def idea_results(beta, lr):
45 vals=[]
46 for s in range(8): vals.append(run(beta, lr, s)[0])
47 return {'per_seed': vals, 'mean': float(np.mean(vals)), 'config': {'beta': beta, 'lr': lr}}
48
49def signature(beta, lr):
50 # Measure trained map contraction on realistic states: q(x)=-(f(x+e)-f(x-e))/(2e*DT)
51 vals=[]
52 for s in range(8):
53 m, net, d = run(beta, lr, s)
54 net = net.to('cpu')
55 net.eval()
56 x=d['xte'][:128].clone()
57 e=1e-3
58 xp=x.clone(); xm=x.clone(); xp[:,-3]+=e; xm[:,-3]-=e
59 with torch.no_grad():
60 yp=net(xp); ym=net(xm)
61 # map derivative in theta, converted to an effective restoring rate
62 deriv=((yp-ym)/(2*e)).numpy().ravel()
63 vals.extend(((1.0-deriv)/DT).tolist())
64 a=np.asarray(vals)
65 return {'quantity':'effective theta restoring rate from trained one-step map',
66 'predicted_baseline_lower_bound': LAMBDA,
67 'observed_mean_rate':float(np.mean(a)),
68 'observed_median_rate':float(np.median(a)),
69 'observed_10th_percentile':float(np.percentile(a,10)),
70 'observed_fraction_above_lambda':float(np.mean(a>=LAMBDA)),
71 'beta':beta, 'confirmed': bool(np.mean(a)>=LAMBDA and np.percentile(a,10)>=LAMBDA-0.15)}
72
73def main():
74 # Baseline sweep on four seeds; union of all idea learning rates is included.
75 grid=[{'lr':lr} for lr in LRS]
76 base=sweep_baseline(base_factory, grid, seeds=(0,1,2,3))
77 best_lr=float(base['best_cfg']['lr'])
78 # Idea sweep at the same three lrs, 8 paired seeds each; report best idea.
79 candidates=[]
80 for beta in BETAS:
81 for lr in LRS:
82 r=idea_results(beta, lr); candidates.append(r)
83 idea=min(candidates, key=lambda r:r['mean'])
84 rep=make_report('dynamics','rnn_small',base,idea,
85 extra=signature(idea['config']['beta'],idea['config']['lr']))
86 rep['idea_sweep']=[{'config':r['config'],'mean':r['mean'],'per_seed':r['per_seed']} for r in candidates]
87 rep['protocol_notes']={'n_train':1200,'n_test':400,'epochs':EPOCHS,'baseline_lr_union':list(LRS),
88 'structural_match':'controlled pendulum rollout; nonlinear restoring force is applied in the recurrent state update',
89 'baseline_best_lr':best_lr}
90 with open('bench_report.json','w') as f: json.dump(rep,f,indent=2)
91 print(json.dumps(rep,indent=2))
92if __name__=='__main__': main()