import sys, json, random import numpy as np sys.path.insert(0, '/home/maxwelhelp/all/math2nn') import torch import torch.nn as nn import torch.nn.functional as F from bench import get_dataset, sweep_baseline, evaluate, make_report # Correct structural match: dynamics is the bench's stability/control task. # Same switching recurrent system on both sides; only the contraction loss differs. DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu' SEEDS = tuple(range(8)); M=4; RHO=.81; TAU=.05 class SwitchingCore(nn.Module): def __init__(self, hidden=16): super().__init__(); self.hidden=hidden self.A=nn.Parameter(torch.randn(M,hidden,hidden)*.12) self.B=nn.Parameter(torch.randn(M,3,hidden)*.12) self.b=nn.Parameter(torch.zeros(M,hidden)); self.head=nn.Linear(hidden,1) def step_all(self,z,x): # returns all M next states, vectorized over modes and batch return torch.tanh(torch.einsum('bh,mhk->mbk',z,self.A)+torch.einsum('bd,mdh->mbh',x,self.B)+self.b[:,None,:]) def forward(self,x,return_z=False): seq=x.view(x.shape[0],-1,3); z=torch.zeros(x.shape[0],self.hidden,device=x.device) for t in range(seq.shape[1]): allz=self.step_all(z,seq[:,t]); mode=torch.clamp(((seq[:,t,2]+1.5)/3*M).long(),0,M-1) z=allz[mode,torch.arange(z.shape[0],device=x.device)] return (self.head(z),z) if return_z else self.head(z) 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 train_one(seed,lr,lam,epochs=5,n_train=400,keep=False): seed_all(seed); ds=get_dataset('dynamics',seed,n_train=n_train,n_test=400) net=SwitchingCore().to(DEVICE); opt=torch.optim.Adam(net.parameters(),lr=lr) x,y=ds['xtr'].to(DEVICE),ds['ytr'].to(DEVICE) for _ in range(epochs): net.train(); p=torch.randperm(len(x),device=DEVICE) for j in range(0,len(x),128): ix=p[j:j+128]; pred=net(x[ix]); task=((pred-y[ix])**2).mean(); loss=task if lam: z=torch.randn(len(ix),net.hidden,device=DEVICE); x0=torch.zeros(len(ix),3,device=DEVICE) nxt=net.step_all(z,x0) e=(nxt*nxt).sum(2)-RHO*(z*z).sum(1)[None,:] loss=task+lam*F.softplus(e/TAU).mean()*TAU opt.zero_grad(); loss.backward(); torch.nn.utils.clip_grad_norm_(net.parameters(),5.); opt.step() net.eval() with torch.no_grad(): metric=float(((net(ds['xte'].to(DEVICE))-ds['yte'].to(DEVICE))**2).mean()) return (metric,net) if keep else metric def fn(lr,lam): return lambda seed: train_one(seed,lr,lam) def mechanism_signature(lr,lam): ratios=[] for s in SEEDS: _,net=train_one(s,lr,lam,keep=True) with torch.no_grad(): z=torch.randn(128,net.hidden,device=DEVICE); x0=torch.zeros(128,3,device=DEVICE) q=(net.step_all(z,x0)**2).sum(2)/((z*z).sum(1)[None,:]+1e-8) ratios += q.cpu().numpy().ravel().tolist() mx=float(np.max(ratios)); mean=float(np.mean(ratios)) return {'certificate':'V(z)=||z||^2; one-node path-complete graph; four all-mode self-edges', 'prediction':'edge contraction ratio <= rho', 'rho':RHO, 'observed_max_zero_input_ratio':mx,'observed_mean_ratio':mean, 'confirmed':bool(mx <= RHO*1.25),'n_trained_models':8} def main(): # All idea learning rates appear in the baseline grid: search-space parity. grid=[{'lr':v,'weight_decay':0.0} for v in (.001,.003,.01)] base=sweep_baseline(lambda c:fn(c['lr'],0.),grid,seeds=(0,1,2,3)) lr=base['best_cfg']['lr'] idea_sweep=[] for lam in (.01,.05,.20): idea_sweep.append({'cfg':{'lr':lr,'lambda_contract':lam},'result':evaluate(fn(lr,lam),SEEDS)}) best=min(idea_sweep,key=lambda q:q['result']['mean']) rep=make_report('dynamics','switching_rnn_small',base,best['result'], {'signature':mechanism_signature(lr,best['cfg']['lambda_contract']), 'idea_cfg':best['cfg'], 'training_budget':'400 train/400 test samples, 5 epochs, batch 128; equal on both systems'}) rep['idea_sweep']=idea_sweep; rep['custom_track']=None; rep['device']=DEVICE with open('bench_report.json','w') as f: json.dump(rep,f,indent=2) print(json.dumps(rep,indent=2)) if __name__=='__main__': main()