Spaces:
Running
Running
chore(sync): mirror backend .py + Dockerfile to Space (hf-sync-backend)
Browse filesAutomated 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.
- Dockerfile +13 -0
- benchmarks/pinn/run_bench.py +523 -0
- serve.py +27 -0
- 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))
|