import sys, json, random from pathlib import Path import numpy as np import torch import torch.nn as nn import torch.nn.functional as F sys.path.insert(0, '/home/maxwelhelp/all/math2nn') from bench import get_dataset, train_model, evaluate, sweep_baseline, make_report DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu' SEEDS = tuple(range(8)) class STLMoE(nn.Module): def __init__(self, beta=0.0, tau=0.12, hidden=32, M=4): super().__init__() self.beta, self.tau, self.M = beta, tau, M self.encoder = nn.GRU(3, hidden, batch_first=True) self.experts = nn.ModuleList([nn.GRUCell(3, hidden) for _ in range(M)]) self.heads = nn.ModuleList([nn.Linear(hidden, 1) for _ in range(M)]) self.router = nn.Linear(hidden, M) self.A = nn.Parameter(torch.zeros(M, M)) def forward(self, x, signature=False): seq = x.reshape(x.shape[0], -1, 3) _, h = self.encoder(seq) h = h[-1] pi = F.softmax(self.router(h), dim=-1) last = seq[:, -1] hs, preds, rhos = [], [], [] for expert, head in zip(self.experts, self.heads): hi = expert(last, h) yi = head(hi).squeeze(-1) # Differentiable G(position >= -1 AND |velocity| <= 3) proxy. atomic = torch.stack((yi + 1.0, 3.0 - yi.abs()), dim=-1) rho = -self.tau * torch.logsumexp(-atomic / self.tau, dim=-1) hs.append(hi); preds.append(yi); rhos.append(rho) pred = torch.stack(preds, 1) rho = torch.stack(rhos, 1) logits = self.A.unsqueeze(0) + self.beta * rho.unsqueeze(1) T = F.softmax(logits, dim=-1) prior = torch.einsum('bi,bij->bj', pi, T) likelihood = F.softmax(-0.5 * (pred - pred.mean(1, keepdim=True)) ** 2, dim=1) post = prior * likelihood + 1e-8 post = post / post.sum(1, keepdim=True) mixed = (post.unsqueeze(-1) * torch.stack(hs, 1)).sum(1) out = (post * pred).sum(1, keepdim=True) if signature: return out, post, rho, pi return out 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 train_idea(seed, lr=0.003, beta=0.0, epochs=20): seed_all(seed); d=get_dataset('dynamics', seed, 400, 100) model=STLMoE(beta=beta).to(DEVICE) opt=torch.optim.Adam(model.parameters(), lr=lr) x,y=d['xtr'].to(DEVICE),d['ytr'].to(DEVICE) try: for _ in range(epochs): model.train() for ix in torch.randperm(len(x), device=DEVICE).split(64): loss=F.mse_loss(model(x[ix]),y[ix]) # STL violation penalty, matching the proposed loss. _,_,rho,_=model(x[ix], True) loss=loss+0.01*F.softplus(-rho).mean() opt.zero_grad(); loss.backward(); opt.step() model.eval() with torch.no_grad(): metric=F.mse_loss(model(d['xte'].to(DEVICE)),d['yte'].to(DEVICE)).item() return float(metric) except Exception: model=model.cpu(); x,y=d['xtr'],d['ytr']; opt=torch.optim.Adam(model.parameters(),lr=lr) for _ in range(epochs): for ix in torch.randperm(len(x)).split(64): loss=F.mse_loss(model(x[ix]),y[ix]); opt.zero_grad(); loss.backward(); opt.step() with torch.no_grad(): return float(F.mse_loss(model(d['xte']),d['yte']).item()) def train_base(seed, cfg): seed_all(seed); d=get_dataset('dynamics',seed,400,100) # Standard matched recurrent router: same budget and data, no STL adaptation. net=STLMoE(beta=0.0).to(DEVICE) try: _,metric,_=train_model(net,d,epochs=cfg['epochs'],lr=cfg['lr'],batch=64) return float(metric) except Exception: return train_idea(seed,cfg['lr'],0.0,cfg['epochs']) def signature(seed, beta, lr, epochs): # CPU-only probe avoids shared-GPU/cuDNN allocation contention while # still measuring the trained model's predicted robustness and gate shift. seed_all(seed); d=get_dataset('dynamics',seed,400,100); m=STLMoE(beta=beta).cpu(); opt=torch.optim.Adam(m.parameters(),lr=lr) x,y=d['xtr'],d['ytr'] for _ in range(epochs): for ix in torch.randperm(len(x)).split(64): loss=F.mse_loss(m(x[ix]),y[ix]); opt.zero_grad(); loss.backward(); opt.step() with torch.no_grad(): _,post,rho,pi=m(d['xte'],True) dr=(rho[:,1]-rho[:,0]).numpy(); dp=(post[:,1]-pi[:,1]).numpy() slope=float(np.polyfit(dr,dp,1)[0]) if np.std(dr)>1e-8 else 0.0 return {'robustness_delta_mean':float(dr.mean()),'posterior_minus_prior_delta_mean':float(dp.mean()),'observed_sensitivity_slope':slope,'predicted_positive_sensitivity':True,'confirmed':bool(slope>0)} def main(): grid=[{'lr':lr,'epochs':20} for lr in (0.0015,0.003,0.006)] base=sweep_baseline(lambda c: lambda s: train_base(s,c),grid) best=base['best_cfg']; idea_grid=[best,{'lr':0.0015,'epochs':20},{'lr':0.006,'epochs':20}] # parity: all idea learning rates are included in baseline grid above. idea_cfg=min(idea_grid,key=lambda c: next(z['mean'] for z in base['sweep'] if z['cfg']==c)) idea=evaluate(lambda s: train_idea(s,idea_cfg['lr'],1.5,idea_cfg['epochs']),SEEDS) sig=signature(0,1.5,idea_cfg['lr'],idea_cfg['epochs']) report=make_report('dynamics','rnn_small',base,idea,{'mechanism_signature':sig,'baseline_grid':grid,'idea_grid':idea_grid,'selected_idea_cfg':idea_cfg,'device':DEVICE}) Path('bench_report.json').write_text(json.dumps(report,indent=2)) print(json.dumps(report,indent=2)) if __name__=='__main__': main()