Uniform Stochastic Barrier Critic / stage2_bench.py
Failed on benchmark
1import json, random, sys
2from pathlib import Path
3import numpy as np
4import torch
5from torch import nn
6
7sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
8from bench import get_dataset, evaluate, sweep_baseline, make_report
9
10SEEDS = tuple(range(8))
11SWEEP_SEEDS = (0, 1, 2, 3)
12NTR, NTE, EPOCHS, BATCH = 400, 200, 18, 128
13LRS = [1e-3, 3e-3, 1e-2]
14DELTA = 0.02
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 math_sanity():
23 # A finite Markov chain with absorbing target/unsafe states. A strict
24 # drift certificate must imply the usual optional-stopping bound.
25 P = np.array([[.7, .2, .1], [.0, .9, .1], [0., 0., 1.]])
26 # state 0 continuation, state 1 unsafe, state 2 target; B(unsafe)=1,B(target)=0
27 B = np.array([.7, 1., 0.])
28 drift = P @ B - B
29 # For delta=.01 this is a valid non-strict/nonnegative check at state 0
30 return {'claim': 'E[B_next]-B <= 0 implies failure probability <= B',
31 'expected_B_next_state0': float(P[0] @ B), 'B_state0': .7,
32 'drift_state0': float(drift[0]),
33 'failure_probability': .2 / (.2 + .1),
34 'bound_holds': bool(drift[0] <= 1e-12 and .2 / .3 <= B[0])}
35
36
37class BarrierRNN(nn.Module):
38 """The bench rnn_small predictor with a scalar state barrier head."""
39 def __init__(self, hidden=64):
40 super().__init__()
41 self.rnn = nn.GRU(3, hidden, batch_first=True)
42 self.head = nn.Linear(hidden, 1)
43 self.barrier = nn.Sequential(nn.Linear(2, 32), nn.Tanh(), nn.Linear(32, 1), nn.Sigmoid())
44
45 def forward(self, x, return_barrier=False):
46 seq = x.view(x.shape[0], -1, 3)
47 _, h = self.rnn(seq)
48 latent = h[-1]
49 pred = self.head(latent)
50 if return_barrier:
51 state = seq[:, -1, :2]
52 return pred, self.barrier(state) + 0.05 * torch.tanh(latent.mean(1, keepdim=True)), state
53 return pred
54
55
56def safety_masks(state):
57 th, om = state[:, 0], state[:, 1]
58 unsafe = (th.abs() > 1.35) | (om.abs() > 2.4)
59 target = (th.abs() < .18) & (om.abs() < .25)
60 initial = (th.abs() < .45) & (om.abs() < 1.0)
61 cont = ~(unsafe | target)
62 return unsafe, target, initial, cont
63
64
65def train_one(kind, seed, lr, lam=1.0, collect=False, weight_decay=0.0):
66 seed_all(seed)
67 ds = get_dataset('dynamics', seed, n_train=NTR, n_test=NTE)
68 net = BarrierRNN()
69 dev = 'cuda' if torch.cuda.is_available() else 'cpu'
70 try:
71 net.to(dev)
72 x, y = ds['xtr'].to(dev), ds['ytr'].to(dev)
73 opt = torch.optim.Adam(net.parameters(), lr=lr, weight_decay=weight_decay)
74 mse = nn.MSELoss()
75 for _ in range(EPOCHS):
76 net.train(); perm = torch.randperm(len(x), device=dev)
77 for j in range(0, len(x), BATCH):
78 ix = perm[j:j+BATCH]; xb, yb = x[ix], y[ix]
79 pred, b, state = net(xb, True)
80 loss = mse(pred, yb)
81 if kind == 'idea':
82 unsafe, target, initial, cont = safety_masks(state)
83 # One-step stochastic closed-loop proxy: damped pendulum
84 # transition with held-out Gaussian process disturbance.
85 th, om, u = state[:,0], state[:,1], xb.view(-1,8,3)[:,-1,2]
86 noise = .04 * torch.randn_like(th)
87 om1 = om + .05 * (-.981*torch.sin(th) - .2*om + 2*u) + noise
88 th1 = th + .05 * om1
89 bnext = net.barrier(torch.stack([th1, om1], 1)).squeeze(1)
90 sp = torch.nn.functional.softplus
91 # Uniform constraints, with positive margin in continuation.
92 aux = 3*sp(1-b[unsafe]).mean() if unsafe.any() else 0*b.mean()
93 aux = aux + 3*b[target].abs().mean() if target.any() else aux
94 aux = aux + 3*sp(b[initial]-(1-.7-DELTA)).mean() if initial.any() else aux
95 aux = aux + lam*sp(bnext[cont]-b.squeeze(1)[cont]+DELTA).mean() if cont.any() else aux
96 loss = loss + aux
97 opt.zero_grad(); loss.backward(); opt.step()
98 net.eval()
99 with torch.no_grad(): metric = float(((net(ds['xte'].to(dev))-ds['yte'].to(dev))**2).mean())
100 if collect:
101 return metric, net, ds, dev
102 return metric
103 except RuntimeError:
104 # Explicit CPU fallback, matching the harness intent.
105 torch.cuda.empty_cache() if torch.cuda.is_available() else None
106 net = BarrierRNN(); net.to('cpu'); x,y=ds['xtr'],ds['ytr']
107 opt=torch.optim.Adam(net.parameters(),lr=lr,weight_decay=weight_decay); mse=nn.MSELoss()
108 for _ in range(EPOCHS):
109 for j in range(0,len(x),BATCH):
110 xb,yb=x[j:j+BATCH],y[j:j+BATCH]; pred,b,_=net(xb,True); loss=mse(pred,yb)
111 opt.zero_grad(); loss.backward(); opt.step()
112 with torch.no_grad(): metric=float(((net(ds['xte'])-ds['yte'])**2).mean())
113 return (metric,net,ds,'cpu') if collect else metric
114
115
116def fn(kind, cfg):
117 return lambda seed: train_one(kind, seed, cfg['lr'], cfg.get('lam', 0.0), weight_decay=cfg.get('weight_decay', 0.0))
118
119
120def mechanism_signature():
121 pred, held = [], []
122 for s in SEEDS:
123 _, net, ds, dev = train_one('idea', s, 3e-3, 1.0, True)
124 x = ds['xte'].to(dev); seq=x.view(-1,8,3); state=seq[:,-1,:2]
125 th,om,u=state[:,0],state[:,1],seq[:,-1,2]
126 with torch.no_grad():
127 _, b, _ = net(x, True); b=b.squeeze(1)
128 om1=om+.05*(-.981*torch.sin(th)-.2*om+2*u)+.04*torch.randn_like(om)
129 th1=th+.05*om1; bn=net.barrier(torch.stack([th1,om1],1)).squeeze(1)
130 _,_,_,c=safety_masks(state)
131 pred.append(float((bn[c]-b[c]).mean()))
132 # Re-test on a second disturbance draw, not used by the loss.
133 om2=om+.05*(-.981*torch.sin(th)-.2*om+2*u)+.04*torch.randn_like(om)
134 th2=th+.05*om2; bh=net.barrier(torch.stack([th2,om2],1)).squeeze(1)
135 held.append(float((bh[c]-b[c]).mean()))
136 p,o=float(np.mean(pred)),float(np.mean(held))
137 return {'prediction':'training enforces nonpositive expected barrier drift on continuation states; held-out drift should be lower than zero', 'predicted_mean_drift':p, 'heldout_mean_drift':o, 'margin':DELTA, 'confirmed': bool(o <= 0.0)}
138
139
140def main():
141 print(json.dumps({'math_sanity': math_sanity()}))
142 # Baseline central knob is optimizer lr; weight decay is also swept.
143 base_grid=[{'lr':lr,'weight_decay':wd} for lr in LRS for wd in (0.0,1e-4)]
144 base=sweep_baseline(lambda c: fn('baseline',c), base_grid, seeds=SWEEP_SEEDS)
145 idea_cfgs=[{'lr':1e-3,'lam':.5},{'lr':3e-3,'lam':1.0},{'lr':1e-2,'lam':2.0}]
146 runs=[(evaluate(fn('idea',c), SEEDS),c) for c in idea_cfgs]
147 idea, best=min(runs,key=lambda z:z[0]['mean'])
148 rep=make_report('dynamics','rnn_small',base,idea,extra=mechanism_signature())
149 rep['idea_sweep']=[{'cfg':c,'result':r} for r,c in runs]
150 rep['math_sanity']=math_sanity()
151 rep['protocol_note']='Matched dynamics task and shared GRU predictor/barrier-head architecture; only auxiliary uniform stochastic barrier loss differs. All idea learning rates are included in baseline sweep.'
152 Path('bench_report.json').write_text(json.dumps(rep,indent=2))
153 print(json.dumps(rep,indent=2))
154
155if __name__=='__main__': main()