File size: 26,853 Bytes
ef31bc3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
# (c) 2026 Lutar, Stephen P. — SZL Holdings — ORCID 0009-0001-0110-4173 — Doctrine v11
"""
run_bench.py — a11oy PINN cross-framework HONEST benchmark harness.

Compares three solver families on the SAME 1D problems with KNOWN ground truth:

  * szl      — SZL Governed spectral collocation (this repo, own code, NumPy-only).
               Linear BVP via least-squares sine collocation; NONLINEAR BVP via a
               Newton loop around the same LS solve (szl_pinn_nonlinear); inverse
               parameter discovery via the governed inverse engine
               (szl_governed_ipinn / szl_pinn_inverse).
  * deepxde  — DeepXDE neural PINN (github.com/lululxvi/deepxde). BENCHMARK-ONLY.
  * modulus  — NVIDIA Modulus, RENAMED PhysicsNeMo. NOT RUN here (needs a CUDA GPU;
               this sandbox is CPU-only). Recorded as NOT-RUN with a reproduce spec.

LICENSING (why DeepXDE lives ONLY in this file):
  DeepXDE is **LGPL-2.1**. It is used here as a *benchmark-only development
  dependency*. It is NEVER imported by serve.py or by any shipped a11oy module; the
  /pinn/bench endpoint only READS the committed results.json this harness writes.
  DeepXDE imports are confined to the _deepxde_* functions below (lazy imports), so
  `--arm szl` and `--assemble` run with NumPy alone.

HONESTY (Doctrine v11):
  - Every number is MEASURED on THIS box (rel-L2 vs the exact closed form, wall time)
    or clearly labelled NOT-RUN / NOT-MEASURED. No number is fabricated.
  - Energy/joules are NOT-MEASURED: the sandbox has no power meter. We never print a
    joule figure for any arm.
  - DISCLOSURE: the Poisson exact solution is a finite sum of sine modes and therefore
    lies INSIDE the SZL trial basis, so SZL reaches ~machine precision BY CONSTRUCTION.
    This is flagged (solution_in_trial_basis) and is NOT a general-accuracy claim.
  - SCOPE: this is a low-dimensional, smooth, CPU-only suite. It favors spectral
    methods. The regimes where neural PINNs are designed to win (high dimension,
    complex/irregular geometry, no known good basis) are NOT exercised here and are
    reported as NOT-TESTED — not as a loss for the neural arm.

Reproduce (each DeepXDE arm fits the 120s per-call budget at 3 seeds on 2 CPUs):
    python benchmarks/pinn/run_bench.py --arm szl
    python benchmarks/pinn/run_bench.py --arm deepxde --problem poisson --seeds 3
    python benchmarks/pinn/run_bench.py --arm deepxde --problem burgers --seeds 3
    python benchmarks/pinn/run_bench.py --arm deepxde --problem duffing --seeds 3
    python benchmarks/pinn/run_bench.py --assemble --out benchmarks/pinn/results.json
"""
from __future__ import annotations

import argparse
import json
import os
import statistics
import sys
import time
from pathlib import Path
from typing import Any, Dict, List, Optional

HERE = Path(__file__).resolve()
REPO_ROOT = HERE.parents[2]
PARTIAL_DIR = HERE.parent / "_partial"
if str(REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(REPO_ROOT))

# Shared problem constants (must match szl_pinn_nonlinear canonical instances).
POISSON_MODES = {1: 1.0, 3: 0.5, 5: 0.2}
BURGERS_C, BURGERS_X0, BURGERS_NU = 1.0, 0.5, 0.05
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)
DUFFING_T = (0.0, 12.0)
DUFFING_NDATA = 120
ALPHA_TRUTH = 1.0


def _now() -> str:
    return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())


def _stats(vals: List[float]) -> Dict[str, float]:
    return {"median": float(statistics.median(vals)),
            "min": float(min(vals)), "max": float(max(vals)),
            "n": len(vals)}


# --------------------------------------------------------------------------- #
# SZL arms (own code, NumPy-only) — fast, run in a single call
# --------------------------------------------------------------------------- #
def run_szl() -> Dict[str, Any]:
    import numpy as np
    import szl_pinn_nonlinear as NL

    # Poisson (linear) — solution IN basis (disclosed)
    t0 = time.time()
    ps = NL.solve_poisson_multimode(POISSON_MODES, N=8, M=64)
    poisson = {"framework": "szl", "method": "spectral sine-collocation least-squares",
               "rel_l2_vs_exact": ps["rel_l2_vs_exact"], "dof_sine_modes": ps["N"],
               "wall_s": round(time.time() - t0, 4),
               "solution_in_trial_basis": ps["solution_in_trial_basis"],
               "label": "MEASURED", "energy": "NOT-MEASURED (no power meter in sandbox)"}

    # Burgers (nonlinear) — solution NOT in basis (honest truncation error)
    C, x0, nu = BURGERS_C, BURGERS_X0, BURGERS_NU
    a = float(NL.exact_shock(0.0, C, x0, nu))
    b = float(NL.exact_shock(1.0, C, x0, nu))
    t0 = time.time()
    bs = NL.solve_steady_burgers(nu=nu, a=a, b=b, N=48, M=240)
    rel = NL.rel_l2_vs_exact(bs, C, x0, nu)
    burgers = {"framework": "szl",
               "method": "Newton-linearized spectral collocation (frontier, own code)",
               "rel_l2_vs_exact": rel, "dof_sine_modes": bs["N"],
               "newton_iterations": bs["newton_iterations"],
               "wall_s": round(time.time() - t0, 4),
               "solution_in_trial_basis": False,
               "label": "MEASURED", "energy": "NOT-MEASURED (no power meter in sandbox)"}

    # Inverse Duffing — governed inverse discovery of alpha (truth 1.0)
    import szl_governed_ipinn as GI
    t0 = time.time()
    out = GI.governed_discover({"demo": "duffing"})
    disc = out["receipt"]["dsse"]
    import base64
    payload = json.loads(base64.b64decode(disc["payload"]).decode())
    alpha_row = next(d for d in payload["discovered"] if d["name"] == "alpha")
    duffing = {"framework": "szl",
               "method": "governed inverse (Fisher-gated LS + physics-residual GD)",
               "alpha_estimate": alpha_row["value"], "alpha_truth": ALPHA_TRUTH,
               "abs_err": abs(alpha_row["value"] - ALPHA_TRUTH),
               "ci95": alpha_row.get("ci95"), "fisher_information": alpha_row.get("fisher_information"),
               "convergence_label": alpha_row.get("convergence_label"),
               "identifiable": alpha_row.get("identifiable"),
               "wall_s": round(time.time() - t0, 4),
               "label": "MEASURED (fit error vs synthetic ground truth; not measured physics)"}

    return {"framework_versions": {"numpy": np.__version__, "python": sys.version.split()[0]},
            "poisson": poisson, "burgers": burgers, "duffing": duffing, "ran_at": _now()}


# --------------------------------------------------------------------------- #
# DeepXDE arms (LGPL, benchmark-only, lazy-imported here ONLY)
# --------------------------------------------------------------------------- #
def _deepxde_setup():
    os.environ.setdefault("DDE_BACKEND", "pytorch")
    import numpy as np  # noqa
    import torch
    torch.set_num_threads(2)
    import deepxde as dde
    dde.optimizers.set_LBFGS_options(maxiter=2000)
    return dde, torch, np


def _deepxde_poisson_seed(seed: int) -> Dict[str, Any]:
    dde, torch, np = _deepxde_setup()
    PI = np.pi
    dde.config.set_random_seed(seed)
    ue = lambda x: sum(c * np.sin(k * PI * x) for k, c in POISSON_MODES.items())
    ft = lambda x: sum(c * (k * PI) ** 2 * torch.sin(k * PI * x) for k, c in POISSON_MODES.items())
    pde = lambda x, y: -dde.grad.hessian(y, x) - ft(x)
    g = dde.geometry.Interval(0, 1)
    bc = dde.icbc.DirichletBC(g, lambda x: 0.0, lambda x, on: on)
    data = dde.data.PDE(g, pde, bc, num_domain=64, num_boundary=2, solution=ue, num_test=200)
    net = dde.nn.FNN([1] + [32] * 3 + [1], "tanh", "Glorot uniform")
    m = dde.Model(data, net)
    t0 = time.time()
    m.compile("adam", lr=1e-3)
    m.train(iterations=8000, display_every=100000)
    m.compile("L-BFGS")
    m.train(display_every=100000)
    wall = time.time() - t0
    x = g.uniform_points(400, True)
    yp = m.predict(x).ravel()
    yeq = ue(x).ravel()
    rel = float(np.sqrt(np.sum((yp - yeq) ** 2) / np.sum(yeq ** 2)))
    return {"seed": seed, "rel_l2_vs_exact": rel, "wall_s": round(wall, 2),
            "trainable_params": int(sum(p.numel() for p in net.parameters()))}


def _deepxde_burgers_seed(seed: int) -> Dict[str, Any]:
    dde, torch, np = _deepxde_setup()
    C, x0, nu = BURGERS_C, BURGERS_X0, BURGERS_NU
    dde.config.set_random_seed(seed)
    exact = lambda x: -C * np.tanh(C * (x - x0) / (2.0 * nu))

    def pde(x, y):
        dy = dde.grad.jacobian(y, x)
        d2y = dde.grad.hessian(y, x)
        return nu * d2y - y * dy

    g = dde.geometry.Interval(0, 1)
    bc = dde.icbc.DirichletBC(g, lambda x: exact(x), lambda x, on: on)
    # standard, adequately-resourced FNN PINN (not shock-adapted): denser collocation
    # + firmer BC weighting so the comparison is fair, not a strawman.
    data = dde.data.PDE(g, pde, bc, num_domain=200, num_boundary=2, solution=exact, num_test=400)
    net = dde.nn.FNN([1] + [40] * 3 + [1], "tanh", "Glorot uniform")
    m = dde.Model(data, net)
    t0 = time.time()
    m.compile("adam", lr=1e-3, loss_weights=[1.0, 100.0])
    m.train(iterations=8000, display_every=100000)
    m.compile("L-BFGS", loss_weights=[1.0, 100.0])
    m.train(display_every=100000)
    wall = time.time() - t0
    x = g.uniform_points(400, True)
    yp = m.predict(x).ravel()
    yeq = exact(x).ravel()
    rel = float(np.sqrt(np.sum((yp - yeq) ** 2) / np.sum(yeq ** 2)))
    return {"seed": seed, "rel_l2_vs_exact": rel, "wall_s": round(wall, 2),
            "trainable_params": int(sum(p.numel() for p in net.parameters()))}


def _deepxde_duffing_seed(seed: int) -> Dict[str, Any]:
    dde, torch, np = _deepxde_setup()
    import szl_pinn_inverse as PIV
    dde.config.set_random_seed(seed)
    t_lo, t_hi = DUFFING_T
    m_, c_, delta_, F_, omega_ = (DUFFING["m"], DUFFING["c"], DUFFING["delta"],
                                  DUFFING["F"], DUFFING["omega"])
    # SAME synthetic data both arms see: integrate the true Duffing with alpha=1.0
    t_obs = np.linspace(t_lo, t_hi, DUFFING_NDATA)
    x_obs = np.asarray(PIV.integrate_duffing(t_obs, **DUFFING)).reshape(-1, 1)
    t_col = t_obs.reshape(-1, 1)

    alpha = dde.Variable(2.0)  # deliberately wrong init; must discover ~1.0

    def ode(t, x):
        dx = dde.grad.jacobian(x, t)
        ddx = dde.grad.hessian(x, t)
        return m_ * ddx + c_ * dx + delta_ * x + alpha * x ** 3 - F_ * torch.cos(omega_ * t)

    geom = dde.geometry.TimeDomain(t_lo, t_hi)
    obs = dde.icbc.PointSetBC(t_col, x_obs, component=0)
    data = dde.data.PDE(geom, ode, [obs], num_domain=200, num_boundary=2,
                        anchors=t_col)
    net = dde.nn.FNN([1] + [40] * 3 + [1], "tanh", "Glorot uniform")
    m = dde.Model(data, net)
    t0 = time.time()
    m.compile("adam", lr=1e-3, external_trainable_variables=[alpha])
    m.train(iterations=10000, display_every=100000)
    # bound L-BFGS: with 2nd-order (hessian) residuals the default maxiter=15000
    # runs far past convergence and blows any wall budget; 3000 is ample here.
    dde.optimizers.config.set_LBFGS_options(maxiter=3000)
    m.compile("L-BFGS", external_trainable_variables=[alpha])
    m.train(display_every=100000)
    wall = time.time() - t0
    a_hat = float(alpha.detach().cpu().numpy()) if hasattr(alpha, "detach") else float(alpha)
    return {"seed": seed, "alpha_estimate": a_hat, "alpha_truth": ALPHA_TRUTH,
            "abs_err": abs(a_hat - ALPHA_TRUTH), "wall_s": round(wall, 2),
            "trainable_params": int(sum(p.numel() for p in net.parameters()))}


_DEEPXDE_RUNNERS = {"poisson": _deepxde_poisson_seed,
                    "burgers": _deepxde_burgers_seed,
                    "duffing": _deepxde_duffing_seed}


def run_deepxde(problem: str, seeds: int) -> Dict[str, Any]:
    """Resumable: writes the partial after EACH seed and skips seeds already done,
    so a per-call timeout on a slow CPU never loses completed work — just re-run."""
    runner = _DEEPXDE_RUNNERS[problem]
    import deepxde as dde
    part_path = PARTIAL_DIR / ("deepxde_%s.json" % problem)
    existing = _load_partial("deepxde_%s.json" % problem) or {}
    rows: List[Dict[str, Any]] = existing.get("seeds", [])
    done = {r["seed"] for r in rows}
    versions = {}
    try:
        import torch
        versions = {"deepxde": dde.__version__, "torch": torch.__version__, "backend": "pytorch"}
    except Exception:
        versions = {"deepxde": getattr(dde, "__version__", "?")}

    def _pack() -> Dict[str, Any]:
        return {"framework": "deepxde", "problem": problem,
                "seeds": sorted(rows, key=lambda r: r["seed"]),
                "framework_versions": versions,
                "license": "LGPL-2.1 (benchmark-only dev dependency; NOT imported by shipped code)",
                "method_class": "neural PINN (MLP minimizes PDE residual via Adam + L-BFGS)",
                "energy": "NOT-MEASURED (no power meter in sandbox)", "ran_at": _now()}

    for s in range(seeds):
        if s in done:
            continue
        row = runner(s)
        rows.append(row)
        part_path.write_text(json.dumps(_pack(), indent=2))
        print("[run_bench] deepxde %s seed %d done -> %s" % (problem, s, part_path), flush=True)
    return _pack()


# --------------------------------------------------------------------------- #
# Assemble committed results.json from partials
# --------------------------------------------------------------------------- #
def _load_partial(name: str) -> Optional[Dict[str, Any]]:
    fp = PARTIAL_DIR / name
    if fp.is_file():
        with fp.open("r", encoding="utf-8") as fh:
            return json.load(fh)
    return None


# Committed DeepXDE training config per problem. These mirror the _deepxde_*_seed
# functions above that produced the partials, recorded here so the assembled artifact
# is SELF-DESCRIBING and reproducible without re-reading this source. (Kept in lockstep
# with the seed functions; the architect verified they match the committed partials.)
_DEEPXDE_CONFIG = {
    "poisson": {"net": "FNN [1,32,32,32,1] tanh (Glorot uniform)",
                "optimizer": "Adam 8000 iters (lr=1e-3) + L-BFGS (maxiter=2000)",
                "num_domain": 64, "num_boundary": 2, "num_test": 200, "loss_weights": None},
    "burgers": {"net": "FNN [1,40,40,40,1] tanh (Glorot uniform)",
                "optimizer": "Adam 8000 iters (lr=1e-3) + L-BFGS (maxiter=2000)",
                "num_domain": 200, "num_boundary": 2, "num_test": 400, "loss_weights": [1.0, 100.0]},
    "duffing": {"net": "FNN [1,40,40,40,1] tanh (Glorot uniform)",
                "optimizer": "Adam 10000 iters (lr=1e-3) + L-BFGS (maxiter=3000)",
                "num_domain": 200, "num_boundary": 2, "anchors": "120 observation points (PointSetBC)"},
}
# Honest caveats attached to specific neural arms so a strong SZL result is never
# read as a universal neural-PINN ceiling.
_DEEPXDE_CAVEAT = {
    "burgers": ("STANDARD, non-shock-adapted PINN config: a plain FNN minimizing the PDE "
                "residual with firm BC weighting. Shock-adaptation techniques (adaptive "
                "resampling / RAR, curriculum in \u03bd, or hard-BC output transforms) would "
                "very likely improve this arm and are NOT-TESTED here \u2014 so the large "
                "burgers error reflects the vanilla config, NOT a ceiling for neural PINNs "
                "on this PDE."),
}


def _dx_summary(part: Optional[Dict[str, Any]], metric: str, problem: str) -> Dict[str, Any]:
    if not part:
        return {"framework": "deepxde", "label": "NOT-RUN",
                "reason": "no partial artifact; run the deepxde arm to populate"}
    vals = [row[metric] for row in part["seeds"]]
    walls = [row["wall_s"] for row in part["seeds"]]
    out = {"framework": "deepxde", "method_class": part["method_class"],
           "license": part["license"], "seeds_run": len(vals),
           metric: _stats(vals), "wall_s": _stats(walls),
           "trainable_params": part["seeds"][0].get("trainable_params"),
           "config": _DEEPXDE_CONFIG.get(problem),
           "framework_versions": part["framework_versions"],
           "label": "MEASURED", "energy": part["energy"]}
    if problem in _DEEPXDE_CAVEAT:
        out["caveat"] = _DEEPXDE_CAVEAT[problem]
    if metric == "abs_err":
        # inverse-parameter fit: harmonize the label with the SZL arm — both arms fit
        # the SAME synthetic data, so both are "MEASURED (fit error vs ground truth)".
        out["label"] = "MEASURED (fit error vs synthetic ground truth; not measured physics)"
        out["alpha_estimate_median"] = float(statistics.median(
            [row["alpha_estimate"] for row in part["seeds"]]))
        out["alpha_truth"] = ALPHA_TRUTH
    return out


def _modulus_stub(problem: str) -> Dict[str, Any]:
    return {"framework": "modulus_physicsnemo", "label": "NOT-RUN",
            "reason": ("NVIDIA Modulus (renamed PhysicsNeMo) requires a CUDA GPU; this "
                       "benchmark box is CPU-only (0 GPUs)."),
            "note": "NVIDIA Modulus was renamed to PhysicsNeMo — same framework lineage.",
            "reproduce": ("on a CUDA GPU host: `pip install nvidia-physicsnemo`; port the "
                          f"'{problem}' 1D residual to a PhysicsNeMo PDE + constraint and "
                          "train; report rel-L2 vs the same exact closed form.")}


def assemble(out_path: str) -> Dict[str, Any]:
    szl = _load_partial("szl.json")
    dx_pois = _load_partial("deepxde_poisson.json")
    dx_burg = _load_partial("deepxde_burgers.json")
    dx_duff = _load_partial("deepxde_duffing.json")

    problems = [
        {
            "id": "poisson_1d_multimode",
            "pde": "-u''(x) = f(x) on [0,1], u(0)=u(1)=0",
            "exact": "u*(x)=Σ c_k sin(kπx), modes {1:1.0, 3:0.5, 5:0.2}",
            "disclosure": {
                "solution_in_trial_basis": True,
                "note": ("the exact solution is a finite sine sum, so it lies INSIDE the SZL "
                         "sine trial basis → SZL reaches ~machine precision BY CONSTRUCTION. "
                         "This is a property of the problem, NOT a general-accuracy claim."),
            },
            "metric": "rel_l2_vs_exact",
            "arms": [szl["poisson"] if szl else {"framework": "szl", "label": "NOT-RUN"},
                     _dx_summary(dx_pois, "rel_l2_vs_exact", "poisson"),
                     _modulus_stub("poisson")],
        },
        {
            "id": "steady_burgers_shock",
            "pde": "ν u''(x) - u(x) u'(x) = 0 on [0,1], Dirichlet BCs from exact",
            "exact": "u*(x)=-C·tanh(C(x-x0)/(2ν)), C=1, x0=0.5, ν=0.05",
            "disclosure": {
                "solution_in_trial_basis": False,
                "note": ("the tanh shock is NOT a finite sine sum, so the SZL error is a "
                         "genuine spectral-truncation error — an honest head-to-head on a "
                         "NONLINEAR PDE (the frontier gap this build closes)."),
            },
            "metric": "rel_l2_vs_exact",
            "arms": [szl["burgers"] if szl else {"framework": "szl", "label": "NOT-RUN"},
                     _dx_summary(dx_burg, "rel_l2_vs_exact", "burgers"),
                     _modulus_stub("burgers")],
        },
        {
            "id": "inverse_duffing",
            "pde": "m x'' + c x' + δ x + α x³ = F cos(ωt); DISCOVER α (truth 1.0)",
            "exact": "synthetic data integrated from the true system (α=1.0); same data both arms",
            "disclosure": {
                "solution_in_trial_basis": None,
                "note": ("inverse parameter-discovery problem: both arms see the SAME synthetic "
                         "x(t) data and must recover α from a deliberately wrong start."),
            },
            "metric": "abs_err",
            "arms": [szl["duffing"] if szl else {"framework": "szl", "label": "NOT-RUN"},
                     _dx_summary(dx_duff, "abs_err", "duffing"),
                     _modulus_stub("duffing")],
        },
    ]

    result = {
        "service": "a11oy.pinn.bench",
        "title": "SZL Governed spectral collocation vs DeepXDE (neural PINN) vs Modulus/PhysicsNeMo",
        "overall_label": "MEASURED (SZL + DeepXDE on this CPU box); Modulus NOT-RUN",
        "ran_at": _now(),
        "hardware": {"cpus": 2, "ram_gib": 15, "gpu": None, "torch_threads": 2,
                     "note": "Replit sandbox — CPU-only, no CUDA GPU"},
        "frameworks": {
            "szl": {"method_class": ("classical spectral collocation least-squares (+ Newton "
                                     "for nonlinear BVP) — NOT a neural PINN"),
                    "deps": ["python-stdlib", "numpy (BSD-3)"],
                    "license": "Apache-2.0", "shipped": True,
                    "versions": szl["framework_versions"] if szl else None},
            "deepxde": {"method_class": "neural PINN (MLP minimizes PDE residual)",
                        "deps": ["pytorch"], "license": "LGPL-2.1",
                        "shipped": False,
                        "usage": ("benchmark-only dev dependency; NEVER imported by serve.py or "
                                  "any shipped module. The /pinn/bench endpoint only reads this "
                                  "committed artifact."),
                        "versions": (dx_pois or dx_burg or dx_duff or {}).get("framework_versions")},
            "modulus_physicsnemo": {"method_class": "neural PINN (NVIDIA)",
                                    "status": "NOT-RUN", "license": "Apache-2.0",
                                    "note": "NVIDIA Modulus was renamed PhysicsNeMo (same framework)."},
        },
        "problems": problems,
        "interpretation": {
            "poisson": ("SZL is ~machine precision BY CONSTRUCTION (solution in basis, disclosed); "
                        "DeepXDE reaches a solid neural-PINN accuracy without knowing the basis."),
            "burgers": ("honest nonlinear head-to-head: SZL's new Newton-spectral solver and the "
                        "neural PINN both target the exact tanh shock; compare rel-L2 and wall time. "
                        "The DeepXDE arm is a STANDARD, non-shock-adapted PINN \u2014 shock-adaptation "
                        "(RAR / curriculum / hard-BC) is NOT-TESTED and would likely narrow the gap."),
            "duffing": ("both recover α from the same data; compare |α̂-1| and cost."),
        },
        "scope_limits": (
            "This is a LOW-DIMENSIONAL (1D), SMOOTH, CPU-ONLY suite with KNOWN good bases. "
            "It structurally favors spectral methods. The regimes neural PINNs are designed "
            "for — high dimension (curse-of-dimensionality resistance), complex/irregular "
            "geometry, and problems with NO known good basis — are NOT exercised here and are "
            "reported as NOT-TESTED, not as a neural-arm loss. Do not read SZL wins on this "
            "suite as universal superiority."),
        "honesty": (
            "All rel-L2 and wall-time numbers are MEASURED on this box against the exact closed "
            "form; ≥3 seeds are reported as median[min,max] for the neural arm. No joules are "
            "reported (NOT-MEASURED: no power meter). Poisson's in-basis advantage is disclosed. "
            "DeepXDE (LGPL) is benchmark-only and never shipped. Modulus/PhysicsNeMo is NOT-RUN "
            "with a reproduce spec (no GPU)."),
        "doctrine": "Doctrine v11 LOCKED — no fabricated numbers; MEASURED/MODELED/NOT-RUN/NOT-MEASURED/NOT-TESTED labels only.",
        "reproduce": {
            "szl": "python benchmarks/pinn/run_bench.py --arm szl",
            "deepxde": "python benchmarks/pinn/run_bench.py --arm deepxde --problem {poisson|burgers|duffing} --seeds 3",
            "assemble": "python benchmarks/pinn/run_bench.py --assemble --out benchmarks/pinn/results.json",
            "modulus": "requires a CUDA GPU host with nvidia-physicsnemo (see each problem's modulus arm)",
        },
    }
    outp = Path(out_path)
    outp.parent.mkdir(parents=True, exist_ok=True)
    with outp.open("w", encoding="utf-8") as fh:
        json.dump(result, fh, indent=2)
    return result


# --------------------------------------------------------------------------- #
# CLI
# --------------------------------------------------------------------------- #
def main() -> int:
    ap = argparse.ArgumentParser(description="a11oy PINN cross-framework honest benchmark harness.")
    ap.add_argument("--arm", choices=["szl", "deepxde"], help="which arm to run")
    ap.add_argument("--problem", choices=["poisson", "burgers", "duffing"],
                    help="problem for the deepxde arm")
    ap.add_argument("--seeds", type=int, default=3, help="seeds for the deepxde arm (>=3 for honesty)")
    ap.add_argument("--assemble", action="store_true", help="merge partials into results.json")
    ap.add_argument("--out", default=str(HERE.parent / "results.json"), help="assembled artifact path")
    args = ap.parse_args()

    PARTIAL_DIR.mkdir(parents=True, exist_ok=True)

    if args.arm == "szl":
        res = run_szl()
        (PARTIAL_DIR / "szl.json").write_text(json.dumps(res, indent=2))
        print("[run_bench] SZL arm -> _partial/szl.json")
        print("  poisson rel_l2=%.3e  burgers rel_l2=%.3e (newton %d it)  duffing |a-1|=%.4f (%s)"
              % (res["poisson"]["rel_l2_vs_exact"], res["burgers"]["rel_l2_vs_exact"],
                 res["burgers"]["newton_iterations"], res["duffing"]["abs_err"],
                 res["duffing"]["convergence_label"]))
        return 0

    if args.arm == "deepxde":
        if not args.problem:
            print("[run_bench] --arm deepxde requires --problem", file=sys.stderr)
            return 2
        res = run_deepxde(args.problem, args.seeds)
        (PARTIAL_DIR / ("deepxde_%s.json" % args.problem)).write_text(json.dumps(res, indent=2))
        metric = "abs_err" if args.problem == "duffing" else "rel_l2_vs_exact"
        vals = [r[metric] for r in res["seeds"]]
        print("[run_bench] DeepXDE %s -> _partial/deepxde_%s.json  %s median=%.3e [%.3e,%.3e] over %d seeds"
              % (args.problem, args.problem, metric, statistics.median(vals),
                 min(vals), max(vals), len(vals)))
        return 0

    if args.assemble:
        res = assemble(args.out)
        print("[run_bench] assembled -> %s  overall_label=%s" % (args.out, res["overall_label"]))
        for p in res["problems"]:
            labels = ", ".join("%s:%s" % (a["framework"], a["label"]) for a in p["arms"])
            print("  %-22s [%s]" % (p["id"], labels))
        return 0

    ap.print_help()
    return 1


if __name__ == "__main__":
    raise SystemExit(main())