CEGAR-certified latent-state abstraction / cegar_abstraction.py
Mechanism confirmed, baseline not beaten
1"""Small, exact interval CEGAR abstraction for a 2-D neural-state-space toy.
2
3The affine map is an intentionally transparent surrogate for a saturated RNN cell:
4 h' = A h + b.
5For axis-aligned boxes, interval propagation is exact for each coordinate's
6independent extrema. Therefore every edge generated by intersecting the
7resulting box with partition cells is conservative (up to floating point).
8"""
9import json, math, random
10from dataclasses import dataclass
11from pathlib import Path
12import numpy as np
13
14A = np.array([[.85, .15], [-.15, .85]], dtype=float)
15b = np.array([.20, 0.0])
16DOMAIN = (-1.0, 1.0, -1.0, 1.0)
17UNSAFE_X = .72
18
19@dataclass(frozen=True)
20class Cell:
21 x0: float; x1: float; y0: float; y1: float
22 def bounds(self): return np.array([self.x0,self.x1,self.y0,self.y1])
23 def center(self): return np.array([(self.x0+self.x1)/2,(self.y0+self.y1)/2])
24 def width(self): return max(self.x1-self.x0,self.y1-self.y0)
25 def intersects(self, box, eps=1e-12):
26 return (self.x1 >= box[0]-eps and self.x0 <= box[1]+eps and
27 self.y1 >= box[2]-eps and self.y0 <= box[3]+eps)
28 def unsafe(self): return self.x1 >= UNSAFE_X
29
30def affine_box(cell, margin=0.0):
31 lo, hi = cell.bounds()[::2], cell.bounds()[1::2]
32 # extrema of each affine coordinate over a rectangle
33 outlo=[]; outhi=[]
34 for row, bias in zip(A,b):
35 mn = bias + sum(c*l if c >= 0 else c*h for c,l,h in zip(row,lo,hi))
36 mx = bias + sum(c*h if c >= 0 else c*l for c,l,h in zip(row,lo,hi))
37 outlo.append(mn-margin); outhi.append(mx+margin)
38 return np.array([outlo[0], outhi[0], outlo[1], outhi[1]])
39
40def uniform_partition(n):
41 xs=np.linspace(DOMAIN[0],DOMAIN[1],n+1); ys=np.linspace(DOMAIN[2],DOMAIN[3],n+1)
42 return [Cell(xs[i],xs[i+1],ys[j],ys[j+1]) for i in range(n) for j in range(n)]
43
44def split(c):
45 xm=(c.x0+c.x1)/2; ym=(c.y0+c.y1)/2
46 return [Cell(c.x0,xm,c.y0,ym),Cell(c.x0,xm,ym,c.y1),Cell(xm,c.x1,c.y0,ym),Cell(xm,c.x1,ym,c.y1)]
47
48def build_graph(cells, margin=0.0):
49 edges=[]
50 for c in cells:
51 box=affine_box(c,margin)
52 edges.append([j for j,d in enumerate(cells) if d.intersects(box)])
53 return edges
54
55def reachable(cells, edges, initial=(.01,.01), steps=12):
56 # Boundary ownership is immaterial because all intersecting cells are kept.
57 cur={next(i for i,c in enumerate(cells) if c.x0 <= initial[0] <= c.x1 and c.y0 <= initial[1] <= c.y1)}
58 levels=[set(cur)]; parents=[]
59 for _ in range(steps):
60 nxt=set(); par={}
61 for i in cur:
62 for j in edges[i]:
63 nxt.add(j); par.setdefault(j,i)
64 parents.append(par); levels.append(nxt); cur=nxt
65 unsafe=sorted({i for lev in levels for i in lev if cells[i].unsafe()})
66 return levels, parents, unsafe
67
68def path_to_unsafe(levels, parents, target):
69 # recover one shortest-ish abstract counterexample
70 t=next(k for k,lev in enumerate(levels) if target in lev)
71 path=[target]
72 for k in range(t-1,-1,-1): path.append(parents[k][path[-1]])
73 return list(reversed(path))
74
75def concrete_rollout(h=(.01,.01), steps=30):
76 z=np.array(h,float); out=[z.copy()]
77 for _ in range(steps): z=A@z+b; out.append(z.copy())
78 return np.array(out)
79
80def cegar(initial_n=4, rounds=8, margin=1e-10):
81 cells=uniform_partition(initial_n); refinements=[]
82 for _ in range(rounds):
83 edges=build_graph(cells,margin); levels,parents,unsafe=reachable(cells,edges)
84 if not unsafe: break
85 path=path_to_unsafe(levels,parents,unsafe[0])
86 # Refine the widest cell on the counterexample, prioritizing its prefix.
87 k=max(range(len(path)), key=lambda q: cells[path[q]].width())
88 chosen=path[k]; old=cells[chosen]; cells=cells[:chosen]+split(old)+cells[chosen+1:]
89 refinements.append(old.width())
90 edges=build_graph(cells,margin); levels,parents,unsafe=reachable(cells,edges)
91 return cells, levels, unsafe, refinements
92
93def sampled_soundness(n=4, margin=0.0, samples=20000, seed=7):
94 rng=np.random.default_rng(seed); cells=uniform_partition(n); edges=build_graph(cells,margin)
95 failures=0; checked=0
96 for i,c in enumerate(cells):
97 pts=rng.uniform([c.x0,c.y0],[c.x1,c.y1],size=(max(1,samples//len(cells)),2))
98 nxt=pts@A.T+b
99 for z in nxt:
100 # The abstraction is over DOMAIN; successors outside it are
101 # intentionally excluded from this conditional soundness check.
102 if not (DOMAIN[0]-1e-12 <= z[0] <= DOMAIN[1]+1e-12 and DOMAIN[2]-1e-12 <= z[1] <= DOMAIN[3]+1e-12):
103 continue
104 js=[j for j,d in enumerate(cells) if d.x0-1e-12<=z[0]<=d.x1+1e-12 and d.y0-1e-12<=z[1]<=d.y1+1e-12]
105 checked+=1
106 if not any(j in edges[i] for j in js): failures+=1
107 return checked, failures
108
109def run():
110 np.set_printoptions(precision=4,suppress=True)
111 exact=concrete_rollout(); concrete_unsafe=bool(np.max(exact[:,0])>=UNSAFE_X)
112 sweep=[]
113 for n in [2,4,8,16]:
114 cells=uniform_partition(n); ed=build_graph(cells); lev,_,u=reachable(cells,ed)
115 area=sum((cells[i].x1-cells[i].x0)*(cells[i].y1-cells[i].y0) for i in u)
116 sweep.append({'n':n,'cells':len(cells),'abstract_unsafe_cells':len(u),
117 'abstract_unsafe_area':area,
118 'abstract_unsafe_levels':sum(any(cells[i].unsafe() for i in q) for q in lev),
119 'concrete_unsafe':concrete_unsafe})
120 ce_cells,ce_lev,ce_u,ref=cegar()
121 ce_area=sum((c.x1-c.x0)*(c.y1-c.y0) for i,c in enumerate(ce_cells) if i in ce_u)
122 margins=[]
123 for m in [0,.001,.01,.05]:
124 checked,fail=sampled_soundness(8,m)
125 edge_count=sum(len(x) for x in build_graph(uniform_partition(8),m))
126 margins.append({'margin':m,'checked':checked,'failures':fail,'coverage':1-fail/checked,'edges':edge_count})
127 result={'map_A':A.tolist(),'map_b':b.tolist(),'unsafe_x':UNSAFE_X,
128 'concrete_max_x':float(np.max(exact[:,0])),'predictions':{
129 'P1_soundness': 'conditional in-domain sampled omissions are zero for every nonnegative margin',
130 'P2_uniform_resolution': 'area of reachable unsafe overapproximation decreases as cell width shrinks',
131 'P3_margin': 'adding interval margin cannot reduce edge count',
132 'P4_CEGAR_focus': 'CEGAR reaches comparable unsafe area with fewer cells than uniform n=16'},
133 'uniform_sweep':sweep,
134 'cegar':{'final_cells':len(ce_cells),'unsafe_cells':len(ce_u),'unsafe_area':ce_area,
135 'unsafe_levels':sum(any(ce_cells[i].unsafe() for i in q) for q in ce_lev),
136 'refined_widths':ref},'margin_sweep':margins}
137 Path('results.json').write_text(json.dumps(result,indent=2))
138 print(json.dumps(result,indent=2))
139
140if __name__=='__main__': run()