Measurement-Space Neural Operator with Mesh Transfer / experiment.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import json, math, time, random
  2import numpy as np
  3import torch
  4from torch import nn
  5
  6SEED=2149
  7random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
  8device='cuda' if torch.cuda.is_available() else 'cpu'
  9try:
 10    if device=='cuda': torch.cuda.set_device(0)
 11except Exception:
 12    device='cpu'
 13
 14torch.set_num_threads(8)
 15
 16# Smooth random Fourier fields and a known operator: output is a shifted,
 17# mildly nonlinear transform of the input field.
 18def fields(n, m, phase=0):
 19    x=np.linspace(0,1,m,endpoint=False)[None,:]
 20    rng=np.random.RandomState(SEED+phase)
 21    a=rng.randn(n,5)*np.array([1,.6,.4,.3,.2])[None,:]
 22    b=rng.randn(n,5)*np.array([1,.6,.4,.3,.2])[None,:]
 23    u=np.zeros((n,m));
 24    for k in range(1,6): u += a[:,k-1,None]*np.sin(2*np.pi*k*x)+b[:,k-1,None]*np.cos(2*np.pi*k*x)
 25    u=u/(np.std(u,axis=1,keepdims=True)+1e-6)*.7
 26    # operator is pointwise nonlinear plus shifted low-frequency component
 27    y=np.tanh(u)+.25*np.roll(u,2,axis=1)
 28    return x.astype(np.float32),u.astype(np.float32),y.astype(np.float32)
 29
 30def interp_periodic(x0, v, q):
 31    xx=np.mod(x0,1); order=np.argsort(xx); xx=xx[order]; vv=v[...,order]
 32    xx2=np.r_[xx,xx[0]+1]; vv2=np.concatenate([vv,vv[...,0:1]],axis=-1)
 33    qq=np.mod(q,1); j=np.searchsorted(xx2,qq,side='right')-1; j=np.clip(j,0,len(xx2)-2)
 34    t=(qq-xx2[j])/(xx2[j+1]-xx2[j]+1e-8)
 35    return vv2[...,j]*(1-t)+vv2[...,j+1]*t
 36
 37class MeasurementNet(nn.Module):
 38    def __init__(self,z=32):
 39        super().__init__(); self.enc=nn.Sequential(nn.Linear(2,48),nn.Tanh(),nn.Linear(48,z),nn.Tanh())
 40        self.g=nn.Sequential(nn.Linear(z,64),nn.Tanh(),nn.Linear(64,z),nn.Tanh())
 41        self.dec=nn.Sequential(nn.Linear(z+1,64),nn.Tanh(),nn.Linear(64,1))
 42    def encode(self, coords, vals):
 43        h=self.enc(torch.stack((coords,vals),-1)); return h.mean(1)
 44    def forward(self, coords, vals, q):
 45        z=self.g(self.encode(coords,vals)); zz=z[:,None,:].expand(-1,q.shape[1],-1)
 46        return self.dec(torch.cat((zz,q[:,:,None]),-1)).squeeze(-1)
 47
 48class FixedNet(nn.Module):
 49    def __init__(self,m=32):
 50        super().__init__(); self.net=nn.Sequential(nn.Linear(m,96),nn.Tanh(),nn.Linear(96,128),nn.Tanh(),nn.Linear(128,64))
 51    def forward(self,v): return self.net(v)
 52
 53def train_models():
 54    x,u,y=fields(900,64,1); xt,ut,yt=fields(180,64,2)
 55    # canonical measurements are first 32 regular points, output 64 grid
 56    ci=np.linspace(0,63,32).round().astype(int); qi=np.arange(64)
 57    mc=MeasurementNet().to(device); base=FixedNet().to(device)
 58    opt=torch.optim.Adam(mc.parameters(),lr=3e-3); opb=torch.optim.Adam(base.parameters(),lr=3e-3)
 59    X=torch.tensor(x[:,ci],device=device); U=torch.tensor(u[:,ci],device=device); Q=torch.tensor(x[:,qi],device=device)
 60    Y=torch.tensor(y,device=device); UB=torch.tensor(u[:,ci],device=device)
 61    for step in range(300):
 62        ix=torch.randint(0,len(x),(96,),device=device)
 63        pred=mc(X[ix],U[ix],Q[ix]); loss=((pred-Y[ix])**2).mean()
 64        opt.zero_grad(); loss.backward(); opt.step()
 65        pb=base(UB[ix]); lb=((pb-Y[ix])**2).mean(); opb.zero_grad(); lb.backward(); opb.step()
 66    return mc.cpu().eval(),base.cpu().eval(),(x,u,y,xt,ut,yt,ci)
 67
 68def predict_mesh(model, coords, vals, q):
 69    with torch.no_grad():
 70        return model(torch.tensor(coords[None],dtype=torch.float32),torch.tensor(vals[None],dtype=torch.float32),torch.tensor(q[None],dtype=torch.float32)).numpy()[0]
 71
 72def main():
 73    t=time.time(); mc,base,data=train_models(); x,u,y,xt,ut,yt,ci=data
 74    # Mesh transfer: input sensors are alternative layouts; output is arbitrary query mesh.
 75    rng=np.random.RandomState(77); rows=[]
 76    for n in [8,16,32,64]:
 77        errs=[]
 78        ss=np.sort(rng.rand(len(xt),n),axis=1); vals=np.stack([interp_periodic(x[0],ut[i],ss[i]) for i in range(len(xt))])
 79        q=np.sort(rng.rand(len(xt),48),axis=1); truth=np.stack([interp_periodic(x[0],yt[i],q[i]) for i in range(len(xt))])
 80        with torch.no_grad():
 81            pred=mc(torch.tensor(ss,dtype=torch.float32),torch.tensor(vals,dtype=torch.float32),torch.tensor(q,dtype=torch.float32)).numpy()
 82        rows.append((n,float(np.sqrt(np.mean((pred-truth)**2)))))
 83    # baseline canonical error on its supported fixed grid, and baseline cannot accept new mesh.
 84    with torch.no_grad():
 85        bp=base(torch.tensor(ut[:,ci],dtype=torch.float32)).numpy()
 86    base_rmse=float(np.sqrt(np.mean((bp-yt)**2)))
 87    # Core prediction 1: permutation invariance of E_X.
 88    i=0; s=np.sort(rng.rand(20)); vv=interp_periodic(x[0],u[i],s)
 89    p1=predict_mesh(mc,s,vv,np.linspace(0,1,20,endpoint=False)); perm=rng.permutation(20)
 90    p1p=predict_mesh(mc,s[perm],vv[perm],np.linspace(0,1,20,endpoint=False))
 91    perm_diff=float(np.max(np.abs(p1-p1p)))
 92    # Core prediction 2: Lipschitz composition discrepancy scales linearly with reconstruction perturbation.
 93    # Hold a measurement set fixed and perturb reconstructed function by amplitude delta.
 94    s=np.linspace(0,1,32,endpoint=False); vv=interp_periodic(x[0],u[0],s); q=np.linspace(0,1,40,endpoint=False)
 95    direction=np.sin(2*np.pi*3*s); deltas=np.array([0,.01,.02,.04,.08,.16]); diffs=[]
 96    for d in deltas:
 97        a=predict_mesh(mc,s,vv,q); b=predict_mesh(mc,s,vv+d*direction,q); diffs.append(float(np.sqrt(np.mean((a-b)**2))))
 98    slope=float(np.polyfit(deltas[1:],diffs[1:],1)[0]); r2=float(np.corrcoef(deltas,diffs)[0,1])
 99    # Core prediction 3: more measurements improve reconstruction/transfer, with diminishing returns.
100    # Compare fixed Fourier interpolation reconstruction error against true input field.
101    recon=[]
102    for n in [4,8,16,32,64]:
103        ee=[]
104        for i in range(50):
105            s=np.sort(rng.rand(n)); vv=interp_periodic(x[0],u[i],s); q=x[0]
106            ee.append(np.mean((interp_periodic(s,vv,q)-u[i])**2))
107        recon.append((n,float(np.sqrt(np.mean(ee)))))
108    out={'device':device,'seconds':round(time.time()-t,2),'baseline_fixed32_rmse':base_rmse,
109         'idea_mesh_transfer_rmse_by_input_sensors':rows,'prediction_permutation_max_difference':perm_diff,
110         'prediction_lipschitz_sweep':{'deltas':deltas.tolist(),'output_discrepancies':diffs,'linear_slope':slope,'correlation':r2},
111         'prediction_sensor_density_reconstruction_rmse':recon,
112         'note':'Measurement model uses coordinate/value mean pooling and coordinate-conditioned output decoder; baseline is fixed-grid MLP.'}
113    print(json.dumps(out,indent=2))
114if __name__=='__main__':
115    try: main()
116    except Exception as e:
117        if device=='cuda':
118            print('CUDA failed, rerun CPU:',repr(e)); device='cpu'; torch.cuda.empty_cache(); main()
119        else: raise