Symplectic Recurrent Block / experiment.py
Mechanism confirmed, baseline not beaten
1import json, math, random
2import numpy as np
3import torch
4from torch import nn
5from symplectic_block import SymplecticBlock
6
7SEED=646
8random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
9
10def gradV(q): return q+0.4*q**3
11def H(q,p): return .5*(q*q+p*p)+.1*q**4
12
13def leap(q,p,h):
14 p=p-.5*h*gradV(q); q=q+h*p; p=p-.5*h*gradV(q); return q,p
15def euler(q,p,h): return q+h*p,p-h*gradV(q)
16def metrics(fn,q0=1.,p0=.3,h=.1,steps=10000):
17 q,p=q0,p0; e0=float(H(q,p)); mx=me=0.; div=None; samples=[]
18 for k in range(steps):
19 try: q,p=fn(q,p,h); norm=math.hypot(float(q),float(p)); en=float(H(q,p))
20 except (OverflowError,ValueError,ZeroDivisionError): div=k+1; norm=en=float('inf'); break
21 if not math.isfinite(norm) or not math.isfinite(en): div=k+1; break
22 mx=max(mx,norm); me=max(me,abs(en-e0))
23 if k in (9,99,999,4999,9999): samples.append([k+1,norm,en-e0])
24 return {'final_norm':norm,'max_norm':mx,'max_abs_energy_error':me,'diverged_at_step':div,'samples':samples}
25
26def learned_regression():
27 torch.manual_seed(SEED); n=256; x=torch.randn(n,1); q=torch.randn(n,2); p=torch.zeros_like(q)
28 target_q=q+0.1*p; target_p=p-0.1*(q+0.4*q**3)
29 sym=SymplecticBlock(2,1,substeps=2,hidden=16,step=.1)
30 opt=torch.optim.Adam(sym.parameters(),lr=3e-3)
31 for _ in range(80):
32 opt.zero_grad(); qo,po=sym(q,p,x); loss=((qo-target_q)**2+(po-target_p)**2).mean(); loss.backward(); opt.step()
33 qo,po=sym(q,p,x); symloss=float(((qo-target_q)**2+(po-target_p)**2).mean().detach())
34 base=nn.Sequential(nn.Linear(5,16),nn.Tanh(),nn.Linear(16,4)); opt=torch.optim.Adam(base.parameters(),lr=3e-3)
35 inp=torch.cat([q,p,x.expand(n,1)],1); target=torch.cat([target_q,target_p],1)
36 for _ in range(80):
37 opt.zero_grad(); loss=((base(inp)-target)**2).mean(); loss.backward(); opt.step()
38 baseloss=float(((base(inp)-target)**2).mean().detach())
39 return {'one_step_mse':{'euler_analogue':baseloss,'learned_symplectic':symloss},'parameters':{'euler':sum(p.numel() for p in base.parameters()),'symplectic':sum(p.numel() for p in sym.parameters())}}
40
41def main():
42 z=torch.tensor([.7,-.4],dtype=torch.float64,requires_grad=True)
43 def lf(v): return torch.stack(leap(v[0],v[1],.2))
44 def eu(v): return torch.stack(euler(v[0],v[1],.2))
45 dl=float(torch.linalg.det(torch.autograd.functional.jacobian(lf,z))); de=float(torch.linalg.det(torch.autograd.functional.jacobian(eu,z)))
46 qr,pr=1.,.3
47 for _ in range(100000): qr,pr=leap(qr,pr,1e-3)
48 errors={}
49 for name,fn in [('leapfrog',leap),('euler',euler)]:
50 try:
51 q,p=1.,.3
52 for _ in range(1000): q,p=fn(q,p,.1)
53 errors[name]=float(math.hypot(q-qr,p-pr))
54 except (OverflowError,ValueError): errors[name]=None
55 out={'seed':SEED,'jacobian_determinant':{'leapfrog':dl,'euler':de},'rollouts':{str(h):{'leapfrog':metrics(leap,h=h),'euler':metrics(euler,h=h)} for h in (.1,.2)},'T100_error':errors,'learned_regression':learned_regression()}
56 with open('results.json','w') as f: json.dump(out,f,indent=2)
57 print(json.dumps(out,indent=2))
58if __name__=='__main__': main()