import sys, json, random import numpy as np import torch import torch.nn as nn sys.path.insert(0, '/home/maxwelhelp/all/math2nn') from bench import get_dataset, train_model, make_report, sweep_baseline # Explicit state-space recurrent predictor: cubic term is the sole intervention. LAMBDA = 0.981 DT = 0.05 BETAS = (0.05, 0.15, 0.30) LRS = (1e-3, 3e-3, 1e-2) EPOCHS = 12 BATCH = 128 class DriftGRU(nn.Module): def __init__(self, beta): super().__init__() self.beta = float(beta) self.gru = nn.GRU(3, 32, batch_first=True) self.resid = nn.Sequential(nn.Linear(32, 32), nn.Tanh(), nn.Linear(32, 1)) def forward(self, x): seq = x.view(x.shape[0], 8, 3) _, h = self.gru(seq) correction = self.resid(h[-1]).squeeze(-1) theta, omega, u = seq[:, -1, 0], seq[:, -1, 1], seq[:, -1, 2] # One physical dt update; learned residual remains trainable on both sides. accel = -LAMBDA * theta - self.beta * theta.pow(3) - 0.15 * omega + 2.0 * u + correction return (theta + DT * (omega + DT * accel)).unsqueeze(1) def seed_all(seed): random.seed(seed); np.random.seed(seed); torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed) def run(beta, lr, seed, ntr=1200, nte=400): seed_all(seed) d = get_dataset('dynamics', seed, n_train=ntr, n_test=nte) net, metric, hist = train_model(DriftGRU(beta), d, epochs=EPOCHS, lr=lr, batch=BATCH, log=lambda *_: None) return float(metric), net, d def base_factory(cfg): return lambda seed: run(0.0, cfg['lr'], seed)[0] def idea_results(beta, lr): vals=[] for s in range(8): vals.append(run(beta, lr, s)[0]) return {'per_seed': vals, 'mean': float(np.mean(vals)), 'config': {'beta': beta, 'lr': lr}} def signature(beta, lr): # Measure trained map contraction on realistic states: q(x)=-(f(x+e)-f(x-e))/(2e*DT) vals=[] for s in range(8): m, net, d = run(beta, lr, s) net = net.to('cpu') net.eval() x=d['xte'][:128].clone() e=1e-3 xp=x.clone(); xm=x.clone(); xp[:,-3]+=e; xm[:,-3]-=e with torch.no_grad(): yp=net(xp); ym=net(xm) # map derivative in theta, converted to an effective restoring rate deriv=((yp-ym)/(2*e)).numpy().ravel() vals.extend(((1.0-deriv)/DT).tolist()) a=np.asarray(vals) return {'quantity':'effective theta restoring rate from trained one-step map', 'predicted_baseline_lower_bound': LAMBDA, 'observed_mean_rate':float(np.mean(a)), 'observed_median_rate':float(np.median(a)), 'observed_10th_percentile':float(np.percentile(a,10)), 'observed_fraction_above_lambda':float(np.mean(a>=LAMBDA)), 'beta':beta, 'confirmed': bool(np.mean(a)>=LAMBDA and np.percentile(a,10)>=LAMBDA-0.15)} def main(): # Baseline sweep on four seeds; union of all idea learning rates is included. grid=[{'lr':lr} for lr in LRS] base=sweep_baseline(base_factory, grid, seeds=(0,1,2,3)) best_lr=float(base['best_cfg']['lr']) # Idea sweep at the same three lrs, 8 paired seeds each; report best idea. candidates=[] for beta in BETAS: for lr in LRS: r=idea_results(beta, lr); candidates.append(r) idea=min(candidates, key=lambda r:r['mean']) rep=make_report('dynamics','rnn_small',base,idea, extra=signature(idea['config']['beta'],idea['config']['lr'])) rep['idea_sweep']=[{'config':r['config'],'mean':r['mean'],'per_seed':r['per_seed']} for r in candidates] rep['protocol_notes']={'n_train':1200,'n_test':400,'epochs':EPOCHS,'baseline_lr_union':list(LRS), 'structural_match':'controlled pendulum rollout; nonlinear restoring force is applied in the recurrent state update', 'baseline_best_lr':best_lr} with open('bench_report.json','w') as f: json.dump(rep,f,indent=2) print(json.dumps(rep,indent=2)) if __name__=='__main__': main()