Particular-Integral Latent Reduction / experiment.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
 1import json, math, random
 2import numpy as np
 3import torch
 4from torch import nn
 5
 6SEED=507
 7random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
 8torch.set_num_threads(4)
 9device=torch.device('cuda' if torch.cuda.is_available() else 'cpu')
10
11class MLP(nn.Module):
12    def __init__(self, din=3, dout=3, width=32):
13        super().__init__(); self.net=nn.Sequential(nn.Linear(din,width),nn.Tanh(),nn.Linear(width,width),nn.Tanh(),nn.Linear(width,dout))
14    def forward(self,z): return self.net(z)
15
16class ClosureModel(nn.Module):
17    # f(z)=z_g, and enforce f_dot=a(z)f exactly in the vector field.
18    def __init__(self,width=32):
19        super().__init__(); self.main=MLP(3,2,width); self.a=MLP(3,1,width)
20    def forward(self,z):
21        return torch.cat([self.main(z), self.a(z)*z[:,2:3]],dim=1)
22
23def rk4(model,z,dt):
24    k1=model(z); k2=model(z+dt*k1/2); k3=model(z+dt*k2/2); k4=model(z+dt*k3)
25    return z+dt*(k1+2*k2+2*k3+k4)/6
26
27def main():
28    # Core math sanity: f_dot=A f, so exact flow preserves f=0; Euler only has roundoff/solver error.
29    A=np.array([[-.3,.2],[-.1,-.4]])
30    f=np.zeros(2); f[0]=1e-12
31    vals=[]
32    for _ in range(10000): f=f+1e-3*A.dot(f); vals.append(np.linalg.norm(f))
33    math_check={'initial_norm':1e-12,'final_norm_euler':float(vals[-1]),'max_norm_euler':float(max(vals)),
34                'zero_exact_norm':float(np.linalg.norm(np.zeros(2)))}
35
36    # Derivative training data: oscillator plus a gauge coordinate g whose true derivative is zero.
37    n=256
38    x=torch.randn(n,2)*1.5
39    g=torch.zeros(n,1)
40    z=torch.cat([x,g],1).to(device)
41    target=torch.cat([torch.stack([x[:,1],-x[:,0]],1),torch.zeros(n,1)],1).to(device)
42    # realistic finite measurement/derivative noise, fixed once for both models
43    noise=torch.tensor(np.random.default_rng(SEED).normal(0,.035,(n,3)),dtype=torch.float32,device=device)
44    noisy_target=target+noise
45    base=MLP(width=46).to(device); clo=ClosureModel(width=32).to(device)
46    # same optimizer budget and comparable widths
47    ob=torch.optim.Adam(base.parameters(),lr=3e-3); oc=torch.optim.Adam(clo.parameters(),lr=3e-3)
48    for step in range(1400):
49        for model,opt in [(base,ob),(clo,oc)]:
50            opt.zero_grad(); pred=model(z); loss=((pred-noisy_target)**2).mean()
51            if model is clo:
52                # explicit residual J_f v - A f; for f=z_g this is v_g-a*f and is zero by construction
53                # retain the stated closure loss as an auditable term
54                f=z[:,2:3]; v=pred; a=clo.a(z); residual=v[:,2:3]-a*f
55                loss=loss+10.*(residual**2).mean()
56            loss.backward(); opt.step()
57    # Long horizon, starting exactly on M_f. True state remains on g=0.
58    init=torch.tensor([[1.2,0.3,0.0]],dtype=torch.float32,device=device)
59    zb=init.clone(); zc=init.clone(); dt=.05; steps=400
60    drift_b=[]; drift_c=[]; err_b=[]; err_c=[]
61    true=init.clone()
62    for i in range(steps):
63        zb=rk4(base,zb,dt); zc=rk4(clo,zc,dt)
64        true=rk4(lambda q: torch.cat([q[:,1:2],-q[:,0:1],torch.zeros_like(q[:,2:3])],1),true,dt)
65        drift_b.append(abs(float(zb[0,2]))); drift_c.append(abs(float(zc[0,2])))
66        err_b.append(float(torch.linalg.norm(zb[0,:2]-true[0,:2]))); err_c.append(float(torch.linalg.norm(zc[0,:2]-true[0,:2])))
67    # Also test the defining property from a perturbed manifold point: closure keeps f=0 invariant, not arbitrary f=constant.
68    out={'device':str(device),'math_check':math_check,
69         'training':{'baseline_params':sum(p.numel() for p in base.parameters()),'closure_params':sum(p.numel() for p in clo.parameters())},
70         'rollout':{'baseline_final_constraint':drift_b[-1],'closure_final_constraint':drift_c[-1],
71                    'baseline_max_constraint':max(drift_b),'closure_max_constraint':max(drift_c),
72                    'baseline_final_xy_error':err_b[-1],'closure_final_xy_error':err_c[-1],
73                    'baseline_mean_xy_error':float(np.mean(err_b)),'closure_mean_xy_error':float(np.mean(err_c))}}
74    with open('results.json','w') as f: json.dump(out,f,indent=2)
75    print(json.dumps(out,indent=2))
76
77if __name__=='__main__':
78    try: main()
79    except Exception as e:
80        if torch.cuda.is_available():
81            print('CUDA failed, rerun on CPU:',repr(e)); os.environ['CUDA_VISIBLE_DEVICES']=''
82        raise