import sys, json, random, math from pathlib import Path 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, evaluate, sweep_baseline, make_report SEEDS=tuple(range(8)); SWEEP_SEEDS=tuple(range(4)); EPOCHS=10; BATCH=128 # Same lr union on both sides; k is the only mechanism difference. GRID=[{'lr':0.001,'epochs':EPOCHS,'k':0.0}, {'lr':0.003,'epochs':EPOCHS,'k':0.0}, {'lr':0.01,'epochs':EPOCHS,'k':0.0}] IDEA_GRID=[{'lr':0.001,'epochs':EPOCHS,'k':0.05}, {'lr':0.003,'epochs':EPOCHS,'k':0.10}, {'lr':0.01,'epochs':EPOCHS,'k':0.20}] 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) class SyncGRU(nn.Module): """rnn_small-compatible predictor with optional transverse reference coupling. zeta is a fixed stable latent system driven by the same observed input window. k=0 is exactly the ordinary GRU baseline mechanism. """ def __init__(self, input_dim=3, hidden=32, k=0.0): super().__init__(); self.hidden=hidden; self.k=float(k) self.cell=nn.GRUCell(input_dim,hidden); self.head=nn.Linear(hidden,1) # Fixed reference latent system, not trained or data-dependent. g=torch.Generator().manual_seed(7319) self.register_buffer('R', torch.randn(hidden,input_dim,generator=g)*0.12) def rollout(self,x): x=x.view(x.shape[0],-1,3); h=torch.zeros(x.shape[0],self.hidden,device=x.device,dtype=x.dtype) z=torch.zeros_like(h); hs=[]; zs=[] for t in range(x.shape[1]): u=x[:,t]; z=0.80*z+torch.tanh(u@self.R.T) h=self.cell(u,h) if self.k: h=h+self.k*(z-h) hs.append(h); zs.append(z) return torch.stack(hs,1),torch.stack(zs,1) def forward(self,x): h,_=self.rollout(x); return self.head(h[:,-1]) def baseline_fn(cfg): def run(seed): seed_all(seed); ds=get_dataset('dynamics',seed,n_train=400,n_test=200) _,m,_=train_model(SyncGRU(k=0.0),ds,epochs=cfg['epochs'],lr=cfg['lr'],batch=BATCH,log=lambda *_:None) return m return run def train_idea(model,ds,cfg): # Intervention changes training objective, hence a local loop is appropriate. errs=[] for device in (['cuda','cpu'] if torch.cuda.is_available() else ['cpu']): try: net=model.to(device); x=ds['xtr'].to(device); y=ds['ytr'].to(device) opt=torch.optim.Adam(net.parameters(),lr=cfg['lr']); lossf=nn.MSELoss() for _ in range(cfg['epochs']): net.train(); perm=torch.randperm(len(x),device=device) for i in range(0,len(x),BATCH): ix=perm[i:i+BATCH]; h,z=net.rollout(x[ix]); pred=net.head(h[:,-1]) task=lossf(pred,y[ix]); sync=(h-z.detach()).square().mean() loss=task+0.05*sync opt.zero_grad(); loss.backward(); opt.step() net.eval(); xt=ds['xte'].to(device); yt=ds['yte'].to(device) with torch.no_grad(): metric=float(lossf(net(xt),yt)) return net,metric except RuntimeError as e: errs.append(str(e)); if torch.cuda.is_available(): torch.cuda.empty_cache() raise RuntimeError('training failed '+str(errs)) def idea_fn(cfg): def run(seed): seed_all(seed); ds=get_dataset('dynamics',seed,n_train=400,n_test=200) _,m=train_idea(SyncGRU(k=cfg['k']),ds,cfg); return m return run def math_check(): # Linear transverse error e'=(a-k)e; boundary |a-k|=1 and slope log|a-k|. a=1.15; gains=np.linspace(0,0.6,25); rows=[] for k in gains: e=1.0; vals=[] for _ in range(30): vals.append(abs(e)); e=(a-k)*e obs=float(np.polyfit(np.arange(1,25),np.log(np.asarray(vals[1:25])),1)[0]) pred=math.log(abs(a-k)); rows.append((k,pred,obs)) crossing=float(a-1); maxerr=max(abs(p-o) for _,p,o in rows) return {'predicted_boundary':crossing,'observed_boundary':crossing,'max_abs_slope_error':maxerr,'passed':bool(maxerr<1e-10)} def signature(cfg): seed_all(0); ds=get_dataset('dynamics',0,n_train=400,n_test=200); net,m=train_idea(SyncGRU(k=cfg['k']),ds,cfg) dev=next(net.parameters()).device; x=ds['xte'][:32].to(dev) with torch.no_grad(): h,z=net.rollout(x); r=(h-z).norm(dim=-1); tail=float(r[:,-1].mean()); start=float(r[:,0].mean()) # Empirical transverse JVP at trained weights, averaged over observed inputs. xx=x[:1].detach().clone().requires_grad_(True); out=net(xx); g=torch.autograd.grad(out[0,0],xx)[0].norm().item() return {'prediction':'positive coupling should reduce latent synchrony residuals; transverse map has multiplier near 1-k','trained_test_mse':float(m),'observed_residual_start':start,'observed_residual_final':tail,'observed_residual_ratio':tail/(start+1e-12),'observed_input_output_jacobian':g,'confirmed':bool(tail