ISS-CLF/RCBF Neural Policy Shield / bench_shield.py

Failed on benchmark

Raw ⬇ ZIP
  1import sys, json, random
  2from pathlib import Path
  3import numpy as np
  4import torch
  5import torch.nn.functional as F
  6
  7sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  8from bench import get_dataset, make_model, train_model, evaluate, sweep_baseline, make_report
  9
 10SEEDS = tuple(range(8))
 11NTR, NTE, EPOCHS, BATCH = 400, 200, 12, 128
 12LRS = (1e-3, 3e-3, 1e-2)
 13WBAR_GRID = (0.05, 0.15, 0.30)
 14C_V, C_H, LIMIT, DT = 0.4, 1.0, 1.5, 0.05
 15
 16
 17def seed_all(seed):
 18    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
 19    if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
 20
 21
 22def shield(z, theta, omega, wbar):
 23    """Scalar robust CLF/CBF projection with differentiable soft fallback.
 24
 25    z is the RNN proposed next angle. The nominal rate is (z-theta)/DT.
 26    CLF V=.5 theta^2+.05 omega^2 and CBF h=LIMIT^2-theta^2.
 27    The closest feasible point is the interval projection; when empty, the
 28    action remains bounded and hinge slacks quantify infeasibility.
 29    """
 30    V = 0.5 * theta.square() + 0.05 * omega.square()
 31    # CLF: theta*(z-theta)/DT + wbar|theta| + cV V <= 0.
 32    eps = torch.as_tensor(1e-5, device=z.device, dtype=z.dtype)
 33    denom = torch.where(theta.abs() > eps, theta, torch.ones_like(theta))
 34    clf_bound = theta - DT * (wbar * theta.abs() + C_V * V) / denom
 35    # CBF: -2 theta*(z-theta)/DT - 2wbar|theta| + cH h >= 0.
 36    h = LIMIT * LIMIT - theta.square()
 37    cbf_bound = theta + DT * (C_H * h - 2.0 * wbar * theta.abs()) / (2.0 * denom)
 38    lo = torch.minimum(clf_bound, cbf_bound)
 39    hi = torch.maximum(clf_bound, cbf_bound)
 40    # The interval orientation depends on theta; use conservative intersection
 41    # of the two one-sided constraints via midpoint and smooth hinge penalty.
 42    lo = torch.clamp(lo, -LIMIT, LIMIT)
 43    hi = torch.clamp(hi, -LIMIT, LIMIT)
 44    proj = torch.minimum(torch.maximum(z, lo), hi)
 45    # If the nominal inequalities disagree, retaining proj is a bounded QP
 46    # relaxation; report exact robust residuals and nonnegative slacks.
 47    clf_res = theta * (proj-theta) / DT + wbar*theta.abs() + C_V*V
 48    cbf_res = -2*theta*(proj-theta) / DT - 2*wbar*theta.abs() + C_H*h
 49    sv = torch.relu(clf_res)
 50    sh = torch.relu(-cbf_res)
 51    return proj, sv, sh, clf_res, cbf_res
 52
 53
 54def train_idea(model, ds, epochs, lr, wbar):
 55    # Custom loop is necessary because the intervention modifies the training
 56    # forward path; all other settings match bench.train_model.
 57    device = 'cuda' if torch.cuda.is_available() else 'cpu'
 58    try:
 59        model = model.to(device)
 60        opt = torch.optim.Adam(model.parameters(), lr=lr)
 61        x, y = ds['xtr'].to(device), ds['ytr'].to(device)
 62        for _ in range(epochs):
 63            model.train(); perm = torch.randperm(len(x), device=device)
 64            for i in range(0, len(x), BATCH):
 65                ix = perm[i:i+BATCH]; raw = model(x[ix]).squeeze(-1)
 66                seq = x[ix].view(-1, 8, 3); th, om = seq[:, -1, 0], seq[:, -1, 1]
 67                pred, sv, sh, _, _ = shield(raw, th, om, wbar)
 68                loss = F.mse_loss(pred, y[ix].squeeze(-1)) + 0.01*(sv.square().mean()+sh.square().mean())
 69                opt.zero_grad(); loss.backward(); opt.step()
 70        model.eval()
 71        with torch.no_grad():
 72            xx=ds['xte'].to(device); raw=model(xx).squeeze(-1)
 73            seq=xx.view(-1,8,3); pred,*_=shield(raw,seq[:,-1,0],seq[:,-1,1],wbar)
 74            metric=float(F.mse_loss(pred,ds['yte'].to(device).squeeze(-1)))
 75        return metric
 76    except RuntimeError:
 77        return float('nan')
 78
 79
 80def baseline_fn(cfg):
 81    def run(seed):
 82        seed_all(seed); ds=get_dataset('dynamics',seed,NTR,NTE)
 83        net=make_model('rnn_small',ds['input_shape'],ds['out_dim'])
 84        _, metric, _=train_model(net,ds,epochs=EPOCHS,lr=cfg['lr'],batch=BATCH,log=lambda *_:None)
 85        return metric
 86    return run
 87
 88
 89def idea_fn(cfg):
 90    def run(seed):
 91        seed_all(seed); ds=get_dataset('dynamics',seed,NTR,NTE)
 92        return train_idea(make_model('rnn_small',ds['input_shape'],ds['out_dim']),ds,EPOCHS,cfg['lr'],cfg['wbar'])
 93    return run
 94
 95
 96def signature():
 97    # NN-scale re-test: train one actual model and measure predicted robust
 98    # margin slope versus observed residual as wbar changes.
 99    seed_all(0); ds=get_dataset('dynamics',0,NTR,NTE)
100    net=make_model('rnn_small',ds['input_shape'],1)
101    train_model(net,ds,epochs=EPOCHS,lr=3e-3,batch=BATCH,log=lambda *_:None)
102    dev='cuda' if torch.cuda.is_available() else 'cpu'; net.eval().to(dev); x=ds['xte'].to(dev)
103    with torch.no_grad():
104        z=net(x).squeeze(-1); q=x.view(-1,8,3); th,om=q[:,-1,0],q[:,-1,1]
105        vals=[]
106        for wb in [0.,.1,.2,.3]:
107            _,_,_,res,_=shield(z,th,om,wb); vals.append(float(res.mean()))
108    slope=float(np.polyfit([0.,.1,.2,.3],vals,1)[0])
109    predicted=float(torch.abs(th).mean())
110    return {'quantity':'mean robust CLF residual versus disturbance','predicted_slope_abs_theta':predicted,'observed_slope':slope,'relative_error':abs(slope-predicted)/(abs(predicted)+1e-8),'confirmed':bool(abs(slope-predicted)/(abs(predicted)+1e-8)<0.20)}
111
112
113def main():
114    baseline_grid=[{'lr':lr,'wbar':wb} for lr in LRS for wb in WBAR_GRID]
115    # Baseline receives union of every idea lr and the method's central knob;
116    # wbar is inert for standard training but retained for explicit parity.
117    base=sweep_baseline(baseline_fn,baseline_grid,seeds=(0,1,2,3))
118    idea_cfgs=[{'lr':base['best_cfg']['lr'],'wbar':base['best_cfg']['wbar']}, {'lr':1e-3,'wbar':.15}, {'lr':1e-2,'wbar':.30}]
119    idea_runs=[]
120    for cfg in idea_cfgs:
121        r=evaluate(idea_fn(cfg),seeds=SEEDS); idea_runs.append({'cfg':cfg,'result':r})
122    best=min(idea_runs,key=lambda q:q['result']['mean'])
123    rep=make_report('dynamics','rnn_small',base,best['result'],{'chosen_cfg':best['cfg'],'cV':C_V,'predicted_envelope':'Vdot <= -cV V + disturbance margin','signature':signature()})
124    rep['idea_sweep']=idea_runs
125    Path('bench_report.json').write_text(json.dumps(rep,indent=2))
126    print(json.dumps(rep,indent=2))
127
128if __name__=='__main__': main()