import sys, json, math, random import numpy as np import torch from torch import nn sys.path.insert(0, '/home/maxwelhelp/all/math2nn') from bench import get_dataset, sweep_baseline, make_report SEEDS=(0,1,2,3,4,5,6,7) SWEEP_SEEDS=(0,1,2,3) Z=1.645 class MatchedRNN(nn.Module): # Matched recurrent architecture for both systems; only the loss differs. def __init__(self, input_dim, out_dim, hidden=64): super().__init__() self.cell=nn.GRUCell(3, hidden) self.head=nn.Linear(hidden, out_dim) self.hidden=hidden def forward(self, x, monitor=False, sigma=0.04, K=3): seq=x.view(x.shape[0],-1,3); B,T,_=seq.shape h=torch.zeros(B,self.hidden,device=x.device) if monitor: qs=torch.randn(K,B,self.hidden,device=x.device) qs=qs/(qs.norm(dim=2,keepdim=True)+1e-8); sums=[] sums=torch.zeros(K,B,device=x.device) for t in range(T): old=h.detach().requires_grad_(monitor) h=self.cell(seq[:,t],old) if monitor: # Directional finite-difference JVP (cheap monitor; detached update). newq=[] eps=1e-3 for k in range(K): vp=(self.cell(seq[:,t],old + eps*qs[k])-h)/eps if sigma: vp=vp + sigma*torch.randn_like(vp)*vp.detach().std().clamp_min(1e-4) n=vp.norm(dim=1)+1e-8 newq.append(vp/(n[:,None])); sums[k]=sums[k]+n.log() qs=torch.stack(newq) out=self.head(h) return (out,sums/T) if monitor else out def seed_all(s): random.seed(s); np.random.seed(s); torch.manual_seed(s) if torch.cuda.is_available(): torch.cuda.manual_seed_all(s) def run_one(seed, cfg, risk): seed_all(seed) ds=get_dataset('dynamics', seed, n_train=2000, n_test=500) dev='cuda' if torch.cuda.is_available() else 'cpu' try: torch.zeros(1,device=dev) except Exception: dev='cpu' net=MatchedRNN(np.prod(ds['input_shape']),ds['out_dim']).to(dev) x=ds['xtr'].to(dev); y=ds['ytr'].to(dev) opt=torch.optim.Adam(net.parameters(),lr=cfg['lr'],weight_decay=cfg.get('weight_decay',0.0)) bs=128 for ep in range(4): net.train(); perm=torch.randperm(len(x),device=dev) for j in range(0,len(x),bs): ix=perm[j:j+bs]; result=net(x[ix],monitor=(risk=='ucb'),sigma=cfg.get('sigma',.04),K=3) pred=result[0] if risk=='ucb' else result loss=((pred-y[ix])**2).mean() if risk=='ucb': lam=result[1]; mu=lam.mean(); sd=lam.std(unbiased=True) loss=loss+cfg['rho']*torch.relu(mu+Z*sd).square() opt.zero_grad(); loss.backward(); torch.nn.utils.clip_grad_norm_(net.parameters(),5.0); opt.step() net.eval() with torch.no_grad(): pred=net(ds['xte'].to(dev)); metric=float(((pred-ds['yte'].to(dev))**2).mean()) # Re-test behavior on trained models with fresh perturbations. vals=[] net.train() with torch.enable_grad(): for j in range(0,len(ds['xte']),128): _,lam=net(ds['xte'][j:j+128].to(dev),monitor=True,sigma=cfg.get('sigma',.04),K=8) vals.append(lam.detach().cpu().numpy().ravel()) a=np.concatenate(vals); mu=float(a.mean()); sd=float(a.std(ddof=1)) return metric, {'metric':metric,'ftle_mean':mu,'ftle_sd':sd,'positive_fraction':float((a>0).mean()),'ucb':mu+Z*sd} def main(): # Search-space parity: every idea lr is included in the baseline sweep. lrs=[0.001,0.003] base_grid=[{'lr':lr,'weight_decay':0.0} for lr in lrs] def base_fn(cfg): return lambda s: run_one(s,cfg,'base')[0] base=sweep_baseline(base_fn,base_grid,seeds=SWEEP_SEEDS) idea_grid=[{'lr':base['best_cfg']['lr'],'weight_decay':0.0,'rho':r,'sigma':.04} for r in (.05,.15,.30)] idea_blocks=[] for cfg in idea_grid: per=[]; sig=[] for s in SEEDS: m,z=run_one(s,cfg,'ucb'); per.append(m); sig.append(z) idea_blocks.append({'cfg':cfg,'res':{'mean':float(np.mean(per)),'std':float(np.std(per)),'per_seed':per,'n':len(per)},'sig':sig}) best=min(idea_blocks,key=lambda q:q['res']['mean']) # Official baseline best re-evaluation is already produced by sweep_baseline full. signature=[] for z in best['sig']: signature.append(z) mu=float(np.mean([z['ftle_mean'] for z in signature])); sd=float(np.mean([z['ftle_sd'] for z in signature])) pred=.5*math.erfc(-mu/(math.sqrt(2)*sd)); obs=float(np.mean([z['positive_fraction'] for z in signature])) extra={'prediction':'Gaussian p+=Phi(mu/sd) for finite-horizon FTLE','predicted_positive_fraction':pred,'observed_positive_fraction':obs,'absolute_error':abs(pred-obs),'confirmed':bool(abs(pred-obs)<=.10),'idea_sweep':[{k:v for k,v in b.items() if k!='sig'} for b in idea_blocks]} rep=make_report('dynamics','rnn_small',base,best['res'],extra) rep['official_architecture_note']='Matched GRUCell implementation exposes recurrent state for differentiable FTLE; baseline and idea share all parameters/optimizer/data.' with open('bench_report.json','w') as f: json.dump(rep,f,indent=2) print(json.dumps(rep,indent=2)) if __name__=='__main__': main()