PDE Sinkhorn with asymmetric geometric boundaries / run_bench.py
Mechanism confirmed, baseline not beaten
1import sys, json, random
2from pathlib import Path
3import numpy as np
4import torch
5import torch.nn.functional as F
6
7ROOT = Path(__file__).resolve().parent
8sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
9from bench import make_model, train_model, sweep_baseline, evaluate, make_report
10from pde_boundary_track import get_dataset, C
11
12SEEDS = tuple(range(8))
13EPOCHS = 25
14BATCH = 128
15# The baseline and idea share this union of learning rates and Adam weight decay.
16CONFIGS = [
17 {'lr': 1e-3, 'weight_decay': 0.0},
18 {'lr': 3e-3, 'weight_decay': 0.0},
19 {'lr': 6e-3, 'weight_decay': 0.0},
20]
21
22
23def seed_all(seed):
24 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
25 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
26
27
28def ds_for(seed):
29 d = get_dataset(seed, 400, 400)
30 for k in ('xtr','ytr','xte','yte'):
31 d[k] = torch.as_tensor(d[k], dtype=torch.float32)
32 d['ytr'] = d['ytr'].reshape(-1,1); d['yte'] = d['yte'].reshape(-1,1)
33 d['input_shape'] = tuple(d['xtr'].shape[1:]); d['out_dim'] = 1
34 return d
35
36
37def baseline_fn(cfg):
38 def run(seed):
39 seed_all(seed); d=ds_for(seed)
40 net=make_model('mlp_tiny', d['input_shape'], 1)
41 _, metric, _ = train_model(net, d, epochs=EPOCHS, lr=cfg['lr'], batch=BATCH,
42 weight_decay=cfg['weight_decay'], log=lambda *_: None)
43 return metric
44 return run
45
46
47def boundary_residuals(net, x, device):
48 # Values are measured from the trained model, not an analytical identity.
49 z=x.to(device).detach().clone().requires_grad_(True)
50 u=net(z)
51 g=torch.autograd.grad(u.sum(), z, create_graph=False)[0]
52 xx, yy=z[:,0], z[:,1]
53 left=xx < 1e-7
54 horiz=(yy < 1e-7) | (yy > 1-1e-7)
55 ob=torch.abs(g[:,0] + C*g[:,1])[left]
56 no=torch.abs(g[:,1])[horiz]
57 return float(ob.mean().detach().cpu()) if ob.numel() else 0., float(no.mean().detach().cpu()) if no.numel() else 0.
58
59
60def idea_train(seed, cfg, return_net=False):
61 seed_all(seed); d=ds_for(seed)
62 device='cuda' if torch.cuda.is_available() else 'cpu'
63 try:
64 net=make_model('mlp_tiny', d['input_shape'], 1).to(device)
65 opt=torch.optim.Adam(net.parameters(), lr=cfg['lr'], weight_decay=cfg['weight_decay'])
66 xtr,ytr=d['xtr'].to(device),d['ytr'].to(device)
67 # Boundary penalty is the PDE intervention; all architecture/data/budget terms match.
68 for _ in range(EPOCHS):
69 net.train(); perm=torch.randperm(len(xtr),device=device)
70 for i in range(0,len(xtr),BATCH):
71 x=xtr[perm[i:i+BATCH]].detach().clone().requires_grad_(True)
72 y=ytr[perm[i:i+BATCH]]
73 pred=net(x); loss=F.mse_loss(pred,y)
74 grad=torch.autograd.grad(pred.sum(),x,create_graph=True)[0]
75 left=x[:,0] < 1e-7; horiz=(x[:,1] < 1e-7)|(x[:,1] > 1-1e-7)
76 bc=0.0
77 if left.any(): bc=bc+(grad[left,0]+C*grad[left,1]).square().mean()
78 if horiz.any(): bc=bc+grad[horiz,1].square().mean()
79 total=loss + cfg['bc_weight']*bc
80 opt.zero_grad(); total.backward(); opt.step()
81 net.eval()
82 with torch.no_grad(): metric=float(((net(d['xte'].to(device))-d['yte'].to(device))**2).mean().cpu())
83 if return_net: return net, d, device, metric
84 return metric
85 except RuntimeError:
86 # Explicit CPU fallback for shared GPU failures.
87 torch.cuda.empty_cache() if torch.cuda.is_available() else None
88 old=torch.cuda.is_available
89 # Re-run with CPU by temporarily using an equivalent forced implementation.
90 if device != 'cpu':
91 torch.set_default_device('cpu')
92 try: return idea_train(seed, cfg, return_net)
93 finally: torch.set_default_device('cpu')
94 raise
95
96
97def idea_fn(cfg):
98 return lambda seed: idea_train(seed, cfg)
99
100
101def main():
102 # Baseline sweep uses the same lr union and the same eight-seed final evaluation.
103 base=sweep_baseline(baseline_fn, CONFIGS)
104 idea_configs=[dict(CONFIGS[CONFIGS.index(base['best_cfg'])], bc_weight=w) for w in (0.1, 1.0, 5.0)]
105 idea_results=[]
106 for cfg in idea_configs:
107 r=evaluate(idea_fn(cfg), SEEDS); r['cfg']=cfg; idea_results.append(r)
108 best=min(idea_results, key=lambda r:r['mean'])
109 # Re-test signature on all boundary points from the trained systems.
110 sig=[]
111 for s in SEEDS:
112 bcfg=base['best_cfg']; seed_all(s); d=ds_for(s)
113 bnet=make_model('mlp_tiny',d['input_shape'],1)
114 bnet,_,_=train_model(bnet,d,epochs=EPOCHS,lr=bcfg['lr'],batch=BATCH,weight_decay=bcfg['weight_decay'],log=lambda *_: None)
115 # exact boundary probe, with model derivatives measured after training
116 t=np.linspace(0.01,0.99,32,dtype=np.float32); bx=np.concatenate([np.c_[np.zeros(32),t],np.c_[t,np.zeros(32)],np.c_[t,np.ones(32)]])
117 bd=torch.as_tensor(bx, dtype=torch.float32)
118 bdev='cuda' if next(bnet.parameters()).is_cuda else 'cpu'
119 bo,bn=boundary_residuals(bnet,bd,bdev)
120 inet,_,idev,_=idea_train(s,best['cfg'],True)
121 io,inn=boundary_residuals(inet,bd,idev)
122 sig.append({'seed':s,'baseline_oblique_abs_derivative':bo,'idea_oblique_abs_derivative':io,'baseline_normal_abs_derivative':bn,'idea_normal_abs_derivative':inn})
123 mean_b=float(np.mean([q['baseline_oblique_abs_derivative'] for q in sig])); mean_i=float(np.mean([q['idea_oblique_abs_derivative'] for q in sig]))
124 signature={'prediction':'boundary-aware PDE training reduces measured oblique/no-flux residuals','mean_baseline_oblique_residual':mean_b,'mean_idea_oblique_residual':mean_i,'mean_baseline_normal_residual':float(np.mean([q['baseline_normal_abs_derivative'] for q in sig])),'mean_idea_normal_residual':float(np.mean([q['idea_normal_abs_derivative'] for q in sig])),'per_seed':sig,'confirmed': bool(mean_i < mean_b)}
125 extra={'mechanism_signature':signature,'custom_track':{'name':'anisotropic_oblique_bvp','file':'pde_boundary_track.py','domain':'pde'},'idea_sweep':[{'cfg':r['cfg'],'mean':r['mean'],'std':r['std'],'per_seed':r['per_seed']} for r in idea_results]}
126 report=make_report('anisotropic_oblique_bvp','mlp_tiny',base,best,extra)
127 report['idea']['selected_cfg']=best['cfg']; report['protocol_note']='Custom PDE track required: no built-in track contains boundary-value/PDE structure.'
128 Path('bench_report.json').write_text(json.dumps(report,indent=2))
129 print(json.dumps(report,indent=2))
130
131if __name__=='__main__': main()