Tropical support-restricted PINN / run_experiment.py
Beats tuned baseline
1import json, math, random
2from pathlib import Path
3import numpy as np
4import torch
5
6SEED = 1234
7K = 15
8STEPS = 1800
9LR = 0.03
10N_COL = 96
11DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
12
13def seed_all(seed):
14 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
15 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
16
17# Formal-series recurrence for u' = 1 + u^2, u(0)=0.
18# (n+1)c[n+1] = 1_{n=0} + sum_{i=0}^n c[i]c[n-i].
19def formal_support(K):
20 c = np.zeros(K + 1, dtype=np.float64)
21 for n in range(K):
22 rhs = (1.0 if n == 0 else 0.0) + sum(c[i] * c[n-i] for i in range(n + 1))
23 c[n + 1] = rhs / (n + 1)
24 return [i for i, x in enumerate(c) if abs(x) > 1e-14], c
25
26def polynomial_value(coef, t, exponents):
27 # Horner is unnecessary at this small K and this makes the exponent mask explicit.
28 powers = torch.stack([t ** int(i) for i in exponents], dim=1)
29 return (powers * coef.unsqueeze(0)).sum(dim=1)
30
31def train(exponents, seed):
32 seed_all(seed)
33 t = torch.linspace(0.0, 1.0, N_COL, device=DEVICE)
34 # Coefficients are the minimal coefficient-output layer described in the idea.
35 coef = torch.nn.Parameter(0.01 * torch.randn(len(exponents), device=DEVICE))
36 opt = torch.optim.Adam([coef], lr=LR)
37 history = []
38 ex = [int(i) for i in exponents]
39 for step in range(STEPS):
40 opt.zero_grad(set_to_none=True)
41 u = polynomial_value(coef, t, ex)
42 du = torch.zeros_like(t)
43 for j, i in enumerate(ex):
44 if i > 0:
45 du = du + coef[j] * i * t ** (i - 1)
46 residual = du - 1.0 - u * u
47 loss = torch.mean(residual ** 2) + 10.0 * coef[ex.index(0)] ** 2 if 0 in ex else torch.mean(residual ** 2)
48 loss.backward(); opt.step()
49 if step in (0, 99, 299, 599, 1199, STEPS - 1): history.append(float(loss.detach().cpu()))
50 with torch.no_grad():
51 te = torch.linspace(0.0, 1.0, 401, device=DEVICE)
52 ue = polynomial_value(coef, te, ex)
53 exact = torch.tan(te)
54 rel = torch.linalg.vector_norm(ue - exact) / torch.linalg.vector_norm(exact)
55 u = polynomial_value(coef, t, ex)
56 du = torch.zeros_like(t)
57 for j, i in enumerate(ex):
58 if i > 0: du += coef[j] * i * t ** (i - 1)
59 residual = du - 1.0 - u * u
60 return {"final_loss": float((torch.mean(residual**2)).cpu()),
61 "relative_error": float(rel.cpu()),
62 "coeff_l2": float(torch.linalg.vector_norm(coef).cpu()),
63 "history": history, "parameters": len(ex)}
64
65def main():
66 global DEVICE
67 try:
68 seed_all(SEED)
69 _ = torch.zeros(1, device=DEVICE)
70 except Exception:
71 DEVICE = "cpu"
72 seed_all(SEED)
73 support, coeffs = formal_support(K)
74 # Independent direct check: all even coefficients are forced to zero by recurrence.
75 even_abs = max(abs(coeffs[i]) for i in range(0, K + 1, 2))
76 odd_nonzero = [i for i in range(1, K + 1, 2) if abs(coeffs[i]) > 1e-14]
77 results = {"device": DEVICE, "K": K, "collocation": N_COL, "steps": STEPS,
78 "formal_support": support, "expected_odd_support": odd_nonzero,
79 "max_forced_even_coefficient": even_abs, "math_check_pass": bool(even_abs < 1e-14)}
80 runs = {"dense": [], "restricted": []}
81 for seed in (11, 22, 33):
82 for name, ex in (("dense", list(range(K + 1))), ("restricted", support)):
83 try:
84 runs[name].append(train(ex, seed))
85 except Exception as e:
86 if DEVICE != "cpu":
87 DEVICE = "cpu"
88 runs[name].append(train(ex, seed))
89 else:
90 raise
91 results["runs"] = runs
92 for name in runs:
93 for metric in ("final_loss", "relative_error", "parameters"):
94 vals = [x[metric] for x in runs[name]]
95 results.setdefault(name, {})[metric + "_mean"] = float(np.mean(vals))
96 results[name][metric + "_std"] = float(np.std(vals))
97 Path("results.json").write_text(json.dumps(results, indent=2))
98 print(json.dumps(results, indent=2))
99
100if __name__ == "__main__": main()