Nonreversible latent instanton sampler / experiment.py
Mechanism confirmed, baseline not beaten
1import json, math, random
2import numpy as np
3import torch
4
5SEED = 540
6np.random.seed(SEED); random.seed(SEED); torch.manual_seed(SEED)
7torch.set_default_dtype(torch.float64)
8try:
9 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
10except Exception:
11 device = torch.device('cpu')
12
13# 2D nonequilibrium Langevin system: double well plus rotational current.
14omega, T, dt, K = 1.25, 0.08, 0.05, 50
15A = torch.tensor([-1.0, 0.0], device=device)
16B = torch.tensor([1.0, 0.0], device=device)
17
18def mobility(z):
19 # Positive, state-dependent diagonal mobility (known ground truth).
20 x, y = z[..., 0], z[..., 1]
21 return torch.stack((0.35 + 0.85*torch.sigmoid(2.0*x),
22 0.55 + 0.45*torch.sigmoid(-1.5*y)), dim=-1)
23
24def potential_grad(z):
25 x, y = z[..., 0], z[..., 1]
26 return torch.stack((x*(x*x-1.0), y), dim=-1)
27
28def drift(z):
29 g = potential_grad(z)
30 # nonequilibrium rotational component, not a gradient or time reversal
31 rot = torch.stack((-z[..., 1], z[..., 0]), dim=-1)
32 return -g + omega*rot
33
34def action(path):
35 z0, z1 = path[:-1], path[1:]
36 m = mobility(z0)
37 v = (z1-z0)/dt
38 residual = v-drift(z0)
39 # a = 2 T M; FW action = .5 dt residual^T a^-1 residual
40 return (0.25*dt/T * (residual*residual/m).sum(dim=-1)).sum()
41
42def optimize_path(init, steps=1200, lr=0.025):
43 inner = torch.nn.Parameter(init[1:-1].clone())
44 opt = torch.optim.Adam([inner], lr=lr)
45 history=[]
46 for i in range(steps):
47 path = torch.cat((A[None], inner, B[None]), dim=0)
48 loss = action(path)
49 opt.zero_grad(); loss.backward(); opt.step()
50 if i % 100 == 0: history.append(float(loss.detach().cpu()))
51 path = torch.cat((A[None], inner.detach(), B[None]), dim=0)
52 return path, history
53
54def check_sde(n=150000):
55 # Verify both claims in the stochastic equation: conditional mean drift and variance 2TMdt.
56 z = torch.tensor([0.35, -0.7], device=device)
57 m = mobility(z); b = drift(z)
58 eps = torch.randn(n, 2, device=device)
59 inc = b*dt + torch.sqrt(2*T*m*dt)*eps
60 mean_err = float(torch.max(torch.abs(inc.mean(0)/dt-b)).cpu())
61 var_ratio = (inc.var(0)/(2*T*m*dt)).detach().cpu().numpy()
62 return {'mean_abs_error_per_dt': mean_err, 'variance_ratio': var_ratio.tolist(),
63 'expected_variance_ratio': [1.0,1.0]}
64
65def stochastic_success(path, mode, n=1200, gain=7.0, seed=1234):
66 gen = torch.Generator(device=device); gen.manual_seed(seed)
67 z = A[None,:].repeat(n,1)
68 min_dist = torch.full((n,), 99.0, device=device)
69 for k in range(K):
70 if mode == 'optimized':
71 # local tube controller following the optimized nonreversible path
72 target = path[k]
73 r = (path[k+1]-path[k])/dt + gain*(target-z)
74 elif mode == 'reverse_relaxation':
75 # standard time-reversed-relaxation control, with no path optimization
76 r = -drift(z)
77 else:
78 raise ValueError(mode)
79 m = mobility(z)
80 noise = torch.randn(z.shape, generator=gen, device=device)
81 z = z + r*dt + torch.sqrt(2*T*m*dt)*noise
82 min_dist = torch.minimum(min_dist, torch.linalg.vector_norm(z-B, dim=1))
83 finite = torch.isfinite(z).all(dim=1)
84 dist = torch.linalg.vector_norm(torch.nan_to_num(z, nan=1e6, posinf=1e6, neginf=-1e6)-B, dim=1)
85 reach = finite & (dist < 0.35)
86 finite_dist = dist[finite]
87 mean_dist = float(finite_dist.mean().cpu()) if finite_dist.numel() else float('inf')
88 finite_min = min_dist[torch.isfinite(min_dist)]
89 mean_min = float(finite_min.mean().cpu()) if finite_min.numel() else float('inf')
90 return float(reach.float().mean().cpu()), mean_dist, mean_min, int(finite.sum().cpu())
91
92def main():
93 # Core action sanity check: constant path velocity has the stated quadratic residual.
94 straight = torch.linspace(0,1,K+1,device=device)[:,None]*B + torch.linspace(1,0,K+1,device=device)[:,None]*A
95 straight_s = float(action(straight).cpu())
96 # Add small deterministic perturbations and check action rises around this fixed path.
97 perturb = straight.clone(); perturb[1:-1,1] += 0.08*torch.sin(torch.arange(1,K,device=device))
98 pert_s = float(action(perturb).cpu())
99 path, hist = optimize_path(straight + torch.cat((torch.zeros(1,2,device=device), 0.03*torch.randn(K-1,2,device=device), torch.zeros(1,2,device=device))))
100 opt_s = float(action(path).cpu())
101 check = check_sde()
102 base = stochastic_success(path, 'reverse_relaxation')
103 idea = stochastic_success(path, 'optimized')
104 result = {'device':str(device), 'K':K, 'dt':dt, 'T':T, 'omega':omega,
105 'math_check':check, 'action_check':{'straight':straight_s,'perturbed':pert_s,'optimized':opt_s,
106 'optimization_history':hist},
107 'baseline_reverse_relaxation':{'success_rate':base[0],'mean_final_distance':base[1],'mean_min_distance':base[2], 'finite_trajectories':base[3]},
108 'idea_action_optimized':{'success_rate':idea[0],'mean_final_distance':idea[1],'mean_min_distance':idea[2], 'finite_trajectories':idea[3]}}
109 with open('results.json','w') as f: json.dump(result,f,indent=2)
110 np.savetxt('optimized_path.csv', path.cpu().numpy(), delimiter=',', header='x,y', comments='')
111 print(json.dumps(result, indent=2))
112
113if __name__ == '__main__': main()