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

Failed on benchmark

Raw ⬇ ZIP
 1import json, random
 2from pathlib import Path
 3import numpy as np
 4
 5# Scalar robust control-affine plant: xdot = u + w, |w| <= wbar.
 6# V=x^2/2 and h=1-x^2. The QP uses nonnegative CLF/CBF slacks.
 7
 8def quantities(x, wbar, cV=0.4, ch=1.0):
 9    V = .5*x*x
10    # CLF: gradV*f + margin + cV*V <= sV
11    aV, bV = x, wbar*abs(x) + cV*V
12    # CBF: gradH*f - margin + alpha(h) >= -sH.
13    # Multiplying by -1 gives aH*u+bH <= sH.
14    aH, bH = 2*x, 2*wbar*abs(x) - ch*(1-x*x)
15    return aV, bV, aH, bH, V
16
17def shield(x, unet, wbar, umax=1.0, rho=100.0, cV=.4, ch=1.0):
18    a,b,c,d,V = quantities(x,wbar,cV,ch)
19    cuts=[-umax, umax]
20    for q,r in ((a,b),(c,d)):
21        if abs(q)>1e-12:
22            z=-r/q
23            if -umax <= z <= umax: cuts.append(z)
24    cuts=sorted(set(cuts)); candidates=list(cuts)
25    for lo,hi in zip(cuts[:-1],cuts[1:]):
26        mid=(lo+hi)/2
27        active1=(a*mid+b)>0; active2=(c*mid+d)>0
28        den=1.; num=unet
29        if active1: den += 2*rho*a*a; num -= 2*rho*a*b
30        if active2: den += 2*rho*c*c; num -= 2*rho*c*d
31        candidates.append(min(hi,max(lo,num/den)))
32    def obj(u): return .5*(u-unet)**2 + rho*max(0,a*u+b)**2 + rho*max(0,c*u+d)**2
33    u=min(candidates,key=obj)
34    return float(u), float(max(0,a*u+b)), float(max(0,c*u+d)), float(obj(u))
35
36def check_scaling():
37    x=.6; cV=.4; rows=[]
38    for wb in np.linspace(0,.8,9):
39        pred=wb*abs(x)+cV*.5*x*x
40        # Direct numerical robust CLF residual at u=0.
41        a,b,_,_,_=quantities(x,float(wb),cV)
42        observed=a*0+b
43        rows.append({'wbar':float(wb),'predicted_margin':float(pred),'observed_margin':float(observed)})
44    slope=float(np.polyfit([r['wbar'] for r in rows],[r['observed_margin'] for r in rows],1)[0])
45    return rows,slope
46
47def check_boundary():
48    # At x=-1, CLF requires u >= wbar+cV/2; CBF requires u >= wbar.
49    # Thus zero-slack feasibility ends at wbar=umax-cV/2=.8.
50    umax=1.; cV=.4; predicted=umax-cV/2; rows=[]
51    for wb in np.linspace(.4,1.2,17):
52        u,sv,sh,_=shield(-1.,0.,float(wb),umax=umax,rho=1e6,cV=cV)
53        rows.append({'wbar':float(wb),'u':u,'total_slack':float(sv+sh),
54                     'zero_slack':bool(sv+sh<1e-5)})
55    onset=next((r['wbar'] for r in rows if not r['zero_slack']),None)
56    return predicted,rows,onset
57
58def envelope():
59    # For feasible robust CLF, adversarial V derivative satisfies Vdot <= -cV V.
60    x=.8; wb=.2; dt=.002; T=2.; cV=.4; vs=[]; sl=[]; residual=[]
61    for _ in range(int(T/dt)):
62        u,s,h,_=shield(x,1.,wb,umax=1.,rho=1e6,cV=cV)
63        w=wb*(1 if x>=0 else -1)
64        residual.append(x*(u+w)+cV*.5*x*x)
65        x += dt*(u+w); vs.append(.5*x*x); sl.append(s)
66    t=np.arange(1,len(vs)+1)*dt; v0=.5*.8*.8
67    bound=v0*np.exp(-cV*t)
68    return {'max_V_over_zero_disturbance_bound':float(max(np.array(vs)/(bound+1e-12))),
69            'max_robust_clf_residual':float(max(residual)), 'max_clf_slack':float(max(sl)),
70            'final_V':float(vs[-1]), 'initial_V':v0, 'target_rate':cV}
71
72def baseline_episode(wb=.2,n=1000,dt=.002):
73    x=.8; violations=0
74    for _ in range(n):
75        x += dt*(1.+wb*(1 if x>=0 else -1)); violations += abs(x)>1
76    return {'violation_fraction':violations/n,'final_abs_x':abs(x)}
77
78def shield_episode(wb=.2,n=1000,dt=.002):
79    x=.8; violations=0; dev=[]; sl=[]
80    for _ in range(n):
81        u,s,h,_=shield(x,1.,wb,rho=1e6)
82        x += dt*(u+wb*(1 if x>=0 else -1)); violations += abs(x)>1
83        dev.append(abs(u-1.)); sl.append(s+h)
84    return {'violation_fraction':violations/n,'final_abs_x':abs(x),
85            'mean_action_deviation':float(np.mean(dev)),'mean_slack':float(np.mean(sl))}
86
87def main():
88    np.random.seed(7); random.seed(7)
89    scale,slope=check_scaling(); pred,rows,onset=check_boundary()
90    out={'scaling':{'rows':scale,'observed_slope':slope,'predicted_slope':.6},
91         'boundary':{'predicted_wbar_onset':pred,'observed_grid_onset':onset,'rows':rows},
92         'envelope':envelope(),'baseline_episode':baseline_episode(),'shield_episode':shield_episode()}
93    Path('results.json').write_text(json.dumps(out,indent=2)); print(json.dumps(out,indent=2))
94if __name__=='__main__': main()