Zonotope Active Exposure for Sensor-Fusion Networks / experiment.py
Mechanism failed
1import json, random
2import numpy as np
3import torch
4
5SEED = 7
6np.random.seed(SEED); random.seed(SEED); torch.manual_seed(SEED)
7
8def support(q, H):
9 return float(np.sum(np.abs(q @ H)))
10
11def margin(q, c0, H0, cj, Hj):
12 return float(q @ (c0 - cj) - support(q, H0) - support(q, Hj))
13
14def math_check():
15 # Exact numerical checks of Minkowski addition and linear-map identities.
16 c1=np.array([.3,-.2]); H1=np.array([[.4,.1],[.0,.25]])
17 c2=np.array([-.1,.5]); H2=np.array([[.2],[.3]])
18 K=np.array([[1.2,-.4],[.5,.8]])
19 z1=np.random.uniform(-1,1,(20000,2)); z2=np.random.uniform(-1,1,(20000,1))
20 lhs=(c1+z1@H1.T)+(c2+z2@H2.T)
21 rhs=(c1+c2)+np.concatenate([z1,z2],1)@np.concatenate([H1,H2],1).T
22 mink_err=float(np.max(np.abs(lhs-rhs)))
23 lhs2=(c1+z1@H1.T)@K.T
24 rhs2=(K@c1)+z1@(K@H1).T
25 map_err=float(np.max(np.abs(lhs2-rhs2)))
26
27 # Choose q toward the trusted center, so the certificate is positive.
28 c0=np.array([0.,0.]); H0=np.diag([.1,.08])
29 cj=np.array([.55,0.]); Hj=np.diag([.08,.06]); q=np.array([-1.,0.])
30 m=margin(q,c0,H0,cj,Hj)
31 trusted_right=q@c0-support(q,H0)
32 attack_left=q@cj+support(q,Hj)
33 return dict(minkowski_max_error=mink_err, linear_map_max_error=map_err,
34 positive_certificate_margin=m,
35 sampled_projection_gap=float(trusted_right-attack_left))
36
37class Fusion(torch.nn.Module):
38 def __init__(self):
39 super().__init__()
40 self.net=torch.nn.Sequential(torch.nn.Linear(2,12),torch.nn.Tanh(),torch.nn.Linear(12,1))
41 def forward(self,x): return self.net(x)
42
43def jacobian(model, x):
44 x=x.detach().requires_grad_(True); y=model(x); rows=[]
45 for k in range(y.shape[1]):
46 rows.append(torch.autograd.grad(y[:,k].sum(),x,retain_graph=True)[0])
47 return torch.stack(rows,1) # B,out,in
48
49def probe_test():
50 # Two-sensor nonlinear fusion regression; sensor 0 has a fixed attack shift.
51 torch.manual_seed(SEED)
52 n=256; x=torch.rand(n,2)*2-1
53 y=x[:,0:1]+.6*x[:,1:2]+.2*torch.sin(3*x[:,0:1])
54 model=Fusion(); opt=torch.optim.Adam(model.parameters(),lr=.03)
55 for _ in range(250):
56 opt.zero_grad(); loss=((model(x)-y)**2).mean(); loss.backward(); opt.step()
57
58 x0=x[:1].clone(); J=jacobian(model,x0)[0,0].detach().numpy()[None,:]
59 He=np.diag([.08,.08]); attack=np.array([[.45],[0.]])
60 cbase=float(model(x0)); c0=np.array([cbase]); cj=np.array([cbase+float((J@attack).item())])
61 H0=J@He; Hj=J@He; eps=.25
62 ds=np.linspace(-eps,eps,101)
63 qs=np.array([[-1.],[1.]])
64 local_margins=[]
65 exact_sep=[]
66 for d in ds:
67 # Under the supplied local model, both hypotheses receive the same J*d.
68 local_margins.append(max(margin(q,c0+J[:,0]*d,H0,cj+J[:,0]*d,Hj) for q in qs))
69 with torch.no_grad():
70 a=model(x0+torch.tensor([[d,0.]],dtype=x0.dtype)).item()
71 b=model(x0+torch.tensor([[d+.45,0.]],dtype=x0.dtype)).item()
72 exact_sep.append(abs(b-a))
73 local_margins=np.asarray(local_margins); exact_sep=np.asarray(exact_sep)
74 zero_i=len(ds)//2; best_i=int(np.argmax(exact_sep))
75 # Monte Carlo confirms the support bound for the trusted linearized zonotope.
76 z=np.random.uniform(-1,1,(10000,2)); proj=(c0+(z@(H0.T))).ravel()
77 bound=float(abs(c0[0])+support(np.array([1.]),H0))
78 return dict(train_mse=float(((model(x)-y)**2).mean()), jacobian=J.tolist(),
79 local_margin_at_zero=float(local_margins[zero_i]),
80 local_margin_best=float(local_margins.max()),
81 local_margin_range=float(local_margins.max()-local_margins.min()),
82 exact_separation_at_zero=float(exact_sep[zero_i]),
83 exact_separation_best=float(exact_sep.max()),
84 exact_separation_gain=float(exact_sep[best_i]-exact_sep[zero_i]),
85 best_probe=float(ds[best_i]),
86 trusted_projection_max_abs=float(np.max(np.abs(proj))),
87 trusted_support_bound=bound)
88
89def main():
90 out={'seed':SEED,'math_check':math_check(),'probe_test':probe_test()}
91 with open('results.json','w') as f: json.dump(out,f,indent=2)
92 print(json.dumps(out,indent=2))
93
94if __name__=='__main__': main()