Finite-Candidate Neural Reference Shield / stage2_bench.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import sys, json, itertools, time
  2from pathlib import Path
  3import numpy as np
  4import torch
  5sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  6from bench import get_dataset, make_model, train_model, evaluate, sweep_baseline, make_report
  7
  8SEEDS = tuple(range(8))
  9GRID = [{'lr': 1e-3}, {'lr': 3e-3}, {'lr': 5e-3}]
 10EPOCHS = 20
 11BATCH = 128
 12B = 1.0
 13
 14# Finite-candidate KKT shield for the scalar feasible interval [-B,B].
 15# Candidates are the empty active set z and the two one-constraint boundaries.
 16def interval_shield(z, bound=B):
 17    z = torch.as_tensor(z)
 18    return torch.minimum(torch.maximum(z, torch.as_tensor(-bound, dtype=z.dtype, device=z.device)),
 19                         torch.as_tensor(bound, dtype=z.dtype, device=z.device))
 20
 21def kkt_math_check(seed=2906):
 22    # Also check the 2-D disk+box finite candidate construction against SLSQP.
 23    from scipy.optimize import minimize
 24    rng = np.random.default_rng(seed); box=np.array([1.35,1.10]); errs=[]; viol=[]; interior=[]
 25    def g(r,R): return np.array([r[0]-box[0],-r[0]-box[0],r[1]-box[1],-r[1]-box[1],r@r-R*R])
 26    for _ in range(100):
 27        z=rng.uniform(-2,2,2); R=rng.uniform(.65,1.3)
 28        cand=[z.copy()]
 29        for i in range(2):
 30            for s in (-1,1):
 31                q=z.copy(); q[i]=s*box[i]; cand.append(q)
 32        nz=np.linalg.norm(z)
 33        if nz>R: cand.append(z*R/nz)
 34        for s0 in (-1,1):
 35            for s1 in (-1,1): cand.append(np.array([s0*box[0],s1*box[1]]))
 36        for i in range(2):
 37            j=1-i; rem=R*R-box[i]**2
 38            if rem>=0:
 39                for s in (-1,1):
 40                    q=np.zeros(2); q[i]=box[i]; q[j]=s*np.sqrt(rem); cand.append(q)
 41                    q=np.zeros(2); q[i]=-box[i]; q[j]=s*np.sqrt(rem); cand.append(q)
 42        feas=[(0.5*np.sum((q-z)**2),q) for q in cand if np.max(g(q,R))<=1e-9]
 43        if not feas: continue
 44        got=min(feas,key=lambda a:a[0])[1]
 45        ref=minimize(lambda q:.5*np.sum((q-z)**2), np.clip(z,-box,box),
 46          constraints=[{'type':'ineq','fun':lambda q,R=R:R*R-q@q},
 47                       {'type':'ineq','fun':lambda q:box-q},{'type':'ineq','fun':lambda q:box+q}],
 48          method='SLSQP', options={'ftol':1e-12,'maxiter':300})
 49        if ref.success: errs.append(np.linalg.norm(got-ref.x))
 50        viol.append(max(0.,np.max(g(got,R))))
 51        if np.max(g(z,R))<=0: interior.append(np.linalg.norm(got-z))
 52    return {'max_error_vs_slsqp':float(max(errs) if errs else np.nan),
 53            'max_constraint_violation':float(max(viol) if viol else np.nan),
 54            'interior_identity_max_error':float(max(interior) if interior else np.nan),
 55            'n_reference':len(errs)}
 56
 57def seed_all(s):
 58    np.random.seed(s); torch.manual_seed(s)
 59    if torch.cuda.is_available(): torch.cuda.manual_seed_all(s)
 60
 61def train_metric(track, s, lr, shielded):
 62    seed_all(s)
 63    d=get_dataset(track, seed=s, n_train=400, n_test=400)
 64    base=make_model('rnn_small', d['input_shape'], d['out_dim'])
 65    if shielded:
 66        class Shielded(torch.nn.Module):
 67            def __init__(self, net): super().__init__(); self.net=net
 68            def forward(self,x): return interval_shield(self.net(x))
 69        net=Shielded(base)
 70    else: net=base
 71    _, metric, _ = train_model(net,d,epochs=EPOCHS,lr=lr,batch=BATCH,log=lambda *_:None)
 72    return float(metric)
 73
 74def cfg_fn(shielded):
 75    def make(cfg):
 76        return lambda s: train_metric('dynamics', int(s), cfg['lr'], shielded)
 77    return make
 78
 79def observed_signature(s, lr):
 80    seed_all(s); d=get_dataset('dynamics',seed=s,n_train=400,n_test=400)
 81    raw=make_model('rnn_small',d['input_shape'],d['out_dim'])
 82    trained,_,_=train_model(raw,d,epochs=EPOCHS,lr=lr,batch=BATCH,log=lambda *_:None)
 83    raw = trained if trained is not None else raw
 84    raw.eval()
 85    dev=next(raw.parameters()).device
 86    with torch.no_grad(): z=raw(d['xte'].to(dev)); r=interval_shield(z); z=z.detach().cpu().numpy().ravel(); r=r.detach().cpu().numpy().ravel()
 87    interior=np.abs(z)<=B
 88    return {'bound':B,'n_test':len(z),'interior_fraction':float(interior.mean()),
 89            'interior_identity_max_error':float(np.max(np.abs(r[interior]-z[interior])) if interior.any() else 0.),
 90            'max_constraint_violation':float(np.max(np.maximum(np.abs(r)-B,0))),
 91            'mean_shaping_distance':float(np.mean(np.abs(r-z))),
 92            'predicted_interior_error':0.0,'predicted_max_violation':0.0,
 93            'confirmed': bool((np.max(np.abs(r[interior]-z[interior])) if interior.any() else 0.)<1e-7 and np.max(np.maximum(np.abs(r)-B,0))<1e-7)}
 94
 95def main():
 96    t=time.time(); math=kkt_math_check()
 97    # Official sweep (first four seeds), plus parity full evaluations for every lr.
 98    sb=sweep_baseline(cfg_fn(False),GRID,seeds=(0,1,2,3))
 99    baseline_all=[]
100    for c in GRID: baseline_all.append({'cfg':c,'full':evaluate(cfg_fn(False)(c),SEEDS)})
101    best=min(baseline_all,key=lambda q:q['full']['mean'])
102    base_block={'best_cfg':best['cfg'],'sweep':sb['sweep'],'full':best['full'],
103                'parity_full':baseline_all}
104    idea_all=[]
105    for c in GRID: idea_all.append({'cfg':c,'full':evaluate(cfg_fn(True)(c),SEEDS)})
106    ib=min(idea_all,key=lambda q:q['full']['mean'])
107    sig=observed_signature(0,best['cfg']['lr'])
108    sig['math_check']=math
109    sig['trained_baseline_mean']=best['full']['mean']
110    sig['trained_idea_best_mean']=ib['full']['mean']
111    rep=make_report('dynamics','rnn_small',base_block,ib['full'],sig)
112    rep['idea_sweep']=idea_all; rep['runtime_sec']=time.time()-t
113    Path('bench_report.json').write_text(json.dumps(rep,indent=2))
114    print(json.dumps(rep,indent=2))
115if __name__=='__main__': main()