Fractional Boundary-Factored Neural Solver / experiment.py
Beats tuned baseline
1import json, math, random, time
2from pathlib import Path
3import numpy as np
4import torch
5import torch.nn as nn
6
7SEED=17
8random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
9try:
10 device=torch.device('cuda' if torch.cuda.is_available() else 'cpu')
11 if device.type=='cuda':
12 torch.cuda.manual_seed_all(SEED)
13 # force a tiny allocation to detect unusable CUDA contexts
14 torch.zeros(1, device=device)
15except Exception:
16 device=torch.device('cpu')
17
18a=0.75
19s=0.10
20lam_g=0.01
21EPS=1e-6
22
23class MLP(nn.Module):
24 def __init__(self, width=48, depth=3):
25 super().__init__()
26 layers=[nn.Linear(1,width), nn.Tanh()]
27 for _ in range(depth-1): layers += [nn.Linear(width,width), nn.Tanh()]
28 layers += [nn.Linear(width,1)]
29 self.net=nn.Sequential(*layers)
30 for m in self.modules():
31 if isinstance(m,nn.Linear):
32 nn.init.xavier_uniform_(m.weight); nn.init.zeros_(m.bias)
33 def forward(self,x): return self.net(x)
34
35def d_exact(x): return torch.minimum(x,1-x)
36def target(x):
37 # Smooth quotient times the fractional boundary factor.
38 d=d_exact(x)
39 return d.pow(a)*(1.0+0.35*torch.sin(2*math.pi*x)+0.15*x)
40def weighted_grad(v,x):
41 grad=torch.autograd.grad(v.sum(),x,create_graph=True)[0]
42 d=d_exact(x).clamp_min(EPS)
43 return d.pow(1-a+s)*grad
44
45def train(kind, steps=1800, n=96):
46 torch.manual_seed(SEED)
47 model=MLP().to(device)
48 opt=torch.optim.Adam(model.parameters(),lr=2e-3)
49 hist=[]; t0=time.time()
50 for it in range(steps):
51 x=torch.rand(n,1,device=device)
52 # avoid the cusp of min(x,1-x) at the midpoint for this gradient regularizer
53 x.requires_grad_(kind=='idea')
54 y=target(x)
55 raw=model(x)
56 if kind=='idea':
57 u=d_exact(x).clamp_min(EPS).pow(a)*raw
58 wg=weighted_grad(raw,x)
59 loss=((u-y)**2).mean()+lam_g*(wg**2).mean()
60 else:
61 u=raw
62 # Standard free-output PINN-style boundary penalty.
63 xb=torch.tensor([[0.0],[1.0]],device=device)
64 loss=((u-y)**2).mean()+10.0*(model(xb)**2).mean()
65 opt.zero_grad(); loss.backward(); opt.step()
66 if it in (0,99,499,999,1799): hist.append(float(loss.detach().cpu()))
67 with torch.no_grad():
68 xx=torch.linspace(1e-5,1-1e-5,4000,device=device).view(-1,1)
69 pred=(d_exact(xx).clamp_min(EPS).pow(a)*model(xx) if kind=='idea' else model(xx))
70 yy=target(xx)
71 rel=float(torch.sqrt(((pred-yy)**2).mean())/torch.sqrt((yy**2).mean()))
72 strip=((xx<0.05)|(xx>0.95))
73 striperr=float(torch.sqrt(((pred[strip]-yy[strip])**2).mean())/torch.sqrt((yy[strip]**2).mean()))
74 maxb=float(torch.max(torch.abs(pred[strip]-yy[strip])))
75 return {'loss_checkpoints':hist,'relative_L2':rel,'boundary_strip_relative_L2':striperr,'boundary_strip_max_abs':maxb,'seconds':time.time()-t0}
76
77def math_check():
78 # For smooth v, the claimed weighted derivative should vanish at boundary,
79 # whereas u'=a*d^(a-1)*v has the fractional singular scaling.
80 x=torch.logspace(-6,-2,80,dtype=torch.float64)
81 v=1+0.4*x
82 vp=torch.full_like(x,0.4)
83 up=a*x**(a-1)*v+x**a*vp
84 w=x**(1-a+s)*vp
85 # Fit log-log slopes; expected u' slope a-1 and weighted smooth term slope 1-a+s.
86 su=float(np.polyfit(np.log(x.numpy()),np.log(torch.abs(up).numpy()),1)[0])
87 sw=float(np.polyfit(np.log(x.numpy()),np.log(torch.abs(w).numpy()),1)[0])
88 ratio=float((w[-1]/w[0]).item())
89 return {'unweighted_uprime_log_slope':su,'expected_unweighted_slope':a-1,
90 'weighted_vprime_log_slope':sw,'expected_weighted_slope':1-a+s,
91 'weighted_growth_across_range':ratio,'weighted_decreases_to_boundary':bool(w[0]<w[-1])}
92
93def main():
94 out={'device':str(device),'seed':SEED,'a':a,'s':s,'lambda_g':lam_g,
95 'math_check':math_check()}
96 out['baseline']=train('baseline'); out['idea']=train('idea')
97 out['idea_minus_baseline_relative_L2']=out['idea']['relative_L2']-out['baseline']['relative_L2']
98 out['idea_minus_baseline_boundary_strip']=out['idea']['boundary_strip_relative_L2']-out['baseline']['boundary_strip_relative_L2']
99 Path('results.json').write_text(json.dumps(out,indent=2))
100 print(json.dumps(out,indent=2))
101if __name__=='__main__': main()