Walk-on-Spheres stochastic target layer / wos_experiment.py
Mechanism confirmed, baseline not beaten
1import json, math, time
2import numpy as np
3import torch
4from torch import nn
5
6SEED=2049
7np.random.seed(SEED); torch.manual_seed(SEED)
8try:
9 device=torch.device('cuda' if torch.cuda.is_available() else 'cpu')
10 if device.type=='cuda':
11 torch.zeros(1,device=device).sum().item()
12except Exception:
13 device=torch.device('cpu')
14
15def directions(n,d,rng):
16 q=rng.normal(size=(n,d)); return q/np.linalg.norm(q,axis=1,keepdims=True)
17
18def wos_ball(x, K, M, d, lam=0., delta=1e-4, corrected=False, seed=0):
19 rng=np.random.default_rng(seed); x=np.asarray(x); B=len(x)
20 z=np.repeat(x[:,None,:],K,axis=1); t=np.zeros((B,K)); y=np.zeros((B,K))
21 for j in range(M):
22 r=1.-np.linalg.norm(z,axis=2)
23 active=r>delta
24 dt=r*r/((d if corrected else 2*d))
25 y += np.exp(-lam*t)*dt # h1=1, h0=0
26 u=directions(B*K,d,rng).reshape(B,K,d)
27 z += r[...,None]*u; t += dt
28 if not active.any(): break
29 # zero boundary data, approximate truncation by zero
30 return y.mean(axis=1), y, t
31
32def wos_square(x,K,M,delta=2e-3,seed=0):
33 rng=np.random.default_rng(seed); x=np.asarray(x); B=len(x); d=2
34 z=np.repeat(x[:,None,:],K,axis=1); t=np.zeros((B,K)); y=np.zeros((B,K))
35 for j in range(M):
36 r=np.minimum(z,1-z).min(axis=2); dt=r*r/(2*d)
37 f=2*(z[:,:,0]*(1-z[:,:,0])+z[:,:,1]*(1-z[:,:,1]))
38 y += f*dt
39 u=directions(B*K,d,rng).reshape(B,K,d); z += r[...,None]*u; t += dt
40 if np.max(r)<delta: break
41 return y.mean(axis=1)
42
43def analytic_square(x):
44 return x[:,0]*(1-x[:,0])*x[:,1]*(1-x[:,1])
45
46def analytic_ball(x): return (1-(x*x).sum(axis=1))/(2*x.shape[1])
47
48class MLP(nn.Module):
49 def __init__(self,d):
50 super().__init__(); self.net=nn.Sequential(nn.Linear(d,32),nn.Tanh(),nn.Linear(32,32),nn.Tanh(),nn.Linear(32,1))
51 def forward(self,x): return self.net(x).squeeze(-1)
52
53def train_wos(idea=True, steps=350, d=2, K=8):
54 torch.manual_seed(SEED); rng=np.random.default_rng(SEED+3)
55 model=MLP(d).to(device); opt=torch.optim.Adam(model.parameters(),lr=2e-3)
56 start=time.time()
57 for s in range(steps):
58 x=rng.uniform(.03,.97,size=(64,d))
59 if idea: y=wos_square(x,K,25,seed=SEED+s)
60 else: y=analytic_square(x) # ideal exact target is a favorable standard supervised reference
61 xt=torch.tensor(x,dtype=torch.float32,device=device); yt=torch.tensor(y,dtype=torch.float32,device=device)
62 loss=((model(xt)-yt)**2).mean(); opt.zero_grad(); loss.backward(); opt.step()
63 grid=np.stack(np.meshgrid(np.linspace(.03,.97,25),np.linspace(.03,.97,25),indexing='ij'),-1).reshape(-1,d)
64 with torch.no_grad(): pred=model(torch.tensor(grid,dtype=torch.float32,device=device)).cpu().numpy()
65 rel=np.sqrt(np.mean((pred-analytic_square(grid))**2))/np.sqrt(np.mean(analytic_square(grid)**2))
66 return float(rel),time.time()-start
67
68def main():
69 # Prediction 1: exact Brownian ball exit time is r^2/d; supplied formula predicts r^2/(2d).
70 dvals=[1,2,4,8]; r=.37; exit_rows=[]
71 for d in dvals:
72 supplied=r*r/(2*d); exact=r*r/d
73 exit_rows.append({'d':d,'predicted_supplied':supplied,'exact_MC_theory':exact,'ratio_exact/supplied':exact/supplied})
74 # Direct one-step isotropic identity verifies E|z+rU|^2=|z|^2+r^2 and Poisson exit-time equation.
75 rng=np.random.default_rng(SEED); z=np.array([.2,-.1]); rr=.4; u=directions(500000,2,rng)
76 radial_increment=float(np.mean(np.sum((z+rr*u)**2,axis=1)-np.sum(z*z)))
77 # Prediction 2: with h1=1 and zero boundary, infinite-depth WOS converges to half the exact ball solution under supplied dt.
78 x=np.array([[0.,0.],[.3,0.],[.0,.6]])
79 exact=analytic_ball(x); rows=[]
80 for M in [2,4,8,16,32,64]:
81 est,_,_=wos_ball(x,30000,M,2,seed=SEED+M)
82 rows.append({'M':M,'mean_relative_to_exact':float(np.mean(est/exact)),'abs_bias_center':float(abs(est[0]-exact[0]))})
83 # Prediction 3: MC variance of the mean scales as 1/K.
84 x0=np.array([[.2,.15]])
85 _, paths,_=wos_ball(x0,20000,32,2,seed=77)
86 var1=float(np.var(paths[0],ddof=1)); varrows=[]
87 for K in [1,2,4,8,16,32,64,128]:
88 # grouped means from common samples; observed K*Var(mean) should be constant
89 m=paths[0][:len(paths[0])//K*K].reshape(-1,K).mean(1)
90 var=float(np.var(m,ddof=1)); varrows.append({'K':K,'variance':var,'K_times_variance':K*var,'ratio_to_K1':var/var1})
91 # small comparison: WOS stochastic target versus exact-target MLP (same model/steps).
92 idea_rel,idea_sec=train_wos(True); base_rel,base_sec=train_wos(False)
93 out={'device':str(device),'exit_time_check':exit_rows,'sphere_second_moment_increment':radial_increment,'predicted_increment':rr*rr,'finite_depth_source_check':rows,'variance_scaling':{'single_path_variance':var1,'rows':varrows},'training_comparison':{'wos_target_relative_rmse':idea_rel,'exact_target_relative_rmse':base_rel,'wos_seconds':idea_sec,'exact_seconds':base_sec},'note':'The authoritative formula uses r^2/(2d); standard Brownian motion uses r^2/d, so the source estimator is systematically mis-scaled by approximately 1/2.'}
94 print(json.dumps(out,indent=2))
95
96if __name__=='__main__': main()