Singularity-Enriched Neural Ansatz / experiment.py
Beats tuned baseline
1import json, math, random
2import numpy as np
3import torch
4from torch import nn
5
6SEED=1234
7random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
8torch.set_num_threads(4)
9try:
10 device=torch.device('cuda' if torch.cuda.is_available() else 'cpu')
11 # Probe CUDA so an unavailable/broken shared device falls back cleanly.
12 if device.type=='cuda':
13 torch.zeros(1,device=device).sum().item()
14except Exception:
15 device=torch.device('cpu')
16
17lam=0.5
18
19def field_np(x):
20 r=np.sqrt(x[:,0]**2+x[:,1]**2)
21 th=np.arctan2(x[:,1],x[:,0])
22 sing=r**lam*np.sin(th/2)
23 smooth=0.2*np.sin(np.pi*x[:,0])*np.sin(np.pi*x[:,1])+0.1*x[:,0]*x[:,1]
24 return sing+smooth
25
26def sample_uniform(n): return np.random.rand(n,2)
27def sample_graded(n):
28 # Concentrate radial mass near the junction while retaining the full square.
29 # Independent Beta(.55,1) coordinates gives a near-corner radial grading.
30 return np.random.beta(.55,1.0,size=(n,2))
31
32class MLP(nn.Module):
33 def __init__(self, enriched=False):
34 super().__init__(); self.enriched=enriched
35 self.net=nn.Sequential(nn.Linear(2,64),nn.Tanh(),nn.Linear(64,64),nn.Tanh(),nn.Linear(64,64),nn.Tanh(),nn.Linear(64,1))
36 if enriched: self.a=nn.Parameter(torch.tensor([0.8]))
37 def forward(self,x):
38 z=self.net(x)
39 if self.enriched:
40 r=torch.sqrt((x*x).sum(1)+1e-12); th=torch.atan2(x[:,1],x[:,0])
41 phi=torch.sin(th/2).unsqueeze(1)
42 r=r.unsqueeze(1)
43 # cutoff is one in the relevant near-corner region and smoothly vanishes
44 r0=.7; q=torch.clamp(r/r0,0,1)
45 chi=1-3*q*q+2*q*q*q
46 sing=chi*r.pow(lam)*phi*self.a
47 return sing+(1-chi)*z+chi*z*r.pow(lam)
48 return z
49
50def train(enriched, sampler, n=1600, steps=1400):
51 x=torch.tensor(sampler(n),dtype=torch.float32,device=device)
52 y=torch.tensor(field_np(x.detach().cpu().numpy()),dtype=torch.float32,device=device).view(-1,1)
53 model=MLP(enriched).to(device)
54 opt=torch.optim.Adam(model.parameters(),lr=2e-3)
55 for i in range(steps):
56 # fixed points makes the equal-evaluation comparison deterministic
57 pred=model(x); loss=((pred-y)**2).mean()
58 opt.zero_grad(); loss.backward(); opt.step()
59 return model, float(loss.detach().cpu())
60
61def evaluate(model):
62 # fixed dense test grid, plus a corner-only region
63 g=np.linspace(.001,1,121); xx,yy=np.meshgrid(g,g); X=np.c_[xx.ravel(),yy.ravel()]
64 with torch.no_grad(): p=model(torch.tensor(X,dtype=torch.float32,device=device)).cpu().numpy().ravel()
65 y=field_np(X)
66 rel=np.linalg.norm(p-y)/np.linalg.norm(y)
67 mask=np.sqrt((X*X).sum(1))<.12
68 corner=np.linalg.norm(p[mask]-y[mask])/np.linalg.norm(y[mask])
69 return float(rel),float(corner),int(mask.sum())
70
71def math_check():
72 # Along a fixed ray the singular part must have log-log slope lambda.
73 r=np.logspace(-6,-1,80); X=np.c_[r,r]
74 s=np.sqrt(2*r*r)**lam*np.sin(np.pi/8)
75 slope=np.polyfit(np.log(r),np.log(np.abs(s)),1)[0]
76 # Smooth remainder has bounded first radial derivative numerically at the corner.
77 # Compare its radial increments at two scales (increments should be O(r)).
78 def rem(t): return .2*np.sin(np.pi*t/np.sqrt(2))**2+.1*(t/np.sqrt(2))**2
79 inc1=abs(rem(2e-3)-rem(1e-3)); inc2=abs(rem(4e-3)-rem(2e-3))
80 return {'loglog_slope':float(slope),'expected_lambda':lam,'slope_abs_error':float(abs(slope-lam)), 'smooth_increment_ratio':float(inc2/inc1)}
81
82if __name__=='__main__':
83 check=math_check()
84 results={}
85 configs=[('baseline_uniform',False,sample_uniform),('baseline_graded',False,sample_graded),('enriched_uniform',True,sample_uniform),('enriched_graded',True,sample_graded)]
86 for name,en,samp in configs:
87 m,trainloss=train(en,samp)
88 rel,corner,count=evaluate(m)
89 results[name]={'train_mse':trainloss,'relative_l2':rel,'corner_relative_l2':corner,'corner_test_points':count}
90 # Verify grading actually changes allocation without changing point count.
91 u=sample_uniform(100000); g=sample_graded(100000)
92 grading={'n_uniform':len(u),'n_graded':len(g),'fraction_r_lt_.12_uniform':float((np.linalg.norm(u,axis=1)<.12).mean()),'fraction_r_lt_.12_graded':float((np.linalg.norm(g,axis=1)<.12).mean())}
93 out={'device':str(device),'math_check':check,'grading_check':grading,'results':results,'seed':SEED,'train_points':1600,'steps':1400}
94 print(json.dumps(out,indent=2))