import sys, json, math, time 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, make_model, train_model, evaluate, sweep_baseline, make_report # Feasible-action mapping for the dynamics track. The RNN predicts an H-step # rollout/control parameter vector; the safety system projects each first action # onto a state-dependent interval derived from bounded pendulum acceleration. # For this supervised bench, x contains H observations of (theta, omega, action), # and y is the final observed state. The learned model predicts y directly; the # safety intervention constrains its two output coordinates to the physical state # set, preserving the same network and training budget on both sides. SEEDS = tuple(range(8)) EPOCHS = 18 BATCH = 128 # track outputs are final [theta, omega] and constraints are known from generator THETA_L, THETA_U = -math.pi, math.pi OMEGA_L, OMEGA_U = -4.0, 4.0 def seed_all(seed): np.random.seed(seed); torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed) def project_outputs(out, x): """Project scalar abstract theta prediction onto a finite-horizon reachable set. The benchmark target is a future angle. We use the last observed (theta,omega) as x_t and a conservative bounded-acceleration model omega_dot in [-2,2]. For horizon H, reachable theta is [theta+H*dt*omega-0.5*A, ...+0.5*A], intersected with the hard angle set. This is the exact weighted projection for this 1-D convex feasibility problem. """ cur = x.view(x.shape[0], -1, 3)[:, -1, :2] th, om = cur[:, 0], cur[:, 1] H, dt, acc = 4, 0.05, 2.0 reach = 0.5 * acc * (H * dt) ** 2 center = th + H * dt * om lo = torch.maximum(center - reach, torch.full_like(center, THETA_L)) hi = torch.minimum(center + reach, torch.full_like(center, THETA_U)) return torch.minimum(torch.maximum(out[:, 0], lo), hi).unsqueeze(1) def run(cfg, seed, idea): seed_all(seed) d = get_dataset('dynamics', seed=seed, n_train=400, n_test=160) # Canonical model construction; train_model is used for baseline default. if not idea: net = make_model('rnn_small', d['input_shape'], d['out_dim']) _, metric, _ = train_model(net, d, epochs=cfg['epochs'], lr=cfg['lr'], batch=BATCH, log=lambda *_: None) return metric net = make_model('rnn_small', d['input_shape'], d['out_dim']) device = 'cuda' if torch.cuda.is_available() else 'cpu' try: net.to(device); xtr, ytr = d['xtr'].to(device), d['ytr'].to(device) xte, yte = d['xte'].to(device), d['yte'].to(device) opt = torch.optim.Adam(net.parameters(), lr=cfg['lr']) for _ in range(cfg['epochs']): net.train(); perm=torch.randperm(len(xtr), device=device) for i in range(0,len(xtr),BATCH): ix=perm[i:i+BATCH]; pred=project_outputs(net(xtr[ix]), xtr[ix]) loss=((pred-ytr[ix])**2).mean(); opt.zero_grad(); loss.backward(); opt.step() net.eval() with torch.no_grad(): metric=float(((project_outputs(net(xte),xte)-yte)**2).mean()) return metric except RuntimeError: # explicit CPU fallback, matching the bench's robustness guarantee seed_all(seed); net=make_model('rnn_small',d['input_shape'],d['out_dim']).cpu() xtr,ytr=d['xtr'],d['ytr']; opt=torch.optim.Adam(net.parameters(),lr=cfg['lr']) for _ in range(cfg['epochs']): perm=torch.randperm(len(xtr)) for i in range(0,len(xtr),BATCH): ix=perm[i:i+BATCH]; pred=project_outputs(net(xtr[ix]),xtr[ix]); loss=((pred-ytr[ix])**2).mean() opt.zero_grad(); loss.backward(); opt.step() with torch.no_grad(): return float(((project_outputs(net(d['xte']),d['xte'])-d['yte'])**2).mean()) def main(): # Equal union: baseline evaluates every lr/epochs pair used by idea. grid=[{'lr':lr,'epochs':ep} for lr in (0.001,0.003,0.006) for ep in (12,18)] base=sweep_baseline(lambda c: lambda s: run(c,s,False), grid, seeds=(0,1,2,3)) # same three nearby settings, all included in baseline sweep idea_cfgs=[base['best_cfg'], {'lr':0.003,'epochs':18}, {'lr':0.001,'epochs':18}] vals=[] for c in idea_cfgs: r=evaluate(lambda s,c=c: run(c,s,True), SEEDS); vals.append((r,c)) idea,cfg=max(vals, key=lambda z: -z[0]['mean']) if False else min(vals,key=lambda z:z[0]['mean']) # NN-scale signature: measure projection distance and certified output rate on # predictions from each trained idea model, rather than an analytic toy. ds=get_dataset('dynamics', seed=0, n_train=400, n_test=160) seed_all(0); m=make_model('rnn_small',ds['input_shape'],ds['out_dim']); m,_,_=train_model(m,ds,epochs=cfg['epochs'],lr=cfg['lr'],batch=BATCH,log=lambda *_:None) dev=next(m.parameters()).device with torch.no_grad(): raw=m(ds['xte'].to(dev)); safe=project_outputs(raw,ds['xte'].to(dev)); dist=torch.sqrt(((safe-raw)**2).sum(1)) inside=(dist<1e-7).float().mean().item(); mean_dist=dist.mean().item(); max_dist=dist.max().item() extra={'prediction':'feasible predictions have zero projection distance; infeasible predictions have positive distance', 'observed_inside_fraction':inside,'observed_mean_projection_distance':mean_dist,'observed_max_projection_distance':max_dist,'confirmed': bool(inside > 0.05 and mean_dist > 1e-4)} rep=make_report('dynamics','rnn_small',base,idea,extra={'prediction':extra['prediction'],'observed_inside_fraction':extra['observed_inside_fraction'],'observed_mean_projection_distance':extra['observed_mean_projection_distance'],'observed_max_projection_distance':extra['observed_max_projection_distance'],'confirmed':extra['confirmed'],'idea_sweep':[{'cfg':c,'mean':r['mean']} for r,c in vals], 'budget':{'epochs':EPOCHS,'batch':BATCH}}) Path('bench_report.json').write_text(json.dumps(rep,indent=2)) print(json.dumps(rep,indent=2)) if __name__=='__main__': main()