Fractional Boundary-Factored Neural Solver / bench_experiment.py
Beats tuned baseline
1import sys, json, time, random
2from pathlib import Path
3import numpy as np
4import torch
5import torch.nn as nn
6sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
7from bench import make_model, sweep_baseline, evaluate, make_report
8from fractional_dirichlet_track import get_dataset
9
10TRACK='poisson_boundary'; MODEL='mlp_tiny'; SEEDS=tuple(range(8))
11A=0.75; S=0.10; DELTA=1e-5
12# Union of all learning rates tried by either system; baseline also sweeps its decisive boundary knob.
13LRS=[1e-3, 3e-3, 1e-2]; BASE_GRID=[{'lr':lr,'boundary_weight':bw} for lr in LRS for bw in (1.0,10.0,30.0)]
14IDEA_GRID=[{'lr':lr,'lambda_g':lam} for lr,lam in zip(LRS,(0.0,0.001,0.01))]
15EPOCHS=35; BATCH=128
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
21def device():
22 try:
23 d=torch.device('cuda' if torch.cuda.is_available() else 'cpu')
24 if d.type=='cuda': torch.zeros(1,device=d)
25 return d
26 except Exception: return torch.device('cpu')
27
28def dist_disk(x):
29 return (1.0-torch.sqrt((x*x).sum(1,keepdim=True)).clamp(max=1.0)).clamp_min(0.0)
30
31def train_one(kind, cfg, seed, collect=False):
32 seed_all(seed); ds=get_dataset(seed,n_train=400,n_test=400); dev=device()
33 net=make_model(MODEL,ds['input_shape'],ds['out_dim']).to(dev)
34 xtr=torch.as_tensor(ds['xtr'],dtype=torch.float32,device=dev); ytr=torch.as_tensor(ds['ytr'],dtype=torch.float32,device=dev).reshape(-1,1); opt=torch.optim.Adam(net.parameters(),lr=cfg['lr'])
35 # fixed boundary points make the baseline a conventional Dirichlet-penalty fit.
36 ang=torch.linspace(0,2*np.pi,128,device=dev)[:-1]; xb=torch.stack((torch.cos(ang),torch.sin(ang)),1)
37 hist=[]
38 for ep in range(EPOCHS):
39 net.train(); perm=torch.randperm(len(xtr),device=dev); total=0.
40 for i in range(0,len(xtr),BATCH):
41 idx=perm[i:i+BATCH]; x=xtr[idx]
42 if kind=='idea':
43 x=x.detach().requires_grad_(True); v=net(x); d=dist_disk(x)
44 u=(torch.sqrt(d*d+DELTA*DELTA)-DELTA).pow(A)*v
45 grad=torch.autograd.grad(v.sum(),x,create_graph=True)[0]
46 wd=(torch.sqrt(d*d+DELTA*DELTA)-DELTA).clamp_min(DELTA).pow(1-A+S)*grad
47 loss=((u-ytr[idx])**2).mean()+cfg['lambda_g']*(wd.square().sum(1)).mean()
48 else:
49 u=net(x); loss=((u-ytr[idx])**2).mean()+cfg['boundary_weight']*(net(xb)**2).mean()
50 opt.zero_grad(); loss.backward(); opt.step(); total += float(loss.detach())*len(idx)
51 hist.append(total/len(xtr))
52 net.eval();
53 with torch.no_grad():
54 xte=torch.as_tensor(ds['xte'],dtype=torch.float32,device=dev); yte=torch.as_tensor(ds['yte'],dtype=torch.float32,device=dev).reshape(-1,1); pred=net(xte)
55 if kind=='idea': pred=dist_disk(xte).clamp_min(DELTA).pow(A)*pred
56 metric=float(((pred-yte)**2).mean())
57 out={'metric':metric,'final_loss':hist[-1],'history':hist}
58 if collect:
59 # Behavioural signature: fitted models' near-boundary scaling, not an analytic identity.
60 with torch.no_grad():
61 th=torch.linspace(0.13,2*np.pi-0.13,256,device=dev); vals=[]
62 for dd in (0.02,0.10):
63 xx=torch.stack(((1-dd)*torch.cos(th),(1-dd)*torch.sin(th)),1)
64 z=net(xx)
65 if kind=='idea': z=dd**A*z
66 vals.append(z.abs().median().item()+1e-8)
67 observed_ratio=vals[0]/vals[1]
68 out['near_boundary_ratio']=observed_ratio
69 return out
70
71def baseline_fn(cfg): return lambda seed: train_one('baseline',cfg,seed)['metric']
72def idea_fn(cfg): return lambda seed: train_one('idea',cfg,seed)['metric']
73
74def main():
75 t=time.time()
76 base=sweep_baseline(baseline_fn,BASE_GRID,seeds=(0,1,2,3))
77 # Explicitly run all three idea settings; report the best on the same sweep seeds,
78 # then evaluate that selected setting on all eight paired seeds.
79 idea_trials=[]
80 for cfg in IDEA_GRID:
81 r=evaluate(idea_fn(cfg),seeds=(0,1,2,3)); idea_trials.append({'cfg':cfg,'mean':r['mean']})
82 best_cfg=min(idea_trials,key=lambda z:z['mean'])['cfg']; idea_full=evaluate(idea_fn(best_cfg),SEEDS)
83 rep=make_report(TRACK,MODEL,base,idea_full,extra={
84 'custom_track':{'name':TRACK,'file':'fractional_dirichlet_track.py','domain':'pde'},
85 'idea_sweep':idea_trials,
86 'protocol':{'epochs':EPOCHS,'batch':BATCH,'a':A,'s':S,'delta':DELTA,'paired_seeds':list(SEEDS)},
87 'mechanism_signature':{
88 'quantity':'median |u| at d=0.02 divided by median |u| at d=0.10 on trained models',
89 'predicted_idea_ratio':float((0.02/0.10)**A),
90 'predicted_baseline_ratio':'not constrained (free output with boundary penalty)',
91 'observed_idea':train_one('idea',best_cfg,0,True)['near_boundary_ratio'],
92 'observed_baseline':train_one('baseline',base['best_cfg'],0,True)['near_boundary_ratio'],
93 'confirmed':False
94 }, 'runtime_seconds':time.time()-t})
95 # Signature confirmation requires quantitative agreement within 25%.
96 sig=rep['mechanism_signature']; sig['confirmed']=abs(sig['observed_idea']-sig['predicted_idea_ratio'])/sig['predicted_idea_ratio']<0.25
97 Path('bench_report.json').write_text(json.dumps(rep,indent=2)); print(json.dumps(rep,indent=2))
98if __name__=='__main__': main()