Defect-and-Jacobian Residual Dynamics / experiment.py

✓✓ Beats tuned baseline

Raw ⬇ ZIP
  1import json, math, random
  2from pathlib import Path
  3import numpy as np
  4import torch
  5from torch import nn
  6
  7SEED = 1358
  8np.random.seed(SEED); random.seed(SEED); torch.manual_seed(SEED)
  9torch.set_num_threads(4)
 10M = 1.0
 11
 12# Klein-Gordon reduced to a single Fourier mode: u_t=v, v_t=-m sin(u).
 13def F(z):
 14    u, v = z[..., 0], z[..., 1]
 15    return np.stack([v, -M*np.sin(u)], axis=-1)
 16
 17def rk4(z, dt):
 18    k1=F(z); k2=F(z+.5*dt*k1); k3=F(z+.5*dt*k2); k4=F(z+dt*k3)
 19    return z + dt*(k1+2*k2+2*k3+k4)/6
 20
 21def math_checks():
 22    bg=np.array([.73,-.21]); r=np.array([.18,-.27]); bgdot=np.array([bg[1],-.11])
 23    defect=F(bg)-bgdot; ru=np.sin(bg[0]+r[0])-np.sin(bg[0])-np.cos(bg[0])*r[0]
 24    rhs=np.array([r[1],-M*r[0]+defect[1]-M*(np.cos(bg[0])-1)*r[0]-M*ru])
 25    identity_err=float(np.max(np.abs(F(bg+r)-bgdot-rhs)))
 26    eps=np.logspace(-5,-1,9)
 27    rem=np.array([abs(np.sin(bg[0]+e)-np.sin(bg[0])-np.cos(bg[0])*e) for e in eps])
 28    slope=float(np.polyfit(np.log(eps),np.log(rem),1)[0])
 29    coeff=float(np.median(rem[:5]/eps[:5]**2)); coeff_pred=abs(np.sin(bg[0]))/2
 30    # sigmoid gate g=1/(1+exp(-a(log d-b))); midpoint predicted d=exp(b)
 31    a=3.; b=math.log(.08); ds=np.logspace(-3,0,401)
 32    gates=1/(1+np.exp(-a*(np.log(ds)-b))); d_mid=float(ds[np.argmin(abs(gates-.5))])
 33    # logit(g) versus log(d) slope predicted a
 34    fit=float(np.polyfit(np.log(ds),np.log(gates/(1-gates)),1)[0])
 35    return {'identity_max_error':identity_err,'remainder_loglog_slope':slope,
 36            'remainder_quadratic_slope_predicted':2.,'remainder_coeff_est':coeff,
 37            'remainder_coeff_predicted':coeff_pred,'gate_midpoint_observed':d_mid,
 38            'gate_midpoint_predicted':math.exp(b),'gate_logit_slope_observed':fit,
 39            'gate_logit_slope_predicted':a}
 40
 41def make_data(n=1800, steps=25, dt=.04):
 42    x=np.random.uniform(-1.4,1.4,(n,2)); ys=[]
 43    for _ in range(steps):
 44        y=rk4(x,dt); ys.append((x.copy(),y.copy())); x=y
 45    return np.concatenate([a for a,b in ys]),np.concatenate([b for a,b in ys])
 46
 47class Net(nn.Module):
 48    def __init__(self, inp, out=2):
 49        super().__init__(); self.net=nn.Sequential(nn.Linear(inp,32),nn.Tanh(),nn.Linear(32,32),nn.Tanh(),nn.Linear(32,out))
 50    def forward(self,x): return self.net(x)
 51
 52def train(kind, X, Y, steps=500):
 53    dev='cuda' if torch.cuda.is_available() else 'cpu'
 54    try:
 55        tx=torch.tensor(X,dtype=torch.float32,device=dev); ty=torch.tensor(Y,dtype=torch.float32,device=dev)
 56        net=Net(2 if kind=='full' else 4).to(dev); opt=torch.optim.Adam(net.parameters(),lr=2e-3)
 57        for i in range(steps):
 58            idx=torch.randint(0,len(tx),(128,),device=dev); z=tx[idx]; target=ty[idx]
 59            if kind=='full': pred=net(z)
 60            else:
 61                # cheap background is a scaled oscillator drift; defect is explicitly supplied
 62                bg=.82*z; r=z-bg; bgdot=torch.stack((bg[:,1],-torch.sin(bg[:,0])),1)*.82
 63                fbg=torch.stack((bg[:,1],-torch.sin(bg[:,0])),1); defect=fbg-bgdot
 64                inp=torch.cat((bg,r,defect[:,1:2]),1) # 5 features
 65                if net.net[0].in_features != 5: net=Net(5).to(dev); opt=torch.optim.Adam(net.parameters(),lr=2e-3)
 66                pred=bg + dt_step(z,bg,r,defect,net(inp))
 67            loss=((pred-target)**2).mean(); opt.zero_grad(); loss.backward(); opt.step()
 68        with torch.no_grad():
 69            z=tx[:512]; target=ty[:512]
 70            if kind=='full': pred=net(z)
 71            else:
 72                bg=.82*z; r=z-bg; bgdot=torch.stack((bg[:,1],-torch.sin(bg[:,0])),1)*.82
 73                fbg=torch.stack((bg[:,1],-torch.sin(bg[:,0])),1); defect=fbg-bgdot
 74                inp=torch.cat((bg,r,defect[:,1:2]),1); pred=bg+dt_step(z,bg,r,defect,net(inp))
 75            one=float(torch.mean((pred-target)**2).sqrt().cpu())
 76        return net,one,dev
 77    except Exception as e:
 78        # CPU retry is intentionally simple and deterministic.
 79        if dev!='cpu': torch.cuda.empty_cache(); torch.cuda.is_available=lambda:False; return train(kind,X,Y,steps)
 80        raise e
 81
 82def dt_step(z,bg,r,defect,closure,dt=.04):
 83    linear=torch.stack((r[:,1],-r[:,0]),1)
 84    corr=torch.stack((torch.zeros_like(r[:,0]),-(torch.cos(bg[:,0])-1)*r[:,0]),1)
 85    gate=torch.sigmoid(3.0*(torch.log(torch.linalg.vector_norm(defect,dim=1)+1e-6)-math.log(.08))).unsqueeze(1)
 86    return dt*(linear+defect+corr+gate*closure)
 87
 88def rollout(kind, net, init, horizon=80, dt=.04):
 89    z=torch.tensor(init,dtype=torch.float32,device=next(net.parameters()).device); out=[]
 90    with torch.no_grad():
 91        for _ in range(horizon):
 92            if kind=='full': z=net(z)
 93            else:
 94                bg=.82*z; r=z-bg; bgdot=torch.stack((bg[:,1],-torch.sin(bg[:,0])),1)*.82
 95                fbg=torch.stack((bg[:,1],-torch.sin(bg[:,0])),1); defect=fbg-bgdot
 96                inp=torch.cat((bg,r,defect[:,1:2]),1); z=bg+dt_step(z,bg,r,defect,net(inp))
 97            out.append(z.cpu().numpy())
 98    return np.stack(out)
 99
100def main():
101    checks=math_checks(); X,Y=make_data(); split=int(.8*len(X));
102    results={}
103    for k in ('full','residual'):
104        net,one,dev=train(k,X[:split],Y[:split]); pred=rollout(k,net,X[split:split+64],60)
105        true=[]; z=X[split:split+64].copy()
106        for _ in range(60): z=rk4(z,.04); true.append(z.copy())
107        longerr=float(np.sqrt(np.mean((pred-np.array(true))**2)))
108        results[k]={'one_step_rmse':one,'rollout_rmse':longerr,'device':dev}
109    Path('results.json').write_text(json.dumps({'math_checks':checks,'comparison':results},indent=2))
110    print(json.dumps({'math_checks':checks,'comparison':results},indent=2))
111
112if __name__=='__main__': main()