Exact Neural de Rham Backbone / exact_derham_experiment.py
Mechanism confirmed, baseline not beaten
1import json, math, random
2import numpy as np
3import torch
4from torch import nn
5
6SEED = 1286
7np.random.seed(SEED); random.seed(SEED); torch.manual_seed(SEED)
8torch.set_default_dtype(torch.float64)
9device = "cuda" if torch.cuda.is_available() else "cpu"
10
11
12def basis(d, p):
13 from itertools import combinations
14 return list(combinations(range(d), p))
15
16
17def wedge_matrix(w, d, p):
18 B, T = basis(d, p), basis(d, p + 1)
19 K = np.zeros((len(T), len(B)))
20 for j, a in enumerate(B):
21 for r in range(d):
22 if r in a:
23 continue
24 seq = (r,) + a
25 inversions = sum(seq[u] > seq[v] for u in range(len(seq)) for v in range(u + 1, len(seq)))
26 K[T.index(tuple(sorted(seq))), j] = ((-1) ** inversions) * w[r]
27 return K
28
29
30def relu_power(s, m):
31 return torch.relu(s) ** m / math.factorial(m)
32
33
34class ExactBackbone(nn.Module):
35 def __init__(self, w, b, k=2):
36 super().__init__()
37 self.register_buffer("w", torch.as_tensor(w))
38 self.register_buffer("b", torch.as_tensor(b))
39 self.k = k
40
41 def features(self, x, m):
42 return relu_power(x @ self.w.T + self.b, m)
43
44 def scalar(self, x, coef):
45 return self.features(x, self.k) @ coef
46
47 def gradient_form(self, x, coef):
48 # d(sum_i coef_i sigma_k(s_i)) = sum_i coef_i sigma_{k-1}(s_i) ds_i
49 return self.features(x, self.k - 1) @ (coef[:, None] * self.w)
50
51
52def math_checks():
53 rng = np.random.default_rng(SEED)
54 d, n = 3, 7
55 w = rng.normal(size=(n, d)); w /= np.linalg.norm(w, axis=1, keepdims=True)
56 max_nil = 0.0
57 for wi in w:
58 for p in range(d - 1):
59 max_nil = max(max_nil, np.max(np.abs(wedge_matrix(wi, d, p + 1) @ wedge_matrix(wi, d, p))))
60 # Finite-difference verification of d sigma_m(s)=sigma_{m-1}(s) ds away from kink.
61 x = torch.tensor([[0.37, -0.21, 0.19]], requires_grad=True)
62 wt = torch.tensor(w[:1]); bt = torch.tensor([0.41]); s = x @ wt.T + bt
63 m = 3
64 y = relu_power(s, m).sum(); grad = torch.autograd.grad(y, x)[0].detach().numpy()[0]
65 rhs = (relu_power(s.detach(), m - 1).numpy()[0, 0] * w[0])
66 identity_err = float(np.max(np.abs(grad - rhs)))
67 # Predicted operator scaling: ||K_0|| = ||w||, and composition remains zero.
68 scales = np.array([0.25, 0.5, 1., 2., 4.])
69 norms = []
70 nil_scaled = []
71 for a in scales:
72 wi = a * w[0]
73 k0, k1 = wedge_matrix(wi, d, 0), wedge_matrix(wi, d, 1)
74 norms.append(np.linalg.norm(k0, 2)); nil_scaled.append(np.max(np.abs(k1 @ k0)))
75 slope = float(np.polyfit(np.log(scales), np.log(norms), 1)[0])
76 return {"nilpotence_max_abs": float(max_nil), "derivative_identity_max_abs": identity_err,
77 "scale_sweep": [{"scale": float(a), "K0_norm": float(v), "K1K0_max": float(z)} for a,v,z in zip(scales,norms,nil_scaled)],
78 "loglog_scaling_exponent_observed": slope, "predicted_scaling_exponent": 1.0}
79
80
81def make_data(n=512):
82 x = torch.rand(n, 2, device=device)
83 # exact potential; target vector field is curl-free
84 phi = torch.sin(math.pi*x[:,0]) * torch.sin(math.pi*x[:,1])
85 target = torch.stack((math.pi*torch.cos(math.pi*x[:,0])*torch.sin(math.pi*x[:,1]),
86 math.pi*torch.sin(math.pi*x[:,0])*torch.cos(math.pi*x[:,1])), 1)
87 return x, target
88
89
90def train_compare(steps=700):
91 rng = np.random.default_rng(SEED)
92 nfeat = 28
93 w = rng.normal(size=(nfeat,2)); w /= np.linalg.norm(w,axis=1,keepdims=True)
94 b = rng.uniform(-0.7,0.7,size=nfeat)
95 x, target = make_data()
96 exact = ExactBackbone(w,b,k=2).to(device)
97 coef = nn.Parameter(torch.zeros(nfeat,device=device)); opt = torch.optim.Adam([coef],lr=0.04)
98 mlp = nn.Sequential(nn.Linear(2,24),nn.Tanh(),nn.Linear(24,24),nn.Tanh(),nn.Linear(24,2)).to(device)
99 opt2 = torch.optim.Adam(mlp.parameters(),lr=0.01)
100 for _ in range(steps):
101 opt.zero_grad(); pred=exact.gradient_form(x,coef); loss=((pred-target)**2).mean(); loss.backward(); opt.step()
102 opt2.zero_grad(); pred2=mlp(x); loss2=((pred2-target)**2).mean(); loss2.backward(); opt2.step()
103 with torch.no_grad():
104 p=exact.gradient_form(x,coef); q=mlp(x)
105 exact_mse=float(((p-target)**2).mean()); mlp_mse=float(((q-target)**2).mean())
106 # Autodiff divergence/curl of vector outputs on a smaller probe batch.
107 z=x[:64].detach().requires_grad_(True)
108 def violations(model_kind):
109 if model_kind=="exact": out=exact.gradient_form(z,coef)
110 else: out=mlp(z)
111 grads=[]
112 for j in range(2): grads.append(torch.autograd.grad(out[:,j].sum(),z,retain_graph=True)[0])
113 g0,g1=grads
114 curl=g1[:,0]-g0[:,1]
115 return float(torch.sqrt(torch.mean(curl**2)))
116 return {"exact_params": int(nfeat), "mlp_params": sum(p.numel() for p in mlp.parameters()),
117 "exact_mse": exact_mse, "mlp_mse": mlp_mse,
118 "exact_curl_rms": violations("exact"), "mlp_curl_rms": violations("mlp")}
119
120
121if __name__ == "__main__":
122 try:
123 result={"device":device,"math":math_checks(),"comparison":train_compare()}
124 except Exception as e:
125 if device == "cuda":
126 device="cpu"; torch.cuda.empty_cache(); result={"device":device,"math":math_checks(),"comparison":train_compare()}
127 else:
128 raise
129 with open("results.json","w") as f: json.dump(result,f,indent=2)
130 print(json.dumps(result,indent=2))