Decision-Oriented Optimum Preservation / experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, math, random
  2import numpy as np
  3import torch
  4from torch import nn
  5
  6SEED=2711
  7random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
  8torch.set_num_threads(4)
  9device='cuda' if torch.cuda.is_available() else 'cpu'
 10try:
 11    if device=='cuda': torch.cuda.empty_cache()
 12except Exception:
 13    device='cpu'
 14
 15def true_x(u):
 16    return 0.2 + 0.8*u + 0.08*torch.sin(4*math.pi*u)
 17
 18def cost(x,u):
 19    return (x-0.72)**2 + 0.035*(u-0.15)**2
 20
 21def grid_opt(xfun, n=20001):
 22    u=torch.linspace(0,1,n,device=device)
 23    with torch.no_grad():
 24        j=cost(xfun(u),u); k=int(torch.argmin(j))
 25    return float(u[k]), float(j[k])
 26
 27def stability_sweep():
 28    H=3.7; rows=[]
 29    for rho in np.linspace(.05,.7,14):
 30        e=1.0
 31        for _ in range(80): e=(1-rho*H)*e
 32        rows.append({'rho':float(rho),'rhoH':float(rho*H),'error80':float(abs(e)),
 33                     'stable_observed':bool(abs(e)<1e-4),
 34                     'stable_predicted':bool(abs(1-rho*H)<1)})
 35    return {'H':H,'predicted_rho_boundary_2_over_H':2/H,'rows':rows}
 36
 37def minima_count(eps,k=6*math.pi,n=20001):
 38    u=np.linspace(0,1,n); us=.63
 39    j=(u-us)**2+eps*np.cos(k*u)
 40    idx=np.where((j[1:-1]<j[:-2])&(j[1:-1]<j[2:]))[0]+1
 41    return int(len(idx)),u[idx].tolist(),float(u[np.argmin(j)])
 42
 43def minima_sweep():
 44    k=6*math.pi; predicted=2/(k*k)
 45    epses=np.array([0,.001,.003,.005,.01,.02,.04,.08])
 46    return {'k':k,'predicted_curvature_threshold':predicted,
 47            'rows':[{'eps':float(e),'count':minima_count(e,k)[0],
 48                     'global_u':minima_count(e,k)[2]} for e in epses]}
 49
 50class Surrogate(nn.Module):
 51    def __init__(self):
 52        super().__init__(); self.net=nn.Sequential(nn.Linear(1,32),nn.Tanh(),nn.Linear(32,32),nn.Tanh(),nn.Linear(32,1))
 53    def forward(self,u): return self.net(u)
 54
 55def decision_unroll(model, starts, steps=35, lr=.08):
 56    u=starts.clone().requires_grad_(True)
 57    for _ in range(steps):
 58        j=cost(model(u[:,None]).squeeze(1),u)
 59        g=torch.autograd.grad(j.sum(),u,create_graph=True)[0]
 60        u=torch.clamp(u-lr*g,0,1)
 61    j=cost(model(u[:,None]).squeeze(1),u)
 62    return u,j
 63
 64def train(decision=False, seed=2711, epochs=1800):
 65    torch.manual_seed(seed)
 66    model=Surrogate().to(device)
 67    # Sparse observations intentionally leave decision-relevant interpolation freedom.
 68    ud=torch.tensor([[0.0],[.22],[.48],[.76],[1.0]],dtype=torch.float32,device=device)
 69    yd=true_x(ud)
 70    ut,jt=grid_opt(true_x)
 71    ustar=torch.tensor(ut,device=device); jstar=torch.tensor(jt,device=device)
 72    opt=torch.optim.Adam(model.parameters(),lr=.008)
 73    starts=torch.tensor([.05,.25,.5,.75,.95],device=device)
 74    for ep in range(epochs):
 75        pred=model(ud).squeeze(1); loss=((pred-yd)**2).mean()
 76        if decision:
 77            us,js=decision_unroll(model,starts,steps=18,lr=.10)
 78            # soft selection: lowest-start solution is used as differentiable proxy
 79            q=torch.argmin(js.detach()); ld=(us[q]-ustar)**2 + 2.0*(js[q]-jstar)**2
 80            loss=loss+3.0*ld
 81        opt.zero_grad(); loss.backward(); opt.step()
 82    return model,ut,jt
 83
 84def assess(model, ut, jt):
 85    us=np.linspace(0,1,101); u=torch.tensor(us,dtype=torch.float32,device=device)
 86    with torch.no_grad(): j=cost(model(u[:,None]).squeeze(1),u).cpu().numpy()
 87    ids=np.where((j[1:-1]<j[:-2])&(j[1:-1]<j[2:]))[0]+1
 88    # multistart projected gradient, no graph needed
 89    sols=[]
 90    for s in np.linspace(.01,.99,100):
 91        z=torch.tensor([s],dtype=torch.float32,device=device,requires_grad=True)
 92        for _ in range(120):
 93            jj=cost(model(z),z); g=torch.autograd.grad(jj,z)[0]
 94            with torch.no_grad(): z.clamp_(0,1); z.sub_(.08*g).clamp_(0,1)
 95            z.requires_grad_(True)
 96        sols.append(float(z.detach()))
 97    distinct=[]
 98    for z in sorted(sols):
 99        if not distinct or abs(z-distinct[-1])>.025: distinct.append(z)
100    k=int(np.argmin(j));
101    return {'grid_opt_u':float(us[k]),'grid_opt_j':float(j[k]),'displacement':abs(float(us[k])-ut),
102            'grid_local_minima':int(len(ids)),'multistart_distinct':len(distinct),
103            'multistart_solutions':distinct,'data_mse':float(((model(torch.tensor([[0.],[.22],[.48],[.76],[1.]],device=device))-true_x(torch.tensor([[0.],[.22],[.48],[.76],[1.]],device=device)))**2).mean().detach().cpu())}
104
105def main():
106    ut,jt=grid_opt(true_x)
107    base,_a,_b=train(False); idea,_a,_b=train(True)
108    out={'device':device,'true_optimum':{'u':ut,'j':jt},'math_stability':stability_sweep(),'math_extra_minima':minima_sweep(),
109         'baseline':assess(base,ut,jt),'decision_aware':assess(idea,ut,jt)}
110    with open('results.json','w') as f: json.dump(out,f,indent=2)
111    print(json.dumps(out,indent=2))
112if __name__=='__main__': main()