Uniform Stochastic Barrier Critic / stage2_bench.py

Failed on benchmark

Raw ⬇ ZIP
  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()