betterwithage commited on
Commit
ef31bc3
·
verified ·
1 Parent(s): e5a851f

chore(sync): mirror backend .py + Dockerfile to Space (hf-sync-backend)

Browse files

Automated backend sync from szl-holdings/a11oy main via hf-sync-backend.
Updated (differed from the Space): Dockerfile, benchmarks/pinn/run_bench.py, serve.py, szl_pinn_nonlinear.py
Deleted (gone from the repo + Dockerfile COPY set): (none)

Keeps the Space-built backend (serve.py + the Dockerfile-COPY'd .py
modules) identical to GitHub main so the Space never rebuilds from a
stale backend, new endpoints don't 404 there, and orphaned modules
removed from the repo don't linger in the Space tree.

Files changed (4) hide show
  1. Dockerfile +13 -0
  2. benchmarks/pinn/run_bench.py +523 -0
  3. serve.py +27 -0
  4. szl_pinn_nonlinear.py +363 -0
Dockerfile CHANGED
@@ -576,6 +576,19 @@ COPY szl_wallpa.py ./szl_wallpa.py
576
  # CONSUMES szl_restraint (R1) + szl_energy_sovereign (Forge) only; edits neither.
577
  # 0 runtime CDN (fonts only); 0 visible codenames; Ponytail CITED (MIT).
578
  COPY benchmarks/restraint/run_bench.py ./benchmarks/restraint/run_bench.py
 
 
 
 
 
 
 
 
 
 
 
 
 
579
  # ADDITIVE (Lane F1, 2026-06-14): the 3D/holographic SUBSTRATE demo page, served at
580
  # /holo + /a11oy/holo via _ptg_serve. Loads the shared kit /static/shared/szl_holo3d.js
581
  # (0 CDN). image-only like the other web/*.html demo pages (declared in
 
576
  # CONSUMES szl_restraint (R1) + szl_energy_sovereign (Forge) only; edits neither.
577
  # 0 runtime CDN (fonts only); 0 visible codenames; Ponytail CITED (MIT).
578
  COPY benchmarks/restraint/run_bench.py ./benchmarks/restraint/run_bench.py
579
+ # ADDITIVE (nonlinear-PINN frontier, 2026-07-02): szl_pinn_nonlinear.py is imported by
580
+ # serve.py (try/except guarded) and serves GET /api/a11oy/v1/pinn/burgers (MODELED
581
+ # Newton-linearized spectral collocation for steady nonlinear Burgers) + /pinn/bench
582
+ # (serves the committed honest cross-framework benchmark). Per-file COPY (this Dockerfile
583
+ # never uses `COPY . .`) or the guarded import falls back (merged-but-not-live) and both
584
+ # endpoints 404 to the SPA. The bench artifact benchmarks/pinn/results.json MUST ship too
585
+ # or /pinn/bench honestly degrades to NOT-RUN; benchmarks/pinn/run_bench.py is the runnable
586
+ # reproduce tool (SZL arm is NumPy-only; the DeepXDE comparison arm is a benchmark-ONLY dev
587
+ # dep — LGPL-2.1, lazy-imported in the harness, NEVER imported by serve.py/shipped code).
588
+ # Mirrors the restraint bench pattern above.
589
+ COPY szl_pinn_nonlinear.py ./szl_pinn_nonlinear.py
590
+ COPY benchmarks/pinn/results.json ./benchmarks/pinn/results.json
591
+ COPY benchmarks/pinn/run_bench.py ./benchmarks/pinn/run_bench.py
592
  # ADDITIVE (Lane F1, 2026-06-14): the 3D/holographic SUBSTRATE demo page, served at
593
  # /holo + /a11oy/holo via _ptg_serve. Loads the shared kit /static/shared/szl_holo3d.js
594
  # (0 CDN). image-only like the other web/*.html demo pages (declared in
benchmarks/pinn/run_bench.py ADDED
@@ -0,0 +1,523 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ # (c) 2026 Lutar, Stephen P. — SZL Holdings — ORCID 0009-0001-0110-4173 — Doctrine v11
4
+ """
5
+ run_bench.py — a11oy PINN cross-framework HONEST benchmark harness.
6
+
7
+ Compares three solver families on the SAME 1D problems with KNOWN ground truth:
8
+
9
+ * szl — SZL Governed spectral collocation (this repo, own code, NumPy-only).
10
+ Linear BVP via least-squares sine collocation; NONLINEAR BVP via a
11
+ Newton loop around the same LS solve (szl_pinn_nonlinear); inverse
12
+ parameter discovery via the governed inverse engine
13
+ (szl_governed_ipinn / szl_pinn_inverse).
14
+ * deepxde — DeepXDE neural PINN (github.com/lululxvi/deepxde). BENCHMARK-ONLY.
15
+ * modulus — NVIDIA Modulus, RENAMED PhysicsNeMo. NOT RUN here (needs a CUDA GPU;
16
+ this sandbox is CPU-only). Recorded as NOT-RUN with a reproduce spec.
17
+
18
+ LICENSING (why DeepXDE lives ONLY in this file):
19
+ DeepXDE is **LGPL-2.1**. It is used here as a *benchmark-only development
20
+ dependency*. It is NEVER imported by serve.py or by any shipped a11oy module; the
21
+ /pinn/bench endpoint only READS the committed results.json this harness writes.
22
+ DeepXDE imports are confined to the _deepxde_* functions below (lazy imports), so
23
+ `--arm szl` and `--assemble` run with NumPy alone.
24
+
25
+ HONESTY (Doctrine v11):
26
+ - Every number is MEASURED on THIS box (rel-L2 vs the exact closed form, wall time)
27
+ or clearly labelled NOT-RUN / NOT-MEASURED. No number is fabricated.
28
+ - Energy/joules are NOT-MEASURED: the sandbox has no power meter. We never print a
29
+ joule figure for any arm.
30
+ - DISCLOSURE: the Poisson exact solution is a finite sum of sine modes and therefore
31
+ lies INSIDE the SZL trial basis, so SZL reaches ~machine precision BY CONSTRUCTION.
32
+ This is flagged (solution_in_trial_basis) and is NOT a general-accuracy claim.
33
+ - SCOPE: this is a low-dimensional, smooth, CPU-only suite. It favors spectral
34
+ methods. The regimes where neural PINNs are designed to win (high dimension,
35
+ complex/irregular geometry, no known good basis) are NOT exercised here and are
36
+ reported as NOT-TESTED — not as a loss for the neural arm.
37
+
38
+ Reproduce (each DeepXDE arm fits the 120s per-call budget at 3 seeds on 2 CPUs):
39
+ python benchmarks/pinn/run_bench.py --arm szl
40
+ python benchmarks/pinn/run_bench.py --arm deepxde --problem poisson --seeds 3
41
+ python benchmarks/pinn/run_bench.py --arm deepxde --problem burgers --seeds 3
42
+ python benchmarks/pinn/run_bench.py --arm deepxde --problem duffing --seeds 3
43
+ python benchmarks/pinn/run_bench.py --assemble --out benchmarks/pinn/results.json
44
+ """
45
+ from __future__ import annotations
46
+
47
+ import argparse
48
+ import json
49
+ import os
50
+ import statistics
51
+ import sys
52
+ import time
53
+ from pathlib import Path
54
+ from typing import Any, Dict, List, Optional
55
+
56
+ HERE = Path(__file__).resolve()
57
+ REPO_ROOT = HERE.parents[2]
58
+ PARTIAL_DIR = HERE.parent / "_partial"
59
+ if str(REPO_ROOT) not in sys.path:
60
+ sys.path.insert(0, str(REPO_ROOT))
61
+
62
+ # Shared problem constants (must match szl_pinn_nonlinear canonical instances).
63
+ POISSON_MODES = {1: 1.0, 3: 0.5, 5: 0.2}
64
+ BURGERS_C, BURGERS_X0, BURGERS_NU = 1.0, 0.5, 0.05
65
+ DUFFING = dict(m=1.0, c=0.2, delta=1.0, alpha=1.0, F=0.5, omega=1.0, x0=0.0, v0=0.0)
66
+ DUFFING_T = (0.0, 12.0)
67
+ DUFFING_NDATA = 120
68
+ ALPHA_TRUTH = 1.0
69
+
70
+
71
+ def _now() -> str:
72
+ return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
73
+
74
+
75
+ def _stats(vals: List[float]) -> Dict[str, float]:
76
+ return {"median": float(statistics.median(vals)),
77
+ "min": float(min(vals)), "max": float(max(vals)),
78
+ "n": len(vals)}
79
+
80
+
81
+ # --------------------------------------------------------------------------- #
82
+ # SZL arms (own code, NumPy-only) — fast, run in a single call
83
+ # --------------------------------------------------------------------------- #
84
+ def run_szl() -> Dict[str, Any]:
85
+ import numpy as np
86
+ import szl_pinn_nonlinear as NL
87
+
88
+ # Poisson (linear) — solution IN basis (disclosed)
89
+ t0 = time.time()
90
+ ps = NL.solve_poisson_multimode(POISSON_MODES, N=8, M=64)
91
+ poisson = {"framework": "szl", "method": "spectral sine-collocation least-squares",
92
+ "rel_l2_vs_exact": ps["rel_l2_vs_exact"], "dof_sine_modes": ps["N"],
93
+ "wall_s": round(time.time() - t0, 4),
94
+ "solution_in_trial_basis": ps["solution_in_trial_basis"],
95
+ "label": "MEASURED", "energy": "NOT-MEASURED (no power meter in sandbox)"}
96
+
97
+ # Burgers (nonlinear) — solution NOT in basis (honest truncation error)
98
+ C, x0, nu = BURGERS_C, BURGERS_X0, BURGERS_NU
99
+ a = float(NL.exact_shock(0.0, C, x0, nu))
100
+ b = float(NL.exact_shock(1.0, C, x0, nu))
101
+ t0 = time.time()
102
+ bs = NL.solve_steady_burgers(nu=nu, a=a, b=b, N=48, M=240)
103
+ rel = NL.rel_l2_vs_exact(bs, C, x0, nu)
104
+ burgers = {"framework": "szl",
105
+ "method": "Newton-linearized spectral collocation (frontier, own code)",
106
+ "rel_l2_vs_exact": rel, "dof_sine_modes": bs["N"],
107
+ "newton_iterations": bs["newton_iterations"],
108
+ "wall_s": round(time.time() - t0, 4),
109
+ "solution_in_trial_basis": False,
110
+ "label": "MEASURED", "energy": "NOT-MEASURED (no power meter in sandbox)"}
111
+
112
+ # Inverse Duffing — governed inverse discovery of alpha (truth 1.0)
113
+ import szl_governed_ipinn as GI
114
+ t0 = time.time()
115
+ out = GI.governed_discover({"demo": "duffing"})
116
+ disc = out["receipt"]["dsse"]
117
+ import base64
118
+ payload = json.loads(base64.b64decode(disc["payload"]).decode())
119
+ alpha_row = next(d for d in payload["discovered"] if d["name"] == "alpha")
120
+ duffing = {"framework": "szl",
121
+ "method": "governed inverse (Fisher-gated LS + physics-residual GD)",
122
+ "alpha_estimate": alpha_row["value"], "alpha_truth": ALPHA_TRUTH,
123
+ "abs_err": abs(alpha_row["value"] - ALPHA_TRUTH),
124
+ "ci95": alpha_row.get("ci95"), "fisher_information": alpha_row.get("fisher_information"),
125
+ "convergence_label": alpha_row.get("convergence_label"),
126
+ "identifiable": alpha_row.get("identifiable"),
127
+ "wall_s": round(time.time() - t0, 4),
128
+ "label": "MEASURED (fit error vs synthetic ground truth; not measured physics)"}
129
+
130
+ return {"framework_versions": {"numpy": np.__version__, "python": sys.version.split()[0]},
131
+ "poisson": poisson, "burgers": burgers, "duffing": duffing, "ran_at": _now()}
132
+
133
+
134
+ # --------------------------------------------------------------------------- #
135
+ # DeepXDE arms (LGPL, benchmark-only, lazy-imported here ONLY)
136
+ # --------------------------------------------------------------------------- #
137
+ def _deepxde_setup():
138
+ os.environ.setdefault("DDE_BACKEND", "pytorch")
139
+ import numpy as np # noqa
140
+ import torch
141
+ torch.set_num_threads(2)
142
+ import deepxde as dde
143
+ dde.optimizers.set_LBFGS_options(maxiter=2000)
144
+ return dde, torch, np
145
+
146
+
147
+ def _deepxde_poisson_seed(seed: int) -> Dict[str, Any]:
148
+ dde, torch, np = _deepxde_setup()
149
+ PI = np.pi
150
+ dde.config.set_random_seed(seed)
151
+ ue = lambda x: sum(c * np.sin(k * PI * x) for k, c in POISSON_MODES.items())
152
+ ft = lambda x: sum(c * (k * PI) ** 2 * torch.sin(k * PI * x) for k, c in POISSON_MODES.items())
153
+ pde = lambda x, y: -dde.grad.hessian(y, x) - ft(x)
154
+ g = dde.geometry.Interval(0, 1)
155
+ bc = dde.icbc.DirichletBC(g, lambda x: 0.0, lambda x, on: on)
156
+ data = dde.data.PDE(g, pde, bc, num_domain=64, num_boundary=2, solution=ue, num_test=200)
157
+ net = dde.nn.FNN([1] + [32] * 3 + [1], "tanh", "Glorot uniform")
158
+ m = dde.Model(data, net)
159
+ t0 = time.time()
160
+ m.compile("adam", lr=1e-3)
161
+ m.train(iterations=8000, display_every=100000)
162
+ m.compile("L-BFGS")
163
+ m.train(display_every=100000)
164
+ wall = time.time() - t0
165
+ x = g.uniform_points(400, True)
166
+ yp = m.predict(x).ravel()
167
+ yeq = ue(x).ravel()
168
+ rel = float(np.sqrt(np.sum((yp - yeq) ** 2) / np.sum(yeq ** 2)))
169
+ return {"seed": seed, "rel_l2_vs_exact": rel, "wall_s": round(wall, 2),
170
+ "trainable_params": int(sum(p.numel() for p in net.parameters()))}
171
+
172
+
173
+ def _deepxde_burgers_seed(seed: int) -> Dict[str, Any]:
174
+ dde, torch, np = _deepxde_setup()
175
+ C, x0, nu = BURGERS_C, BURGERS_X0, BURGERS_NU
176
+ dde.config.set_random_seed(seed)
177
+ exact = lambda x: -C * np.tanh(C * (x - x0) / (2.0 * nu))
178
+
179
+ def pde(x, y):
180
+ dy = dde.grad.jacobian(y, x)
181
+ d2y = dde.grad.hessian(y, x)
182
+ return nu * d2y - y * dy
183
+
184
+ g = dde.geometry.Interval(0, 1)
185
+ bc = dde.icbc.DirichletBC(g, lambda x: exact(x), lambda x, on: on)
186
+ # standard, adequately-resourced FNN PINN (not shock-adapted): denser collocation
187
+ # + firmer BC weighting so the comparison is fair, not a strawman.
188
+ data = dde.data.PDE(g, pde, bc, num_domain=200, num_boundary=2, solution=exact, num_test=400)
189
+ net = dde.nn.FNN([1] + [40] * 3 + [1], "tanh", "Glorot uniform")
190
+ m = dde.Model(data, net)
191
+ t0 = time.time()
192
+ m.compile("adam", lr=1e-3, loss_weights=[1.0, 100.0])
193
+ m.train(iterations=8000, display_every=100000)
194
+ m.compile("L-BFGS", loss_weights=[1.0, 100.0])
195
+ m.train(display_every=100000)
196
+ wall = time.time() - t0
197
+ x = g.uniform_points(400, True)
198
+ yp = m.predict(x).ravel()
199
+ yeq = exact(x).ravel()
200
+ rel = float(np.sqrt(np.sum((yp - yeq) ** 2) / np.sum(yeq ** 2)))
201
+ return {"seed": seed, "rel_l2_vs_exact": rel, "wall_s": round(wall, 2),
202
+ "trainable_params": int(sum(p.numel() for p in net.parameters()))}
203
+
204
+
205
+ def _deepxde_duffing_seed(seed: int) -> Dict[str, Any]:
206
+ dde, torch, np = _deepxde_setup()
207
+ import szl_pinn_inverse as PIV
208
+ dde.config.set_random_seed(seed)
209
+ t_lo, t_hi = DUFFING_T
210
+ m_, c_, delta_, F_, omega_ = (DUFFING["m"], DUFFING["c"], DUFFING["delta"],
211
+ DUFFING["F"], DUFFING["omega"])
212
+ # SAME synthetic data both arms see: integrate the true Duffing with alpha=1.0
213
+ t_obs = np.linspace(t_lo, t_hi, DUFFING_NDATA)
214
+ x_obs = np.asarray(PIV.integrate_duffing(t_obs, **DUFFING)).reshape(-1, 1)
215
+ t_col = t_obs.reshape(-1, 1)
216
+
217
+ alpha = dde.Variable(2.0) # deliberately wrong init; must discover ~1.0
218
+
219
+ def ode(t, x):
220
+ dx = dde.grad.jacobian(x, t)
221
+ ddx = dde.grad.hessian(x, t)
222
+ return m_ * ddx + c_ * dx + delta_ * x + alpha * x ** 3 - F_ * torch.cos(omega_ * t)
223
+
224
+ geom = dde.geometry.TimeDomain(t_lo, t_hi)
225
+ obs = dde.icbc.PointSetBC(t_col, x_obs, component=0)
226
+ data = dde.data.PDE(geom, ode, [obs], num_domain=200, num_boundary=2,
227
+ anchors=t_col)
228
+ net = dde.nn.FNN([1] + [40] * 3 + [1], "tanh", "Glorot uniform")
229
+ m = dde.Model(data, net)
230
+ t0 = time.time()
231
+ m.compile("adam", lr=1e-3, external_trainable_variables=[alpha])
232
+ m.train(iterations=10000, display_every=100000)
233
+ # bound L-BFGS: with 2nd-order (hessian) residuals the default maxiter=15000
234
+ # runs far past convergence and blows any wall budget; 3000 is ample here.
235
+ dde.optimizers.config.set_LBFGS_options(maxiter=3000)
236
+ m.compile("L-BFGS", external_trainable_variables=[alpha])
237
+ m.train(display_every=100000)
238
+ wall = time.time() - t0
239
+ a_hat = float(alpha.detach().cpu().numpy()) if hasattr(alpha, "detach") else float(alpha)
240
+ return {"seed": seed, "alpha_estimate": a_hat, "alpha_truth": ALPHA_TRUTH,
241
+ "abs_err": abs(a_hat - ALPHA_TRUTH), "wall_s": round(wall, 2),
242
+ "trainable_params": int(sum(p.numel() for p in net.parameters()))}
243
+
244
+
245
+ _DEEPXDE_RUNNERS = {"poisson": _deepxde_poisson_seed,
246
+ "burgers": _deepxde_burgers_seed,
247
+ "duffing": _deepxde_duffing_seed}
248
+
249
+
250
+ def run_deepxde(problem: str, seeds: int) -> Dict[str, Any]:
251
+ """Resumable: writes the partial after EACH seed and skips seeds already done,
252
+ so a per-call timeout on a slow CPU never loses completed work — just re-run."""
253
+ runner = _DEEPXDE_RUNNERS[problem]
254
+ import deepxde as dde
255
+ part_path = PARTIAL_DIR / ("deepxde_%s.json" % problem)
256
+ existing = _load_partial("deepxde_%s.json" % problem) or {}
257
+ rows: List[Dict[str, Any]] = existing.get("seeds", [])
258
+ done = {r["seed"] for r in rows}
259
+ versions = {}
260
+ try:
261
+ import torch
262
+ versions = {"deepxde": dde.__version__, "torch": torch.__version__, "backend": "pytorch"}
263
+ except Exception:
264
+ versions = {"deepxde": getattr(dde, "__version__", "?")}
265
+
266
+ def _pack() -> Dict[str, Any]:
267
+ return {"framework": "deepxde", "problem": problem,
268
+ "seeds": sorted(rows, key=lambda r: r["seed"]),
269
+ "framework_versions": versions,
270
+ "license": "LGPL-2.1 (benchmark-only dev dependency; NOT imported by shipped code)",
271
+ "method_class": "neural PINN (MLP minimizes PDE residual via Adam + L-BFGS)",
272
+ "energy": "NOT-MEASURED (no power meter in sandbox)", "ran_at": _now()}
273
+
274
+ for s in range(seeds):
275
+ if s in done:
276
+ continue
277
+ row = runner(s)
278
+ rows.append(row)
279
+ part_path.write_text(json.dumps(_pack(), indent=2))
280
+ print("[run_bench] deepxde %s seed %d done -> %s" % (problem, s, part_path), flush=True)
281
+ return _pack()
282
+
283
+
284
+ # --------------------------------------------------------------------------- #
285
+ # Assemble committed results.json from partials
286
+ # --------------------------------------------------------------------------- #
287
+ def _load_partial(name: str) -> Optional[Dict[str, Any]]:
288
+ fp = PARTIAL_DIR / name
289
+ if fp.is_file():
290
+ with fp.open("r", encoding="utf-8") as fh:
291
+ return json.load(fh)
292
+ return None
293
+
294
+
295
+ # Committed DeepXDE training config per problem. These mirror the _deepxde_*_seed
296
+ # functions above that produced the partials, recorded here so the assembled artifact
297
+ # is SELF-DESCRIBING and reproducible without re-reading this source. (Kept in lockstep
298
+ # with the seed functions; the architect verified they match the committed partials.)
299
+ _DEEPXDE_CONFIG = {
300
+ "poisson": {"net": "FNN [1,32,32,32,1] tanh (Glorot uniform)",
301
+ "optimizer": "Adam 8000 iters (lr=1e-3) + L-BFGS (maxiter=2000)",
302
+ "num_domain": 64, "num_boundary": 2, "num_test": 200, "loss_weights": None},
303
+ "burgers": {"net": "FNN [1,40,40,40,1] tanh (Glorot uniform)",
304
+ "optimizer": "Adam 8000 iters (lr=1e-3) + L-BFGS (maxiter=2000)",
305
+ "num_domain": 200, "num_boundary": 2, "num_test": 400, "loss_weights": [1.0, 100.0]},
306
+ "duffing": {"net": "FNN [1,40,40,40,1] tanh (Glorot uniform)",
307
+ "optimizer": "Adam 10000 iters (lr=1e-3) + L-BFGS (maxiter=3000)",
308
+ "num_domain": 200, "num_boundary": 2, "anchors": "120 observation points (PointSetBC)"},
309
+ }
310
+ # Honest caveats attached to specific neural arms so a strong SZL result is never
311
+ # read as a universal neural-PINN ceiling.
312
+ _DEEPXDE_CAVEAT = {
313
+ "burgers": ("STANDARD, non-shock-adapted PINN config: a plain FNN minimizing the PDE "
314
+ "residual with firm BC weighting. Shock-adaptation techniques (adaptive "
315
+ "resampling / RAR, curriculum in \u03bd, or hard-BC output transforms) would "
316
+ "very likely improve this arm and are NOT-TESTED here \u2014 so the large "
317
+ "burgers error reflects the vanilla config, NOT a ceiling for neural PINNs "
318
+ "on this PDE."),
319
+ }
320
+
321
+
322
+ def _dx_summary(part: Optional[Dict[str, Any]], metric: str, problem: str) -> Dict[str, Any]:
323
+ if not part:
324
+ return {"framework": "deepxde", "label": "NOT-RUN",
325
+ "reason": "no partial artifact; run the deepxde arm to populate"}
326
+ vals = [row[metric] for row in part["seeds"]]
327
+ walls = [row["wall_s"] for row in part["seeds"]]
328
+ out = {"framework": "deepxde", "method_class": part["method_class"],
329
+ "license": part["license"], "seeds_run": len(vals),
330
+ metric: _stats(vals), "wall_s": _stats(walls),
331
+ "trainable_params": part["seeds"][0].get("trainable_params"),
332
+ "config": _DEEPXDE_CONFIG.get(problem),
333
+ "framework_versions": part["framework_versions"],
334
+ "label": "MEASURED", "energy": part["energy"]}
335
+ if problem in _DEEPXDE_CAVEAT:
336
+ out["caveat"] = _DEEPXDE_CAVEAT[problem]
337
+ if metric == "abs_err":
338
+ # inverse-parameter fit: harmonize the label with the SZL arm — both arms fit
339
+ # the SAME synthetic data, so both are "MEASURED (fit error vs ground truth)".
340
+ out["label"] = "MEASURED (fit error vs synthetic ground truth; not measured physics)"
341
+ out["alpha_estimate_median"] = float(statistics.median(
342
+ [row["alpha_estimate"] for row in part["seeds"]]))
343
+ out["alpha_truth"] = ALPHA_TRUTH
344
+ return out
345
+
346
+
347
+ def _modulus_stub(problem: str) -> Dict[str, Any]:
348
+ return {"framework": "modulus_physicsnemo", "label": "NOT-RUN",
349
+ "reason": ("NVIDIA Modulus (renamed PhysicsNeMo) requires a CUDA GPU; this "
350
+ "benchmark box is CPU-only (0 GPUs)."),
351
+ "note": "NVIDIA Modulus was renamed to PhysicsNeMo — same framework lineage.",
352
+ "reproduce": ("on a CUDA GPU host: `pip install nvidia-physicsnemo`; port the "
353
+ f"'{problem}' 1D residual to a PhysicsNeMo PDE + constraint and "
354
+ "train; report rel-L2 vs the same exact closed form.")}
355
+
356
+
357
+ def assemble(out_path: str) -> Dict[str, Any]:
358
+ szl = _load_partial("szl.json")
359
+ dx_pois = _load_partial("deepxde_poisson.json")
360
+ dx_burg = _load_partial("deepxde_burgers.json")
361
+ dx_duff = _load_partial("deepxde_duffing.json")
362
+
363
+ problems = [
364
+ {
365
+ "id": "poisson_1d_multimode",
366
+ "pde": "-u''(x) = f(x) on [0,1], u(0)=u(1)=0",
367
+ "exact": "u*(x)=Σ c_k sin(kπx), modes {1:1.0, 3:0.5, 5:0.2}",
368
+ "disclosure": {
369
+ "solution_in_trial_basis": True,
370
+ "note": ("the exact solution is a finite sine sum, so it lies INSIDE the SZL "
371
+ "sine trial basis → SZL reaches ~machine precision BY CONSTRUCTION. "
372
+ "This is a property of the problem, NOT a general-accuracy claim."),
373
+ },
374
+ "metric": "rel_l2_vs_exact",
375
+ "arms": [szl["poisson"] if szl else {"framework": "szl", "label": "NOT-RUN"},
376
+ _dx_summary(dx_pois, "rel_l2_vs_exact", "poisson"),
377
+ _modulus_stub("poisson")],
378
+ },
379
+ {
380
+ "id": "steady_burgers_shock",
381
+ "pde": "ν u''(x) - u(x) u'(x) = 0 on [0,1], Dirichlet BCs from exact",
382
+ "exact": "u*(x)=-C·tanh(C(x-x0)/(2ν)), C=1, x0=0.5, ν=0.05",
383
+ "disclosure": {
384
+ "solution_in_trial_basis": False,
385
+ "note": ("the tanh shock is NOT a finite sine sum, so the SZL error is a "
386
+ "genuine spectral-truncation error — an honest head-to-head on a "
387
+ "NONLINEAR PDE (the frontier gap this build closes)."),
388
+ },
389
+ "metric": "rel_l2_vs_exact",
390
+ "arms": [szl["burgers"] if szl else {"framework": "szl", "label": "NOT-RUN"},
391
+ _dx_summary(dx_burg, "rel_l2_vs_exact", "burgers"),
392
+ _modulus_stub("burgers")],
393
+ },
394
+ {
395
+ "id": "inverse_duffing",
396
+ "pde": "m x'' + c x' + δ x + α x³ = F cos(ωt); DISCOVER α (truth 1.0)",
397
+ "exact": "synthetic data integrated from the true system (α=1.0); same data both arms",
398
+ "disclosure": {
399
+ "solution_in_trial_basis": None,
400
+ "note": ("inverse parameter-discovery problem: both arms see the SAME synthetic "
401
+ "x(t) data and must recover α from a deliberately wrong start."),
402
+ },
403
+ "metric": "abs_err",
404
+ "arms": [szl["duffing"] if szl else {"framework": "szl", "label": "NOT-RUN"},
405
+ _dx_summary(dx_duff, "abs_err", "duffing"),
406
+ _modulus_stub("duffing")],
407
+ },
408
+ ]
409
+
410
+ result = {
411
+ "service": "a11oy.pinn.bench",
412
+ "title": "SZL Governed spectral collocation vs DeepXDE (neural PINN) vs Modulus/PhysicsNeMo",
413
+ "overall_label": "MEASURED (SZL + DeepXDE on this CPU box); Modulus NOT-RUN",
414
+ "ran_at": _now(),
415
+ "hardware": {"cpus": 2, "ram_gib": 15, "gpu": None, "torch_threads": 2,
416
+ "note": "Replit sandbox — CPU-only, no CUDA GPU"},
417
+ "frameworks": {
418
+ "szl": {"method_class": ("classical spectral collocation least-squares (+ Newton "
419
+ "for nonlinear BVP) — NOT a neural PINN"),
420
+ "deps": ["python-stdlib", "numpy (BSD-3)"],
421
+ "license": "Apache-2.0", "shipped": True,
422
+ "versions": szl["framework_versions"] if szl else None},
423
+ "deepxde": {"method_class": "neural PINN (MLP minimizes PDE residual)",
424
+ "deps": ["pytorch"], "license": "LGPL-2.1",
425
+ "shipped": False,
426
+ "usage": ("benchmark-only dev dependency; NEVER imported by serve.py or "
427
+ "any shipped module. The /pinn/bench endpoint only reads this "
428
+ "committed artifact."),
429
+ "versions": (dx_pois or dx_burg or dx_duff or {}).get("framework_versions")},
430
+ "modulus_physicsnemo": {"method_class": "neural PINN (NVIDIA)",
431
+ "status": "NOT-RUN", "license": "Apache-2.0",
432
+ "note": "NVIDIA Modulus was renamed PhysicsNeMo (same framework)."},
433
+ },
434
+ "problems": problems,
435
+ "interpretation": {
436
+ "poisson": ("SZL is ~machine precision BY CONSTRUCTION (solution in basis, disclosed); "
437
+ "DeepXDE reaches a solid neural-PINN accuracy without knowing the basis."),
438
+ "burgers": ("honest nonlinear head-to-head: SZL's new Newton-spectral solver and the "
439
+ "neural PINN both target the exact tanh shock; compare rel-L2 and wall time. "
440
+ "The DeepXDE arm is a STANDARD, non-shock-adapted PINN \u2014 shock-adaptation "
441
+ "(RAR / curriculum / hard-BC) is NOT-TESTED and would likely narrow the gap."),
442
+ "duffing": ("both recover α from the same data; compare |α̂-1| and cost."),
443
+ },
444
+ "scope_limits": (
445
+ "This is a LOW-DIMENSIONAL (1D), SMOOTH, CPU-ONLY suite with KNOWN good bases. "
446
+ "It structurally favors spectral methods. The regimes neural PINNs are designed "
447
+ "for — high dimension (curse-of-dimensionality resistance), complex/irregular "
448
+ "geometry, and problems with NO known good basis — are NOT exercised here and are "
449
+ "reported as NOT-TESTED, not as a neural-arm loss. Do not read SZL wins on this "
450
+ "suite as universal superiority."),
451
+ "honesty": (
452
+ "All rel-L2 and wall-time numbers are MEASURED on this box against the exact closed "
453
+ "form; ≥3 seeds are reported as median[min,max] for the neural arm. No joules are "
454
+ "reported (NOT-MEASURED: no power meter). Poisson's in-basis advantage is disclosed. "
455
+ "DeepXDE (LGPL) is benchmark-only and never shipped. Modulus/PhysicsNeMo is NOT-RUN "
456
+ "with a reproduce spec (no GPU)."),
457
+ "doctrine": "Doctrine v11 LOCKED — no fabricated numbers; MEASURED/MODELED/NOT-RUN/NOT-MEASURED/NOT-TESTED labels only.",
458
+ "reproduce": {
459
+ "szl": "python benchmarks/pinn/run_bench.py --arm szl",
460
+ "deepxde": "python benchmarks/pinn/run_bench.py --arm deepxde --problem {poisson|burgers|duffing} --seeds 3",
461
+ "assemble": "python benchmarks/pinn/run_bench.py --assemble --out benchmarks/pinn/results.json",
462
+ "modulus": "requires a CUDA GPU host with nvidia-physicsnemo (see each problem's modulus arm)",
463
+ },
464
+ }
465
+ outp = Path(out_path)
466
+ outp.parent.mkdir(parents=True, exist_ok=True)
467
+ with outp.open("w", encoding="utf-8") as fh:
468
+ json.dump(result, fh, indent=2)
469
+ return result
470
+
471
+
472
+ # --------------------------------------------------------------------------- #
473
+ # CLI
474
+ # --------------------------------------------------------------------------- #
475
+ def main() -> int:
476
+ ap = argparse.ArgumentParser(description="a11oy PINN cross-framework honest benchmark harness.")
477
+ ap.add_argument("--arm", choices=["szl", "deepxde"], help="which arm to run")
478
+ ap.add_argument("--problem", choices=["poisson", "burgers", "duffing"],
479
+ help="problem for the deepxde arm")
480
+ ap.add_argument("--seeds", type=int, default=3, help="seeds for the deepxde arm (>=3 for honesty)")
481
+ ap.add_argument("--assemble", action="store_true", help="merge partials into results.json")
482
+ ap.add_argument("--out", default=str(HERE.parent / "results.json"), help="assembled artifact path")
483
+ args = ap.parse_args()
484
+
485
+ PARTIAL_DIR.mkdir(parents=True, exist_ok=True)
486
+
487
+ if args.arm == "szl":
488
+ res = run_szl()
489
+ (PARTIAL_DIR / "szl.json").write_text(json.dumps(res, indent=2))
490
+ print("[run_bench] SZL arm -> _partial/szl.json")
491
+ print(" poisson rel_l2=%.3e burgers rel_l2=%.3e (newton %d it) duffing |a-1|=%.4f (%s)"
492
+ % (res["poisson"]["rel_l2_vs_exact"], res["burgers"]["rel_l2_vs_exact"],
493
+ res["burgers"]["newton_iterations"], res["duffing"]["abs_err"],
494
+ res["duffing"]["convergence_label"]))
495
+ return 0
496
+
497
+ if args.arm == "deepxde":
498
+ if not args.problem:
499
+ print("[run_bench] --arm deepxde requires --problem", file=sys.stderr)
500
+ return 2
501
+ res = run_deepxde(args.problem, args.seeds)
502
+ (PARTIAL_DIR / ("deepxde_%s.json" % args.problem)).write_text(json.dumps(res, indent=2))
503
+ metric = "abs_err" if args.problem == "duffing" else "rel_l2_vs_exact"
504
+ vals = [r[metric] for r in res["seeds"]]
505
+ print("[run_bench] DeepXDE %s -> _partial/deepxde_%s.json %s median=%.3e [%.3e,%.3e] over %d seeds"
506
+ % (args.problem, args.problem, metric, statistics.median(vals),
507
+ min(vals), max(vals), len(vals)))
508
+ return 0
509
+
510
+ if args.assemble:
511
+ res = assemble(args.out)
512
+ print("[run_bench] assembled -> %s overall_label=%s" % (args.out, res["overall_label"]))
513
+ for p in res["problems"]:
514
+ labels = ", ".join("%s:%s" % (a["framework"], a["label"]) for a in p["arms"])
515
+ print(" %-22s [%s]" % (p["id"], labels))
516
+ return 0
517
+
518
+ ap.print_help()
519
+ return 1
520
+
521
+
522
+ if __name__ == "__main__":
523
+ raise SystemExit(main())
serve.py CHANGED
@@ -803,6 +803,33 @@ try:
803
  except Exception as _szl_ipinn_e: # pragma: no cover
804
  print(f"[a11oy] Governed Inverse-PINN NOT registered (a11oy continues): {_szl_ipinn_e!r}", file=__import__("sys").stderr)
805
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
806
  # ── Governed Materials-PROPERTY predictor (materials-property-prediction) — the
807
  # SECOND materials vertical: POST /api/a11oy/v1/materials/predict (+ GET
808
  # /materials/health, alias prefix /v1/materials). A NUMPY-ONLY calibrated SURROGATE
 
803
  except Exception as _szl_ipinn_e: # pragma: no cover
804
  print(f"[a11oy] Governed Inverse-PINN NOT registered (a11oy continues): {_szl_ipinn_e!r}", file=__import__("sys").stderr)
805
 
806
+ # ── Nonlinear-PINN frontier + honest cross-framework benchmark (pinn-nonlinear) —
807
+ # closes the frontier gap flagged in review: a Newton-linearized spectral-collocation
808
+ # solver for the NONLINEAR steady viscous Burgers shock (u u_x = nu u_xx), plus a
809
+ # read-only honest benchmark surface. Adds GET /api/a11oy/v1/pinn/burgers (MODELED
810
+ # nonlinear field; refuses nu<=0) and GET /api/a11oy/v1/pinn/bench (serves the
811
+ # COMMITTED benchmarks/pinn/results.json — SZL vs DeepXDE MEASURED, Modulus NOT-RUN;
812
+ # NEVER runs DeepXDE in the request path — DeepXDE is LGPL, benchmark-only). NumPy-only,
813
+ # Doctrine v11 labels (MEASURED/MODELED/NOT-RUN). Additive, try/except-guarded, then
814
+ # FRONT-MOVED to the router head (same ROUTE-ORDERING FIX as the PINN blocks above) so
815
+ # it wins ordered matching instead of 404'ing to the /api/a11oy/{path:path} proxy.
816
+ try:
817
+ import szl_pinn_nonlinear as _szl_pinn_nonlinear
818
+ _szl_pinn_nl_routes = _szl_pinn_nonlinear.register(app, ns="a11oy")
819
+ try:
820
+ _pinn_nl_paths = set(_szl_pinn_nl_routes)
821
+ _moved = [r for r in app.router.routes if getattr(r, "path", None) in _pinn_nl_paths]
822
+ for _r in _moved:
823
+ app.router.routes.remove(_r)
824
+ for _r in reversed(_moved):
825
+ app.router.routes.insert(0, _r)
826
+ print(f"[a11oy] Nonlinear-PINN + benchmark routes front-moved to router head: {len(_moved)} routes", file=__import__("sys").stderr)
827
+ except Exception as _szl_pinn_nl_move_e: # pragma: no cover
828
+ print(f"[a11oy] Nonlinear-PINN front-move skipped (routes still registered): {_szl_pinn_nl_move_e!r}", file=__import__("sys").stderr)
829
+ print(f"[a11oy] Nonlinear-PINN + benchmark registered: GET /api/a11oy/v1/pinn/burgers + /pinn/bench {_szl_pinn_nl_routes}", file=__import__("sys").stderr)
830
+ except Exception as _szl_pinn_nl_e: # pragma: no cover
831
+ print(f"[a11oy] Nonlinear-PINN + benchmark NOT registered (a11oy continues): {_szl_pinn_nl_e!r}", file=__import__("sys").stderr)
832
+
833
  # ── Governed Materials-PROPERTY predictor (materials-property-prediction) — the
834
  # SECOND materials vertical: POST /api/a11oy/v1/materials/predict (+ GET
835
  # /materials/health, alias prefix /v1/materials). A NUMPY-ONLY calibrated SURROGATE
szl_pinn_nonlinear.py ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ # (c) 2026 Lutar, Stephen P. — SZL Holdings — ORCID 0009-0001-0110-4173
4
+ #
5
+ # szl_pinn_nonlinear.py — SZL Governed NONLINEAR spectral-collocation solver +
6
+ # honest cross-framework benchmark surface (a11oy frontier).
7
+ #
8
+ # Closes the "linear-only" gap in the SZL PINN/bounds mesh: the existing spectral
9
+ # solver (run_measured_pinn.py) handles the LINEAR Poisson BVP -u''=f; this module
10
+ # adds a NONLINEAR BVP solver via a Newton loop around the SAME least-squares
11
+ # spectral collocation, validated on the STEADY VISCOUS BURGERS equation, which has
12
+ # an EXACT closed-form traveling-shock solution. It also carries the shipped Poisson
13
+ # solver used as the SZL arm of the cross-framework benchmark, and serves the
14
+ # committed benchmark artifact at /pinn/bench (read-only).
15
+ #
16
+ # Doctrine v11 LOCKED · Λ = Conjecture 1 (advisory only).
17
+ #
18
+ # OWN CODE / PERMISSIVE DEPS ONLY (NumPy BSD-3). No torch, no DeepXDE (LGPL),
19
+ # nothing proprietary is imported here. The benchmark HARNESS (benchmarks/pinn/
20
+ # run_bench.py) is the ONLY place DeepXDE is used, and it is a benchmark-only dev
21
+ # dependency — never imported by this module or serve.py. This endpoint only READS
22
+ # the committed results.json produced by that harness.
23
+ #
24
+ # THE NONLINEAR PDE (steady viscous Burgers, 1D BVP on [0,1]):
25
+ # ν u''(x) − u(x) u'(x) = 0 , u(0)=a, u(1)=b
26
+ # EXACT solution family (Cole-Hopf / Taylor viscous shock):
27
+ # u*(x) = −C · tanh( C (x − x0) / (2ν) )
28
+ # so a benchmark with a KNOWN ground truth is available: pick (C, x0, ν),
29
+ # set a=u*(0), b=u*(1), and the exact field is known by construction.
30
+ #
31
+ # METHOD (Newton-linearized spectral collocation):
32
+ # Trial u(x) = T(x) + Σ_{k=1..N} c_k sin(kπx), T(x)=a+(b−a)x (satisfies BCs;
33
+ # the sine modes vanish at x=0,1 so the Dirichlet data is exact for any c).
34
+ # Residual R = ν u'' − u u'. Newton step δ = Σ d_k sin(kπx) solves the LINEARISED
35
+ # system ν δ'' − (u δ' + δ u') = −R at M interior collocation points by the SAME
36
+ # least-squares solve used for the linear engine; update c += d until ‖R‖ → 0.
37
+ #
38
+ # HONEST LABELS: every returned field is MODELED (a numerical solution), never
39
+ # MEASURED. `rel_l2_vs_exact` is a REAL computed error against the closed form — it
40
+ # certifies the solver, it is not a physical measurement.
41
+
42
+ from __future__ import annotations
43
+
44
+ import json
45
+ import math
46
+ import os
47
+ from pathlib import Path
48
+ from typing import Dict, List, Optional
49
+
50
+ import numpy as np
51
+
52
+ try: # only needed when served under a Starlette/FastAPI app
53
+ from starlette.requests import Request
54
+ from starlette.responses import JSONResponse
55
+ except Exception: # pragma: no cover - keep this module standalone-importable
56
+ Request = object # type: ignore
57
+ JSONResponse = None # type: ignore
58
+
59
+ try: # reuse the shared doctrine strings for a consistent honesty envelope
60
+ from szl_pinn_bounds import DOCTRINE, LAMBDA_NOTE # type: ignore
61
+ except Exception: # pragma: no cover
62
+ DOCTRINE = ("No free energy. Every certified quantity is DERIVED from measured "
63
+ "inputs or a closed-form model; nothing is fabricated.")
64
+ LAMBDA_NOTE = ("Λ = Conjecture 1 — advisory only, NOT 'proven trust'.")
65
+
66
+ PI = math.pi
67
+
68
+ _RESULTS_PATHS = [
69
+ "benchmarks/pinn/results.json",
70
+ "/app/benchmarks/pinn/results.json",
71
+ os.path.join(os.path.dirname(os.path.abspath(__file__)), "benchmarks", "pinn", "results.json"),
72
+ ]
73
+
74
+
75
+ # --------------------------------------------------------------------------- #
76
+ # Sine design matrix shared by both solvers
77
+ # --------------------------------------------------------------------------- #
78
+ def _design(xs: np.ndarray, N: int):
79
+ """Return (S, Sp, Spp): the sine basis and its 1st/2nd derivatives at xs.
80
+ S[j,k]=sin(kπx_j), Sp=kπcos(kπx_j), Spp=−(kπ)²sin(kπx_j) (k=1..N)."""
81
+ k = np.arange(1, N + 1, dtype=float)
82
+ kp = k * PI
83
+ arg = np.outer(xs, kp)
84
+ S = np.sin(arg)
85
+ Sp = np.cos(arg) * kp
86
+ Spp = -S * (kp * kp)
87
+ return S, Sp, Spp
88
+
89
+
90
+ # --------------------------------------------------------------------------- #
91
+ # LINEAR arm — Poisson multimode (SZL's shipped spectral method; same math as
92
+ # run_measured_pinn.py). NOTE (honest disclosure): when the exact solution is a
93
+ # finite sum of sine modes it lies INSIDE this trial basis, so the solver hits
94
+ # near machine precision BY CONSTRUCTION. That is a property of the problem, not a
95
+ # general accuracy claim — the benchmark artifact flags solution_in_trial_basis.
96
+ # --------------------------------------------------------------------------- #
97
+ def exact_poisson(x, modes: Dict[int, float]):
98
+ x = np.asarray(x, dtype=float)
99
+ return sum(c * np.sin(k * PI * x) for k, c in modes.items())
100
+
101
+
102
+ def solve_poisson_multimode(modes: Dict[int, float], N: int = 8, M: int = 64) -> Dict:
103
+ """Solve -u''(x)=f(x) on [0,1], u(0)=u(1)=0 by sine-basis LS collocation,
104
+ where f = Σ c_k (kπ)² sin(kπx) for the given modes {k: c_k}."""
105
+ xs = np.array([(j + 1) / (M + 1) for j in range(M)], dtype=float)
106
+ S, _, Spp = _design(xs, N)
107
+ f = sum(c * (k * PI) ** 2 * np.sin(k * PI * xs) for k, c in modes.items())
108
+ A = -Spp # -u'' operator on the sine coefficients
109
+ c, *_ = np.linalg.lstsq(A, f, rcond=None)
110
+ xt = np.linspace(0.0, 1.0, 400)
111
+ St, _, _ = _design(xt, N)
112
+ u = St @ c
113
+ ue = exact_poisson(xt, modes)
114
+ rel = float(np.sqrt(np.sum((u - ue) ** 2) / np.sum(ue ** 2)))
115
+ max_mode = max(modes)
116
+ return {"coeffs": c, "N": N, "M": M, "modes": modes,
117
+ "rel_l2_vs_exact": rel, "solution_in_trial_basis": bool(N >= max_mode)}
118
+
119
+
120
+ # --------------------------------------------------------------------------- #
121
+ # Exact reference for the NONLINEAR arm (closed form ground truth)
122
+ # --------------------------------------------------------------------------- #
123
+ def exact_shock(x, C: float, x0: float, nu: float):
124
+ """u*(x) = −C·tanh(C(x−x0)/(2ν)) — exact steady viscous-Burgers shock."""
125
+ x = np.asarray(x, dtype=float)
126
+ return -C * np.tanh(C * (x - x0) / (2.0 * nu))
127
+
128
+
129
+ def exact_shock_deriv(x, C: float, x0: float, nu: float):
130
+ x = np.asarray(x, dtype=float)
131
+ th = np.tanh(C * (x - x0) / (2.0 * nu))
132
+ return -(C * C) / (2.0 * nu) * (1.0 - th * th)
133
+
134
+
135
+ # --------------------------------------------------------------------------- #
136
+ # NONLINEAR arm — Newton-linearized spectral-collocation solver (own code)
137
+ # --------------------------------------------------------------------------- #
138
+ def solve_steady_burgers(nu: float, a: float, b: float,
139
+ N: int = 48, M: int = 240,
140
+ newton_iters: int = 40, tol: float = 1e-12) -> Dict:
141
+ """Solve ν u'' − u u' = 0 on [0,1], u(0)=a, u(1)=b by Newton + spectral LS."""
142
+ if not (nu > 0):
143
+ raise ValueError("viscosity nu must be > 0")
144
+ xs = np.array([(j + 1) / (M + 1) for j in range(M)], dtype=float)
145
+ S, Sp, Spp = _design(xs, N)
146
+ slope = (b - a)
147
+ T = a + slope * xs
148
+ Tp = np.full_like(xs, slope)
149
+
150
+ c = np.zeros(N, dtype=float)
151
+ res_hist: List[float] = []
152
+ for _ in range(newton_iters):
153
+ u = T + S @ c
154
+ up = Tp + Sp @ c
155
+ upp = Spp @ c
156
+ R = nu * upp - u * up
157
+ res_hist.append(float(np.max(np.abs(R))))
158
+ A = nu * Spp - (u[:, None] * Sp + S * up[:, None])
159
+ d, *_ = np.linalg.lstsq(A, -R, rcond=None)
160
+ c = c + d
161
+ if float(np.max(np.abs(d))) < tol:
162
+ break
163
+ xt = np.linspace(0.02, 0.98, 400)
164
+ St, Spt, Sppt = _design(xt, N)
165
+ ut = a + slope * xt + St @ c
166
+ upt = slope + Spt @ c
167
+ uppt = Sppt @ c
168
+ Rt = nu * uppt - ut * upt
169
+ max_res = float(np.max(np.abs(Rt)))
170
+ rel_l2_res = float(np.sqrt(np.mean(Rt ** 2)) / (np.sqrt(np.mean(ut ** 2)) + 1e-30))
171
+ return {"coeffs": c, "N": N, "M": M, "nu": nu, "a": a, "b": b, "slope": slope,
172
+ "newton_residual_history": res_hist, "newton_iterations": len(res_hist),
173
+ "max_pde_residual_on_test": max_res, "rel_l2_pde_residual_on_test": rel_l2_res}
174
+
175
+
176
+ def eval_solution(sol: Dict, x) -> np.ndarray:
177
+ x = np.asarray(x, dtype=float)
178
+ S, _, _ = _design(x, sol["N"])
179
+ return sol["a"] + sol["slope"] * x + S @ sol["coeffs"]
180
+
181
+
182
+ def rel_l2_vs_exact(sol: Dict, C: float, x0: float, nu: float, npts: int = 400) -> float:
183
+ x = np.linspace(0.0, 1.0, npts)
184
+ u = eval_solution(sol, x)
185
+ ue = exact_shock(x, C, x0, nu)
186
+ return float(np.sqrt(np.sum((u - ue) ** 2) / np.sum(ue ** 2)))
187
+
188
+
189
+ # --------------------------------------------------------------------------- #
190
+ # Canonical benchmark instances (deterministic; used by endpoint + tests + bench)
191
+ # --------------------------------------------------------------------------- #
192
+ POISSON_MODES = {1: 1.0, 3: 0.5, 5: 0.2}
193
+ BURGERS_C, BURGERS_X0, BURGERS_NU = 1.0, 0.5, 0.05
194
+
195
+
196
+ def canonical_poisson() -> Dict:
197
+ sol = solve_poisson_multimode(POISSON_MODES, N=8, M=64)
198
+ return {"modes": POISSON_MODES, "sol": sol, "rel_l2_vs_exact": sol["rel_l2_vs_exact"]}
199
+
200
+
201
+ def canonical_instance() -> Dict:
202
+ """Fixed, reproducible steady viscous-Burgers shock; BCs set from exact."""
203
+ C, x0, nu = BURGERS_C, BURGERS_X0, BURGERS_NU
204
+ a = float(exact_shock(0.0, C, x0, nu))
205
+ b = float(exact_shock(1.0, C, x0, nu))
206
+ sol = solve_steady_burgers(nu=nu, a=a, b=b, N=48, M=240)
207
+ err = rel_l2_vs_exact(sol, C, x0, nu)
208
+ return {"C": C, "x0": x0, "nu": nu, "a": a, "b": b, "sol": sol, "rel_l2_vs_exact": err}
209
+
210
+
211
+ # --------------------------------------------------------------------------- #
212
+ # HTTP surface
213
+ # --------------------------------------------------------------------------- #
214
+ def _now_iso() -> str:
215
+ import time
216
+ return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
217
+
218
+
219
+ def _burgers_payload(nu: float, a: float, b: float, N: int,
220
+ ref: Optional[Dict] = None) -> Dict:
221
+ sol = solve_steady_burgers(nu=nu, a=a, b=b, N=N)
222
+ x = [i / 128.0 for i in range(129)]
223
+ u = eval_solution(sol, np.array(x)).tolist()
224
+ body = {
225
+ "service": "a11oy.pinn.nonlinear.burgers",
226
+ "status": "MODELED (numerical solution) — NOT MEASURED",
227
+ "modeled_not_measured": True,
228
+ "pde": "nu*u''(x) - u(x)*u'(x) = 0 on [0,1] (steady viscous Burgers)",
229
+ "method_class": ("Newton-linearized spectral collocation (own code, NumPy) — "
230
+ "a NONLINEAR BVP solver; NOT a neural PINN"),
231
+ "boundary_conditions": {"u(0)": a, "u(1)": b},
232
+ "viscosity_nu": nu, "dof_sine_modes": N,
233
+ "newton_iterations": sol["newton_iterations"],
234
+ "newton_residual_history": [round(v, 12) for v in sol["newton_residual_history"]],
235
+ "max_pde_residual_on_test": sol["max_pde_residual_on_test"],
236
+ "rel_l2_pde_residual_on_test": sol["rel_l2_pde_residual_on_test"],
237
+ "solution": {"x": x, "u": u},
238
+ "doctrine": DOCTRINE, "lambda_note": LAMBDA_NOTE,
239
+ "honesty": ("MODELED numerical field; no free-energy claim, no measured joule. "
240
+ "rel_l2_vs_exact (when a closed form exists) is a solver-verification "
241
+ "error, not a physical measurement."),
242
+ "ts": _now_iso(),
243
+ }
244
+ if ref is not None:
245
+ body["exact_reference"] = {
246
+ "form": "-C*tanh(C(x-x0)/(2*nu))", "C": ref["C"], "x0": ref["x0"], "nu": ref["nu"],
247
+ "rel_l2_vs_exact": rel_l2_vs_exact(sol, ref["C"], ref["x0"], ref["nu"]),
248
+ "solution_in_trial_basis": False,
249
+ "note": ("the tanh shock is NOT in the finite sine basis, so this error is a "
250
+ "genuine spectral-truncation error (not a by-construction win)"),
251
+ }
252
+ return body
253
+
254
+
255
+ def _h_burgers(req: Request):
256
+ """MODELED nonlinear steady-Burgers solution. Defaults to the canonical shock
257
+ instance (with exact-reference error); accepts ?nu=&a=&b=&N= for exploration."""
258
+ qp = getattr(req, "query_params", {}) or {}
259
+
260
+ def _q(name, default):
261
+ try:
262
+ return type(default)(qp.get(name)) if qp.get(name) not in (None, "") else default
263
+ except Exception:
264
+ return default
265
+
266
+ if not any(k in qp for k in ("nu", "a", "b")):
267
+ ci = canonical_instance()
268
+ ref = {"C": ci["C"], "x0": ci["x0"], "nu": ci["nu"]}
269
+ return JSONResponse(_burgers_payload(ci["nu"], ci["a"], ci["b"], N=48, ref=ref))
270
+ nu = _q("nu", BURGERS_NU)
271
+ a = _q("a", 0.9)
272
+ b = _q("b", -0.9)
273
+ N = int(_q("N", 48))
274
+ if not (nu > 0):
275
+ return JSONResponse({"error": "REFUSED", "reason": "viscosity nu must be > 0",
276
+ "honesty": "the solver refuses degenerate inputs; it never fabricates a field"},
277
+ status_code=400)
278
+ N = max(4, min(N, 128))
279
+ return JSONResponse(_burgers_payload(nu, a, b, N))
280
+
281
+
282
+ def _read_results() -> Optional[Dict]:
283
+ for p in _RESULTS_PATHS:
284
+ try:
285
+ fp = Path(p)
286
+ if fp.is_file():
287
+ with fp.open("r", encoding="utf-8") as fh:
288
+ return json.load(fh)
289
+ except Exception:
290
+ continue
291
+ return None
292
+
293
+
294
+ def _h_bench(req: Request):
295
+ """Serve the committed honest PINN cross-framework benchmark artifact.
296
+
297
+ Read-only: this endpoint NEVER runs DeepXDE/Modulus in the request path (DeepXDE
298
+ is LGPL and is a benchmark-only dev dependency). It serves benchmarks/pinn/
299
+ results.json produced by run_bench.py; if the artifact is absent it returns an
300
+ honest NOT-RUN roadmap with the exact reproduce command."""
301
+ data = _read_results()
302
+ if data is not None:
303
+ data.setdefault("served_at", _now_iso())
304
+ data.setdefault("source", "committed benchmarks/pinn/results.json (read-only)")
305
+ return JSONResponse(data)
306
+ return JSONResponse({
307
+ "service": "a11oy.pinn.bench",
308
+ "overall_label": "NOT-RUN",
309
+ "reason": "no committed benchmarks/pinn/results.json found in this deployment",
310
+ "reproduce": "python benchmarks/pinn/run_bench.py --assemble (see harness --help)",
311
+ "doctrine": DOCTRINE, "lambda_note": LAMBDA_NOTE,
312
+ "ts": _now_iso(),
313
+ })
314
+
315
+
316
+ def register(app, ns: str = "a11oy"):
317
+ """Wire the nonlinear-PINN + benchmark surface under /api/<ns>/v1/pinn/*.
318
+ Additive; mirrors szl_pinn_bounds.register()."""
319
+ base = f"/api/{ns}/v1/pinn"
320
+ handlers = [(f"{base}/burgers", _h_burgers), (f"{base}/bench", _h_bench)]
321
+ add_api_route = getattr(app, "add_api_route", None)
322
+ for path, fn in handlers:
323
+ if callable(add_api_route):
324
+ app.add_api_route(path, fn, methods=["GET"])
325
+ else:
326
+ from starlette.routing import Route
327
+ app.router.routes.append(Route(path, fn))
328
+ return [p for p, _ in handlers]
329
+
330
+
331
+ # --------------------------------------------------------------------------- #
332
+ # Self-test (no server): proves solver convergence + honesty
333
+ # --------------------------------------------------------------------------- #
334
+ def _selftest() -> Dict:
335
+ out: Dict[str, object] = {}
336
+ # Poisson (linear) — must hit the near-machine-precision floor (solution in basis)
337
+ cp = canonical_poisson()
338
+ out["poisson_solution_in_trial_basis"] = bool(cp["sol"]["solution_in_trial_basis"])
339
+ out["poisson_near_exact"] = bool(cp["rel_l2_vs_exact"] < 1e-9)
340
+ # Burgers (nonlinear) — honest spectral-truncation error vs exact tanh
341
+ ci = canonical_instance()
342
+ hist = ci["sol"]["newton_residual_history"]
343
+ out["newton_converged"] = bool(ci["sol"]["newton_iterations"] < 40
344
+ and hist[-1] < hist[0] * 0.05)
345
+ out["matches_exact_closed_form"] = bool(ci["rel_l2_vs_exact"] < 1e-5)
346
+ out["small_pde_residual"] = bool(ci["sol"]["rel_l2_pde_residual_on_test"] < 1e-3)
347
+ refused = False
348
+ try:
349
+ solve_steady_burgers(nu=0.0, a=1.0, b=-1.0)
350
+ except ValueError:
351
+ refused = True
352
+ out["refuses_degenerate_viscosity"] = refused
353
+ out["poisson_rel_l2_vs_exact"] = cp["rel_l2_vs_exact"]
354
+ out["burgers_rel_l2_vs_exact"] = ci["rel_l2_vs_exact"]
355
+ out["burgers_newton_iterations"] = ci["sol"]["newton_iterations"]
356
+ out["ok"] = all(out[k] is True for k in (
357
+ "poisson_solution_in_trial_basis", "poisson_near_exact", "newton_converged",
358
+ "matches_exact_closed_form", "small_pde_residual", "refuses_degenerate_viscosity"))
359
+ return out
360
+
361
+
362
+ if __name__ == "__main__":
363
+ print(json.dumps(_selftest(), indent=2))