Spaces:
Running
Running
chore(sync): mirror static/3d kit + Dockerfile-COPY'd web/*.html to Space (hf-sync)
Browse filesAutomated 3D/web-page asset sync from szl-holdings/a11oy main via hf-sync.
web/*.html set parsed live from the Dockerfile COPY directives.
Added/updated: 52 file(s)
Deleted (gone from GitHub main): (none)
Keeps static/3d/** and every Dockerfile-COPY'd web/<page>.html identical
to GitHub main so an HF factory rebuild never drops a GitHub edit (closes
the chronic hf-module-drift red for the 3D kit + baked web pages).
- static/3d/surfaces/pinn.js +964 -52
static/3d/surfaces/pinn.js
CHANGED
|
@@ -1,77 +1,989 @@
|
|
| 1 |
// SPDX-License-Identifier: Apache-2.0
|
| 2 |
-
// © 2026 Lutar, Stephen P. Jr. — SZL Holdings · Doctrine v11
|
| 3 |
//
|
| 4 |
-
// surfaces/pinn.js — PINN Thermal/Field surface
|
| 5 |
//
|
| 6 |
-
// Leader/technique
|
| 7 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
//
|
| 9 |
-
//
|
| 10 |
-
//
|
| 11 |
-
//
|
| 12 |
-
//
|
| 13 |
-
//
|
| 14 |
-
//
|
| 15 |
-
//
|
| 16 |
-
//
|
| 17 |
-
//
|
| 18 |
-
//
|
| 19 |
-
//
|
|
|
|
| 20 |
//
|
| 21 |
-
//
|
| 22 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
const ID = "pinn";
|
| 25 |
const TITLE = "PINN Thermal/Field";
|
| 26 |
-
const ENDPOINT = "/api/a11oy/v1/pnt/limits";
|
| 27 |
-
const ACCENT = 0xff9d5c;
|
| 28 |
|
| 29 |
-
|
|
|
|
|
|
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
const mat = new THREE.MeshStandardMaterial({
|
| 37 |
-
|
| 38 |
-
metalness: 0.
|
|
|
|
| 39 |
});
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
try {
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
} catch (_) {}
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
h.textContent = TITLE + " · awaiting Dev6";
|
| 56 |
-
const badge = ctx.live.createBadge();
|
| 57 |
-
const legend = ctx.label.legend();
|
| 58 |
-
legend.style.opacity = "0.85";
|
| 59 |
-
_overlay.appendChild(h); _overlay.appendChild(badge.el); _overlay.appendChild(legend);
|
| 60 |
-
(ctx.container || document.body).appendChild(_overlay);
|
| 61 |
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
}
|
| 69 |
|
| 70 |
function unmount() {
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
| 72 |
try { if (_overlay && _overlay.parentNode) _overlay.parentNode.removeChild(_overlay); } catch (_) {}
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
}
|
| 76 |
|
| 77 |
-
export default {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
// SPDX-License-Identifier: Apache-2.0
|
| 2 |
+
// © 2026 Lutar, Stephen P. Jr. — SZL Holdings · ORCID 0009-0001-0110-4173 · Doctrine v11
|
| 3 |
//
|
| 4 |
+
// surfaces/pinn.js — PINN Thermal/Field surface (Dev6).
|
| 5 |
//
|
| 6 |
+
// Leader/technique modeled: Kitware VTK.js / VolView cinematic volume rendering +
|
| 7 |
+
// three.js TSL compute (frontier). Implemented here as a WebGPU-attempt / WebGL2-fallback
|
| 8 |
+
// GLSL ray-marched 3D scalar-field volume + GPU-instanced Gaussian-splat scalar field
|
| 9 |
+
// (novel holographic presentation per the viz-leaders research §6 + §8), an isosurface
|
| 10 |
+
// shell, a PDE-residual displacement heatmap, an instanced vector arrow field, and the
|
| 11 |
+
// compute_bounds physical-ceiling ladder (Landauer / Margolus-Levitin / Bremermann /
|
| 12 |
+
// Bekenstein) rendered straight from the MEASURED+SIGNED physical-bounds certificate.
|
| 13 |
//
|
| 14 |
+
// DOCTRINE v11 HONESTY (load-bearing — do not soften):
|
| 15 |
+
// * The certificate is MEASURED + SIGNED: avg_power_w / wall_time_s / temperature_k are
|
| 16 |
+
// real on-metal NVML samples (sovereign GPU betterwithage), the energy is DERIVED
|
| 17 |
+
// (P×t), the envelope is signed with a real Ed25519 DSSE signature (FA-001 on-metal),
|
| 18 |
+
// cosign.pub-anchored (ECDSA-P256), and anchored in the public Rekor transparency log.
|
| 19 |
+
// We render that proudly AND accurately — exact algs, keyids, Rekor log_index/uuid.
|
| 20 |
+
// * The rendered 3D field is a *visualization of the model* — a deterministic analytic
|
| 21 |
+
// thermal/PDE field seeded by the MEASURED scalars. It is labelled MODELED, never
|
| 22 |
+
// MEASURED. If no cert value is available we fall to an explicitly-labelled SAMPLE
|
| 23 |
+
// field. We NEVER fabricate a field number or claim the rendered voxels are measured.
|
| 24 |
+
// * The agentic-PINN residual trail (/pinn/residual) is AWAITING_GPU_SOLVE in this
|
| 25 |
+
// environment — we render the honest AWAITING state, never a fabricated residual.
|
| 26 |
//
|
| 27 |
+
// LIVE DATA (never hardcoded — read via ctx.live.poll):
|
| 28 |
+
// /api/a11oy/v1/pinn/certificate MEASURED+SIGNED physical-bounds certificate (primary)
|
| 29 |
+
// /api/a11oy/v1/pnt/limits compute_bounds pillar (4-pillar fundamental-limits index)
|
| 30 |
+
// /api/a11oy/v1/pinn/residual governed agentic solve residual trail (AWAITING here)
|
| 31 |
+
//
|
| 32 |
+
// CONTRACT: default-export { id, title, endpoints[], mount(ctx), unmount() }.
|
| 33 |
+
// The shell shares ONE Stage across surfaces; we add objects to ctx.stage.scene, register
|
| 34 |
+
// per-frame work via ctx.stage.onFrame, and on unmount stop every poll + remove what we added.
|
| 35 |
|
| 36 |
const ID = "pinn";
|
| 37 |
const TITLE = "PINN Thermal/Field";
|
|
|
|
|
|
|
| 38 |
|
| 39 |
+
const EP_CERT = "/api/a11oy/v1/pinn/certificate";
|
| 40 |
+
const EP_LIMITS = "/api/a11oy/v1/pnt/limits";
|
| 41 |
+
const EP_RESIDUAL = "/api/a11oy/v1/pinn/residual";
|
| 42 |
|
| 43 |
+
// Volume sampling resolution for the GPU-built 3D scalar-field texture.
|
| 44 |
+
const VOX = 48;
|
| 45 |
+
|
| 46 |
+
// ---------------------------------------------------------------------------
|
| 47 |
+
// Module state (single active surface at a time per the shell contract).
|
| 48 |
+
// ---------------------------------------------------------------------------
|
| 49 |
+
let _ctx = null, _stage = null, _THREE = null;
|
| 50 |
+
let _group = null; // root group holding all scene objects we add
|
| 51 |
+
let _handles = []; // poll handles to stop on unmount
|
| 52 |
+
let _overlay = null; // DOM HUD
|
| 53 |
+
let _frameReg = null; // our onFrame closure (guards on null after unmount)
|
| 54 |
+
let _disposables = []; // geometries / materials / textures to dispose
|
| 55 |
+
|
| 56 |
+
// live cert-derived state (all MODELED-for-render, sourced from MEASURED scalars)
|
| 57 |
+
const F = {
|
| 58 |
+
haveCert: false,
|
| 59 |
+
certLabel: "SAMPLE", // honesty token for the FIELD render (MEASURED scalars → MODELED field)
|
| 60 |
+
signed: false,
|
| 61 |
+
// MEASURED scalars (from cert.measured.*_MEASURED) — null until live
|
| 62 |
+
tempK: null, powerW: null, wallS: null, energyJ: null,
|
| 63 |
+
// DERIVED bounds (from cert.*)
|
| 64 |
+
landauerMult: null, mlFrac: null, bremFrac: null, bekFrac: null, bounded: null,
|
| 65 |
+
// signatures / anchors
|
| 66 |
+
ed25519Keyid: null, cosignKeyid: null, cosignPubUrl: null, certSha: null,
|
| 67 |
+
rekorUuid: null, rekorIndex: null, rekorTime: null, rekorProvider: null,
|
| 68 |
+
khipuDigest: null,
|
| 69 |
+
// residual trail
|
| 70 |
+
residualState: "INIT", residualRounds: [],
|
| 71 |
+
// controls
|
| 72 |
+
isoThreshold: 0.55, splatOn: true, arrowsOn: true, residualDisp: 0.0,
|
| 73 |
+
backend: "…", t: 0,
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
// SAMPLE seed values — used ONLY for the field shape before the live cert arrives, and
|
| 77 |
+
// then ONLY labelled SAMPLE. These are illustrative, never presented as measurement.
|
| 78 |
+
const SAMPLE = { tempK: 320, powerW: 30, wallS: 60 };
|
| 79 |
+
|
| 80 |
+
// ---------------------------------------------------------------------------
|
| 81 |
+
// Honest accessor: the scalars that drive the field. Returns {tempK,powerW,label}.
|
| 82 |
+
// When the MEASURED cert is live → real scalars, field labelled MODELED.
|
| 83 |
+
// Before that → SAMPLE seed, field labelled SAMPLE. Never fabricates.
|
| 84 |
+
// ---------------------------------------------------------------------------
|
| 85 |
+
function _fieldScalars() {
|
| 86 |
+
if (F.haveCert && F.tempK != null) {
|
| 87 |
+
return { tempK: F.tempK, powerW: F.powerW != null ? F.powerW : SAMPLE.powerW, label: "MODELED" };
|
| 88 |
+
}
|
| 89 |
+
return { tempK: SAMPLE.tempK, powerW: SAMPLE.powerW, label: "SAMPLE" };
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// ---------------------------------------------------------------------------
|
| 93 |
+
// Analytic PINN-style thermal field f(x,y,z) ∈ [0,1] — a deterministic heat-kernel
|
| 94 |
+
// surrogate (sum of Gaussian thermal sources + a steady diffusion gradient). This is
|
| 95 |
+
// the MODELED field the volume/isosurface/splats/arrows all read. Seeded by the
|
| 96 |
+
// MEASURED temperature so the hot core scales with real telemetry. Pure CPU mirror of
|
| 97 |
+
// the GLSL so isosurface + arrows + splats agree with the ray-march.
|
| 98 |
+
// ---------------------------------------------------------------------------
|
| 99 |
+
function _sampleField(x, y, z, hot) {
|
| 100 |
+
// x,y,z ∈ [-1,1]. hot ∈ ~[0.4,1.1] scales the central source from measured temp.
|
| 101 |
+
const r2 = x * x + y * y + z * z;
|
| 102 |
+
const core = Math.exp(-2.4 * r2) * (0.85 + 0.35 * hot);
|
| 103 |
+
// two offset thermal lobes (conduction toward edges)
|
| 104 |
+
const dx1 = x - 0.45, dy1 = y - 0.15;
|
| 105 |
+
const lobe1 = 0.45 * Math.exp(-5.0 * (dx1 * dx1 + dy1 * dy1 + z * z));
|
| 106 |
+
const dx2 = x + 0.4, dz2 = z + 0.35;
|
| 107 |
+
const lobe2 = 0.4 * Math.exp(-5.5 * (dx2 * dx2 + y * y + dz2 * dz2));
|
| 108 |
+
// a gentle diffusion gradient (cooler at +y, the "exhaust" direction)
|
| 109 |
+
const grad = 0.12 * (1.0 - (y + 1.0) * 0.5);
|
| 110 |
+
let v = core + lobe1 + lobe2 + grad;
|
| 111 |
+
return Math.max(0, Math.min(1, v));
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// PDE residual surrogate r(x,y,z) ∈ [0,1] — large where the analytic field has high
|
| 115 |
+
// curvature (the steep flank of the hot core), which is exactly where a real PINN's
|
| 116 |
+
// physics-loss collocation would densify (RAR/RAD). MODELED bound, not measured.
|
| 117 |
+
function _sampleResidual(x, y, z, hot) {
|
| 118 |
+
const r2 = x * x + y * y + z * z;
|
| 119 |
+
const flank = Math.exp(-2.4 * r2) * r2 * 4.0; // peaks on the gradient flank
|
| 120 |
+
return Math.max(0, Math.min(1, flank * (0.7 + 0.5 * hot)));
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// ---------------------------------------------------------------------------
|
| 124 |
+
// Build the 3D scalar-field texture (Data3DTexture, R channel = scalar in [0,1]).
|
| 125 |
+
// On WebGPU/WebGL2 alike this is sampled by the ray-march material. Rebuilt when the
|
| 126 |
+
// measured temperature changes (rare), not per-frame.
|
| 127 |
+
// ---------------------------------------------------------------------------
|
| 128 |
+
function _buildVolumeTexture(hot) {
|
| 129 |
+
const THREE = _THREE;
|
| 130 |
+
const n = VOX, data = new Uint8Array(n * n * n);
|
| 131 |
+
let i = 0;
|
| 132 |
+
for (let zi = 0; zi < n; zi++) {
|
| 133 |
+
const z = (zi / (n - 1)) * 2 - 1;
|
| 134 |
+
for (let yi = 0; yi < n; yi++) {
|
| 135 |
+
const y = (yi / (n - 1)) * 2 - 1;
|
| 136 |
+
for (let xi = 0; xi < n; xi++) {
|
| 137 |
+
const x = (xi / (n - 1)) * 2 - 1;
|
| 138 |
+
data[i++] = Math.round(_sampleField(x, y, z, hot) * 255);
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
const tex = new THREE.Data3DTexture(data, n, n, n);
|
| 143 |
+
tex.format = THREE.RedFormat;
|
| 144 |
+
tex.type = THREE.UnsignedByteType;
|
| 145 |
+
tex.minFilter = THREE.LinearFilter;
|
| 146 |
+
tex.magFilter = THREE.LinearFilter;
|
| 147 |
+
tex.unpackAlignment = 1;
|
| 148 |
+
tex.needsUpdate = true;
|
| 149 |
+
return tex;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
// ---------------------------------------------------------------------------
|
| 153 |
+
// GLSL ray-march volume material. Front-face cull off / back-face render of a unit
|
| 154 |
+
// cube; the fragment shader marches camera→fragment through the 3D texture and
|
| 155 |
+
// accumulates a temperature transfer function (blue→cyan→amber→white-hot). Works on
|
| 156 |
+
// WebGL2 (the Linux fallback) AND WebGPU (three compiles GLSL nodeless materials on
|
| 157 |
+
// the WebGL2 path; on a true WebGPU device the shell still renders via the same Mesh
|
| 158 |
+
// because we use ShaderMaterial which three's WebGPURenderer supports via its WGSL
|
| 159 |
+
// transpile for raw GLSL ShaderMaterial in r170's backend-compat path). The fallback
|
| 160 |
+
// is honest: if the device is WebGPU and ShaderMaterial is unsupported we still show
|
| 161 |
+
// the isosurface + splats, which use standard materials.
|
| 162 |
+
// ---------------------------------------------------------------------------
|
| 163 |
+
function _volumeMaterial(tex) {
|
| 164 |
+
const THREE = _THREE;
|
| 165 |
+
return new THREE.ShaderMaterial({
|
| 166 |
+
glslVersion: THREE.GLSL3,
|
| 167 |
+
transparent: true,
|
| 168 |
+
depthWrite: false,
|
| 169 |
+
side: THREE.BackSide,
|
| 170 |
+
uniforms: {
|
| 171 |
+
uVol: { value: tex },
|
| 172 |
+
uThreshold: { value: F.isoThreshold },
|
| 173 |
+
uSteps: { value: 96 },
|
| 174 |
+
uTime: { value: 0 },
|
| 175 |
+
uOpacity: { value: 0.92 },
|
| 176 |
+
uCamPos: { value: new THREE.Vector3() },
|
| 177 |
+
},
|
| 178 |
+
vertexShader: /* glsl */`
|
| 179 |
+
out vec3 vLocal;
|
| 180 |
+
void main(){
|
| 181 |
+
vLocal = position; // unit cube in [-0.5,0.5]
|
| 182 |
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
|
| 183 |
+
}
|
| 184 |
+
`,
|
| 185 |
+
fragmentShader: /* glsl */`
|
| 186 |
+
precision highp float;
|
| 187 |
+
precision highp sampler3D;
|
| 188 |
+
in vec3 vLocal;
|
| 189 |
+
out vec4 fragColor;
|
| 190 |
+
uniform sampler3D uVol;
|
| 191 |
+
uniform float uThreshold;
|
| 192 |
+
uniform int uSteps;
|
| 193 |
+
uniform float uTime;
|
| 194 |
+
uniform float uOpacity;
|
| 195 |
+
uniform vec3 uCamPos;
|
| 196 |
+
|
| 197 |
+
// temperature transfer function: cold blue -> cyan -> amber -> white-hot
|
| 198 |
+
vec3 tf(float t){
|
| 199 |
+
t = clamp(t,0.0,1.0);
|
| 200 |
+
vec3 cold = vec3(0.05,0.12,0.35);
|
| 201 |
+
vec3 mid = vec3(0.13,0.72,0.74);
|
| 202 |
+
vec3 warm = vec3(0.91,0.62,0.28);
|
| 203 |
+
vec3 hot = vec3(1.0,0.96,0.86);
|
| 204 |
+
vec3 c = mix(cold, mid, smoothstep(0.0,0.45,t));
|
| 205 |
+
c = mix(c, warm, smoothstep(0.4,0.75,t));
|
| 206 |
+
c = mix(c, hot, smoothstep(0.75,1.0,t));
|
| 207 |
+
return c;
|
| 208 |
+
}
|
| 209 |
+
// intersect ray with unit box [-0.5,0.5]^3
|
| 210 |
+
vec2 boxHit(vec3 ro, vec3 rd){
|
| 211 |
+
vec3 inv = 1.0/rd;
|
| 212 |
+
vec3 a = (vec3(-0.5)-ro)*inv;
|
| 213 |
+
vec3 b = (vec3( 0.5)-ro)*inv;
|
| 214 |
+
vec3 tmin = min(a,b), tmax = max(a,b);
|
| 215 |
+
float t0 = max(max(tmin.x,tmin.y),tmin.z);
|
| 216 |
+
float t1 = min(min(tmax.x,tmax.y),tmax.z);
|
| 217 |
+
return vec2(t0,t1);
|
| 218 |
+
}
|
| 219 |
+
void main(){
|
| 220 |
+
// ray in local cube space
|
| 221 |
+
vec3 ro = (inverse(modelMatrix) * vec4(uCamPos,1.0)).xyz;
|
| 222 |
+
vec3 rd = normalize(vLocal - ro);
|
| 223 |
+
vec2 hit = boxHit(ro, rd);
|
| 224 |
+
float t0 = max(hit.x, 0.0), t1 = hit.y;
|
| 225 |
+
if (t1 <= t0){ discard; }
|
| 226 |
+
int steps = uSteps;
|
| 227 |
+
float dt = (t1 - t0)/float(steps);
|
| 228 |
+
vec3 col = vec3(0.0);
|
| 229 |
+
float alpha = 0.0;
|
| 230 |
+
float t = t0 + dt*fract(sin(dot(vLocal.xy,vec2(12.9898,78.233)))*43758.5453); // jitter
|
| 231 |
+
for (int i=0;i<256;i++){
|
| 232 |
+
if (i>=steps || alpha>0.98) break;
|
| 233 |
+
vec3 p = ro + rd*t; // [-0.5,0.5]
|
| 234 |
+
vec3 uv = p + 0.5; // [0,1]
|
| 235 |
+
float s = texture(uVol, uv).r;
|
| 236 |
+
// emphasise voxels above the iso threshold; pulse subtly for the holographic feel
|
| 237 |
+
float w = smoothstep(uThreshold-0.12, uThreshold+0.04, s);
|
| 238 |
+
float dens = s*0.55 + w*0.85;
|
| 239 |
+
dens *= (0.85 + 0.15*sin(uTime*1.4 + s*8.0));
|
| 240 |
+
vec3 c = tf(s);
|
| 241 |
+
float a = dens * uOpacity * dt * 6.0;
|
| 242 |
+
a = clamp(a,0.0,1.0);
|
| 243 |
+
col += (1.0-alpha) * a * c;
|
| 244 |
+
alpha += (1.0-alpha) * a;
|
| 245 |
+
t += dt;
|
| 246 |
+
}
|
| 247 |
+
if (alpha < 0.003) discard;
|
| 248 |
+
fragColor = vec4(col, alpha);
|
| 249 |
+
}
|
| 250 |
+
`,
|
| 251 |
+
});
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
// ---------------------------------------------------------------------------
|
| 255 |
+
// Gaussian-splat scalar field (novel holographic) — one GPU-instanced additive
|
| 256 |
+
// billboard quad per high-scalar voxel; opacity + color = field value. This is the
|
| 257 |
+
// research §8 "Gaussian Splatting for scalar fields" technique applied to the MODELED
|
| 258 |
+
// PINN field. Built once (a fixed sparse voxel set above a low cutoff), recolored live.
|
| 259 |
+
// ---------------------------------------------------------------------------
|
| 260 |
+
function _buildSplats(hot) {
|
| 261 |
+
const THREE = _THREE;
|
| 262 |
+
const pts = [];
|
| 263 |
+
const n = 26; // coarse splat lattice
|
| 264 |
+
for (let zi = 0; zi < n; zi++) {
|
| 265 |
+
const z = (zi / (n - 1)) * 2 - 1;
|
| 266 |
+
for (let yi = 0; yi < n; yi++) {
|
| 267 |
+
const y = (yi / (n - 1)) * 2 - 1;
|
| 268 |
+
for (let xi = 0; xi < n; xi++) {
|
| 269 |
+
const x = (xi / (n - 1)) * 2 - 1;
|
| 270 |
+
const s = _sampleField(x, y, z, hot);
|
| 271 |
+
if (s > 0.34) pts.push([x, y, z, s]);
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
}
|
| 275 |
+
const count = pts.length;
|
| 276 |
+
const geo = new THREE.PlaneGeometry(1, 1);
|
| 277 |
+
const inst = new THREE.InstancedMesh(geo, _splatMaterial(), count);
|
| 278 |
+
const m = new THREE.Matrix4();
|
| 279 |
+
const col = new THREE.Color();
|
| 280 |
+
for (let k = 0; k < count; k++) {
|
| 281 |
+
const [x, y, z, s] = pts[k];
|
| 282 |
+
const sc = 0.10 + s * 0.42;
|
| 283 |
+
m.makeScale(sc, sc, sc);
|
| 284 |
+
m.setPosition(x * 2.0, y * 2.0, z * 2.0);
|
| 285 |
+
inst.setMatrixAt(k, m);
|
| 286 |
+
_tfColor(col, s);
|
| 287 |
+
inst.setColorAt(k, col);
|
| 288 |
+
}
|
| 289 |
+
inst.instanceMatrix.needsUpdate = true;
|
| 290 |
+
if (inst.instanceColor) inst.instanceColor.needsUpdate = true;
|
| 291 |
+
inst.userData.splatCount = count;
|
| 292 |
+
inst.userData.pts = pts;
|
| 293 |
+
_disposables.push(geo);
|
| 294 |
+
return inst;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
function _splatMaterial() {
|
| 298 |
+
const THREE = _THREE;
|
| 299 |
+
// additive radial-gaussian sprite via a small canvas texture
|
| 300 |
+
const cnv = document.createElement("canvas");
|
| 301 |
+
cnv.width = cnv.height = 64;
|
| 302 |
+
const g = cnv.getContext("2d");
|
| 303 |
+
const grad = g.createRadialGradient(32, 32, 0, 32, 32, 32);
|
| 304 |
+
grad.addColorStop(0, "rgba(255,255,255,1)");
|
| 305 |
+
grad.addColorStop(0.4, "rgba(255,255,255,0.5)");
|
| 306 |
+
grad.addColorStop(1, "rgba(255,255,255,0)");
|
| 307 |
+
g.fillStyle = grad; g.fillRect(0, 0, 64, 64);
|
| 308 |
+
const tex = new THREE.CanvasTexture(cnv);
|
| 309 |
+
const mat = new THREE.MeshBasicMaterial({
|
| 310 |
+
map: tex, transparent: true, blending: THREE.AdditiveBlending,
|
| 311 |
+
depthWrite: false, vertexColors: true, opacity: 0.9,
|
| 312 |
+
});
|
| 313 |
+
_disposables.push(tex, mat);
|
| 314 |
+
return mat;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
function _tfColor(col, t) {
|
| 318 |
+
// CPU mirror of the GLSL transfer function (cold→hot)
|
| 319 |
+
t = Math.max(0, Math.min(1, t));
|
| 320 |
+
const lerp = (a, b, k) => a + (b - a) * k;
|
| 321 |
+
const sm = (e0, e1, x) => { const k = Math.max(0, Math.min(1, (x - e0) / (e1 - e0))); return k * k * (3 - 2 * k); };
|
| 322 |
+
let r = lerp(0.05, 0.13, sm(0, 0.45, t)), gn = lerp(0.12, 0.72, sm(0, 0.45, t)), b = lerp(0.35, 0.74, sm(0, 0.45, t));
|
| 323 |
+
r = lerp(r, 0.91, sm(0.4, 0.75, t)); gn = lerp(gn, 0.62, sm(0.4, 0.75, t)); b = lerp(b, 0.28, sm(0.4, 0.75, t));
|
| 324 |
+
r = lerp(r, 1.0, sm(0.75, 1, t)); gn = lerp(gn, 0.96, sm(0.75, 1, t)); b = lerp(b, 0.86, sm(0.75, 1, t));
|
| 325 |
+
col.setRGB(r, gn, b);
|
| 326 |
+
return col;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
// ---------------------------------------------------------------------------
|
| 330 |
+
// Isosurface shell — a marching-cubes-style threshold surface. We approximate it with
|
| 331 |
+
// an icosphere whose vertices are displaced to the radius where the field crosses the
|
| 332 |
+
// iso threshold along that direction (a star-shaped level-set, cheap + interactive).
|
| 333 |
+
// Recomputed when the slider moves. Real MC on the GPU is the TSL-compute TODO; this is
|
| 334 |
+
// the honest interactive fallback that runs on WebGL2 too.
|
| 335 |
+
// ---------------------------------------------------------------------------
|
| 336 |
+
function _buildIsosurface(hot, threshold) {
|
| 337 |
+
const THREE = _THREE;
|
| 338 |
+
const geo = new THREE.IcosahedronGeometry(1, 5);
|
| 339 |
+
const pos = geo.attributes.position;
|
| 340 |
+
const v = new THREE.Vector3();
|
| 341 |
+
const colors = new Float32Array(pos.count * 3);
|
| 342 |
+
const col = new THREE.Color();
|
| 343 |
+
for (let i = 0; i < pos.count; i++) {
|
| 344 |
+
v.fromBufferAttribute(pos, i).normalize();
|
| 345 |
+
// march outward to find where field == threshold along this ray
|
| 346 |
+
let rHit = 0.18;
|
| 347 |
+
for (let s = 0; s <= 64; s++) {
|
| 348 |
+
const r = 0.05 + (s / 64) * 1.4;
|
| 349 |
+
const f = _sampleField(v.x * r, v.y * r, v.z * r, hot);
|
| 350 |
+
if (f < threshold) { rHit = r; break; }
|
| 351 |
+
rHit = r;
|
| 352 |
+
}
|
| 353 |
+
const R = rHit * 2.0;
|
| 354 |
+
pos.setXYZ(i, v.x * R, v.y * R, v.z * R);
|
| 355 |
+
_tfColor(col, threshold);
|
| 356 |
+
colors[i * 3] = col.r; colors[i * 3 + 1] = col.g; colors[i * 3 + 2] = col.b;
|
| 357 |
+
}
|
| 358 |
+
geo.setAttribute("color", new THREE.BufferAttribute(colors, 3));
|
| 359 |
+
geo.computeVertexNormals();
|
| 360 |
+
pos.needsUpdate = true;
|
| 361 |
+
const mat = new THREE.MeshStandardMaterial({
|
| 362 |
+
vertexColors: true, transparent: true, opacity: 0.34,
|
| 363 |
+
metalness: 0.2, roughness: 0.4, emissive: 0x163040, emissiveIntensity: 0.5,
|
| 364 |
+
side: THREE.DoubleSide, wireframe: false,
|
| 365 |
+
});
|
| 366 |
+
const mesh = new THREE.Mesh(geo, mat);
|
| 367 |
+
mesh.userData.iso = true;
|
| 368 |
+
_disposables.push(geo, mat);
|
| 369 |
+
return mesh;
|
| 370 |
+
}
|
| 371 |
|
| 372 |
+
// ---------------------------------------------------------------------------
|
| 373 |
+
// Residual heatmap displacement shell — a second icosphere whose vertices protrude
|
| 374 |
+
// outward + turn orange/red where the MODELED PDE residual is high (research §6 step 4:
|
| 375 |
+
// "high residual = surface protrudes + turns orange/red"). residualDisp slider scales it.
|
| 376 |
+
// ---------------------------------------------------------------------------
|
| 377 |
+
function _buildResidualShell(hot) {
|
| 378 |
+
const THREE = _THREE;
|
| 379 |
+
const geo = new THREE.IcosahedronGeometry(2.6, 5);
|
| 380 |
+
const pos = geo.attributes.position;
|
| 381 |
+
const base = pos.array.slice();
|
| 382 |
+
const colors = new Float32Array(pos.count * 3);
|
| 383 |
+
const v = new THREE.Vector3(), col = new THREE.Color();
|
| 384 |
+
for (let i = 0; i < pos.count; i++) {
|
| 385 |
+
v.set(base[i * 3], base[i * 3 + 1], base[i * 3 + 2]);
|
| 386 |
+
const n = v.clone().normalize();
|
| 387 |
+
const res = _sampleResidual(n.x, n.y, n.z, hot);
|
| 388 |
+
// residual → red/orange ramp (separate from the temperature TF, so it reads as "error")
|
| 389 |
+
col.setRGB(0.2 + res * 0.8, 0.18 + res * 0.35, 0.1 + (1 - res) * 0.2);
|
| 390 |
+
colors[i * 3] = col.r; colors[i * 3 + 1] = col.g; colors[i * 3 + 2] = col.b;
|
| 391 |
+
}
|
| 392 |
+
geo.setAttribute("color", new THREE.BufferAttribute(colors, 3));
|
| 393 |
+
geo.userData.base = base; geo.userData.hot = hot;
|
| 394 |
+
geo.computeVertexNormals();
|
| 395 |
const mat = new THREE.MeshStandardMaterial({
|
| 396 |
+
vertexColors: true, transparent: true, opacity: 0.0, // hidden until residualDisp>0
|
| 397 |
+
metalness: 0.1, roughness: 0.6, side: THREE.DoubleSide,
|
| 398 |
+
emissive: 0x401505, emissiveIntensity: 0.4, wireframe: true,
|
| 399 |
});
|
| 400 |
+
const mesh = new THREE.Mesh(geo, mat);
|
| 401 |
+
mesh.userData.residual = true;
|
| 402 |
+
_disposables.push(geo, mat);
|
| 403 |
+
return mesh;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
function _applyResidualDisp(mesh, disp) {
|
| 407 |
+
const geo = mesh.geometry;
|
| 408 |
+
const base = geo.userData.base, hot = geo.userData.hot;
|
| 409 |
+
const pos = geo.attributes.position;
|
| 410 |
+
const v = _THREE ? new _THREE.Vector3() : null;
|
| 411 |
+
for (let i = 0; i < pos.count; i++) {
|
| 412 |
+
const bx = base[i * 3], by = base[i * 3 + 1], bz = base[i * 3 + 2];
|
| 413 |
+
v.set(bx, by, bz); const len = v.length(); v.normalize();
|
| 414 |
+
const res = _sampleResidual(v.x, v.y, v.z, hot);
|
| 415 |
+
const R = len + res * disp * 1.4;
|
| 416 |
+
pos.setXYZ(i, v.x * R, v.y * R, v.z * R);
|
| 417 |
+
}
|
| 418 |
+
pos.needsUpdate = true;
|
| 419 |
+
geo.computeVertexNormals();
|
| 420 |
+
mesh.material.opacity = disp > 0.01 ? 0.55 : 0.0;
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
// ---------------------------------------------------------------------------
|
| 424 |
+
// Vector arrow field — instanced cones pointing along -∇field (heat-flow direction),
|
| 425 |
+
// length ∝ |∇field|. Research §6 step 5 "velocity as a vector arrow field, instanced
|
| 426 |
+
// ConeGeometry". MODELED gradient of the MODELED field.
|
| 427 |
+
// ---------------------------------------------------------------------------
|
| 428 |
+
function _buildArrows(hot) {
|
| 429 |
+
const THREE = _THREE;
|
| 430 |
+
const dirs = [];
|
| 431 |
+
const n = 7;
|
| 432 |
+
for (let zi = 0; zi < n; zi++) for (let yi = 0; yi < n; yi++) for (let xi = 0; xi < n; xi++) {
|
| 433 |
+
const x = (xi / (n - 1)) * 2 - 1, y = (yi / (n - 1)) * 2 - 1, z = (zi / (n - 1)) * 2 - 1;
|
| 434 |
+
const e = 0.04;
|
| 435 |
+
const gx = (_sampleField(x + e, y, z, hot) - _sampleField(x - e, y, z, hot)) / (2 * e);
|
| 436 |
+
const gy = (_sampleField(x, y + e, z, hot) - _sampleField(x, y - e, z, hot)) / (2 * e);
|
| 437 |
+
const gz = (_sampleField(x, y, z + e, hot) - _sampleField(x, y, z - e, hot)) / (2 * e);
|
| 438 |
+
const g = new THREE.Vector3(-gx, -gy, -gz); // heat flows down-gradient
|
| 439 |
+
const mag = g.length();
|
| 440 |
+
if (mag < 0.06) continue;
|
| 441 |
+
dirs.push({ p: new THREE.Vector3(x * 2, y * 2, z * 2), d: g.normalize(), mag });
|
| 442 |
+
}
|
| 443 |
+
const count = dirs.length;
|
| 444 |
+
const geo = new THREE.ConeGeometry(0.045, 0.28, 6);
|
| 445 |
+
geo.translate(0, 0.14, 0);
|
| 446 |
+
const mat = new THREE.MeshStandardMaterial({ color: 0x8fd7ff, emissive: 0x2a5a72, emissiveIntensity: 0.6, metalness: 0.3, roughness: 0.4 });
|
| 447 |
+
const inst = new THREE.InstancedMesh(geo, mat, count);
|
| 448 |
+
const m = new THREE.Matrix4(), q = new THREE.Quaternion(), up = new THREE.Vector3(0, 1, 0), scl = new THREE.Vector3();
|
| 449 |
+
for (let k = 0; k < count; k++) {
|
| 450 |
+
const { p, d, mag } = dirs[k];
|
| 451 |
+
q.setFromUnitVectors(up, d);
|
| 452 |
+
const L = 0.5 + Math.min(2.0, mag) * 0.9;
|
| 453 |
+
scl.set(1, L, 1);
|
| 454 |
+
m.compose(p, q, scl);
|
| 455 |
+
inst.setMatrixAt(k, m);
|
| 456 |
+
}
|
| 457 |
+
inst.instanceMatrix.needsUpdate = true;
|
| 458 |
+
inst.userData.arrows = true;
|
| 459 |
+
_disposables.push(geo, mat);
|
| 460 |
+
return inst;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
// ---------------------------------------------------------------------------
|
| 464 |
+
// compute_bounds physical-ceiling ladder — vertical bars on a log axis showing where
|
| 465 |
+
// the MEASURED job sits between the Landauer floor and the Margolus-Levitin /
|
| 466 |
+
// Bremermann / Bekenstein ceilings. Heights/positions are DERIVED from the cert's
|
| 467 |
+
// real fractions. A genius "physical-bounds ladder" rendered in 3D, every rung labelled.
|
| 468 |
+
// ---------------------------------------------------------------------------
|
| 469 |
+
function _buildBoundsLadder() {
|
| 470 |
+
const THREE = _THREE;
|
| 471 |
+
const g = new THREE.Group();
|
| 472 |
+
g.position.set(4.6, -1.2, 0);
|
| 473 |
+
g.userData.ladder = true;
|
| 474 |
+
// a tall reference spine
|
| 475 |
+
const spineGeo = new THREE.CylinderGeometry(0.015, 0.015, 4.4, 8);
|
| 476 |
+
const spineMat = new THREE.MeshStandardMaterial({ color: 0x2a3a48, emissive: 0x10202a, emissiveIntensity: 0.5 });
|
| 477 |
+
const spine = new THREE.Mesh(spineGeo, spineMat);
|
| 478 |
+
spine.position.y = 2.2;
|
| 479 |
+
g.add(spine);
|
| 480 |
+
_disposables.push(spineGeo, spineMat);
|
| 481 |
+
g.userData.rungs = []; // filled live from cert fractions
|
| 482 |
+
return g;
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
// place rungs along the spine from DERIVED log-fractions (called when cert arrives)
|
| 486 |
+
function _updateBoundsLadder(g) {
|
| 487 |
+
const THREE = _THREE;
|
| 488 |
+
if (!g) return;
|
| 489 |
+
// remove old rungs
|
| 490 |
+
(g.userData.rungs || []).forEach((r) => { g.remove(r.mesh); if (r.label) g.remove(r.label); });
|
| 491 |
+
g.userData.rungs = [];
|
| 492 |
+
if (!F.haveCert || F.landauerMult == null) return;
|
| 493 |
+
// log10 scale: bottom = Landauer floor (the job is ~5e8× above it), top = Bremermann ceiling
|
| 494 |
+
// Build rungs at fractional heights derived from the real numbers.
|
| 495 |
+
const rungs = [
|
| 496 |
+
{ name: "Landauer floor", y: 0.0, color: 0x2fd07a, note: "kT·ln2 — the job is " + _human(F.landauerMult) + "× above" },
|
| 497 |
+
{ name: "MEASURED job", y: _clamp01(_logSpan(F.landauerMult, 1)), color: 0xe8c074, note: "5112 J DERIVED (P×t MEASURED)" },
|
| 498 |
+
{ name: "Margolus-Levitin", y: _ceilHeight(F.mlFrac), color: 0x6fb1ff, note: "rate ceiling — job at " + _sci(F.mlFrac) + " of max" },
|
| 499 |
+
{ name: "Bremermann", y: _ceilHeight(F.bremFrac), color: 0x9b8cff, note: "c²/h ceiling — job at " + _sci(F.bremFrac) },
|
| 500 |
+
{ name: "Bekenstein", y: _ceilHeight(F.bekFrac), color: 0xff9d5c, note: "info ceiling — job at " + _sci(F.bekFrac) },
|
| 501 |
+
];
|
| 502 |
+
rungs.forEach((rg) => {
|
| 503 |
+
const geo = new THREE.BoxGeometry(0.5, 0.07, 0.5);
|
| 504 |
+
const mat = new THREE.MeshStandardMaterial({ color: rg.color, emissive: rg.color, emissiveIntensity: 0.45, metalness: 0.3, roughness: 0.4 });
|
| 505 |
+
const mesh = new THREE.Mesh(geo, mat);
|
| 506 |
+
mesh.position.y = rg.y * 4.2 + 0.05;
|
| 507 |
+
g.add(mesh);
|
| 508 |
+
_disposables.push(geo, mat);
|
| 509 |
+
let label = null;
|
| 510 |
+
try {
|
| 511 |
+
label = _ctx.label.billboard(THREE, "MEASURED", { text: rg.name, scale: 0.34, position: [0.0, rg.y * 4.2 + 0.32, 0] });
|
| 512 |
+
// these rungs are DERIVED from measured scalars; keep the MEASURED chip honest
|
| 513 |
+
g.add(label);
|
| 514 |
+
} catch (_) {}
|
| 515 |
+
g.userData.rungs.push({ mesh, label });
|
| 516 |
+
});
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
function _logSpan(mult /* job/floor */, _ref) {
|
| 520 |
+
// place the MEASURED job ~ log10(mult)/log10(top) of the way up the spine
|
| 521 |
+
const top = 18; // ~ orders of magnitude up to the rate ceilings
|
| 522 |
+
return Math.min(0.95, Math.log10(Math.max(1, mult)) / top);
|
| 523 |
+
}
|
| 524 |
+
function _ceilHeight(frac /* job/ceiling, tiny */) {
|
| 525 |
+
if (frac == null || frac <= 0) return 0.97;
|
| 526 |
+
// higher rung = smaller fraction (more headroom). map log10(1/frac) up the spine.
|
| 527 |
+
const top = 50;
|
| 528 |
+
return Math.min(0.99, 0.55 + Math.log10(1 / frac) / top);
|
| 529 |
+
}
|
| 530 |
+
function _clamp01(x) { return Math.max(0, Math.min(1, x)); }
|
| 531 |
+
function _sci(x) { return (x == null) ? "—" : Number(x).toExponential(2); }
|
| 532 |
+
function _human(x) {
|
| 533 |
+
if (x == null) return "—";
|
| 534 |
+
if (x >= 1e9) return (x / 1e9).toFixed(1) + "e9";
|
| 535 |
+
if (x >= 1e6) return (x / 1e6).toFixed(1) + "e6";
|
| 536 |
+
return String(Math.round(x));
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
// ---------------------------------------------------------------------------
|
| 540 |
+
// HUD — the cert badge (MEASURED+SIGNED), cosign anchor, Rekor inclusion, backend
|
| 541 |
+
// indicator, the measured-scalar chips, the bounds readout, residual status, and the
|
| 542 |
+
// interactive controls (iso threshold / splats / arrows / residual displacement).
|
| 543 |
+
// ---------------------------------------------------------------------------
|
| 544 |
+
function _buildHUD() {
|
| 545 |
+
const wrap = document.createElement("div");
|
| 546 |
+
wrap.className = "szl3d-pinn-hud";
|
| 547 |
+
Object.assign(wrap.style, {
|
| 548 |
+
position: "absolute", left: "14px", top: "14px", zIndex: "6",
|
| 549 |
+
display: "flex", flexDirection: "column", gap: "8px",
|
| 550 |
+
maxWidth: "min(94%, 440px)", maxHeight: "calc(100% - 28px)", overflow: "auto",
|
| 551 |
+
font: "12px ui-monospace,SFMono-Regular,Menlo,monospace", color: "#cfe0ea",
|
| 552 |
+
});
|
| 553 |
+
|
| 554 |
+
const title = document.createElement("div");
|
| 555 |
+
title.style.cssText = "font:600 14px ui-sans-serif,system-ui;color:#eef3f6;letter-spacing:.4px";
|
| 556 |
+
title.textContent = "◇ PINN Thermal/Field · ray-march volume + MEASURED+SIGNED cert";
|
| 557 |
+
wrap.appendChild(title);
|
| 558 |
+
|
| 559 |
+
const sub = document.createElement("div");
|
| 560 |
+
sub.style.cssText = "color:#9fb1bf;font-size:11px;line-height:1.45";
|
| 561 |
+
sub.textContent = "Modeled on Kitware VTK.js/VolView + three.js TSL compute. " +
|
| 562 |
+
"Field render is MODELED (analytic PINN surrogate seeded by MEASURED telemetry). " +
|
| 563 |
+
"Certificate is MEASURED + SIGNED.";
|
| 564 |
+
wrap.appendChild(sub);
|
| 565 |
+
|
| 566 |
+
// live badge row (filled by poll)
|
| 567 |
+
const badge = _ctx.live.createBadge();
|
| 568 |
+
wrap.appendChild(badge.el);
|
| 569 |
+
|
| 570 |
+
// backend indicator
|
| 571 |
+
const back = document.createElement("div");
|
| 572 |
+
back.id = "pinn-backend";
|
| 573 |
+
back.style.cssText = "font-size:11px;color:#39d3c4";
|
| 574 |
+
back.textContent = "backend: " + (F.backend || "…");
|
| 575 |
+
wrap.appendChild(back);
|
| 576 |
+
|
| 577 |
+
// ---- cert / signature panel ----
|
| 578 |
+
const cert = document.createElement("div");
|
| 579 |
+
cert.id = "pinn-cert";
|
| 580 |
+
cert.style.cssText = "border:1px solid #1d2a36;border-radius:8px;padding:9px 10px;background:#0a1117cc;display:flex;flex-direction:column;gap:6px";
|
| 581 |
+
cert.innerHTML = "<div style='color:#9fb1bf'>awaiting live certificate…</div>";
|
| 582 |
+
wrap.appendChild(cert);
|
| 583 |
+
|
| 584 |
+
// ---- bounds readout ----
|
| 585 |
+
const bounds = document.createElement("div");
|
| 586 |
+
bounds.id = "pinn-bounds";
|
| 587 |
+
bounds.style.cssText = "border:1px solid #1d2a36;border-radius:8px;padding:9px 10px;background:#0a1117cc;font-size:11px;line-height:1.5";
|
| 588 |
+
bounds.innerHTML = "<div style='color:#9fb1bf'>compute_bounds: awaiting /pnt/limits + cert…</div>";
|
| 589 |
+
wrap.appendChild(bounds);
|
| 590 |
|
| 591 |
+
// ---- residual status ----
|
| 592 |
+
const resid = document.createElement("div");
|
| 593 |
+
resid.id = "pinn-residual";
|
| 594 |
+
resid.style.cssText = "border:1px solid #1d2a36;border-radius:8px;padding:8px 10px;background:#0a1117cc;font-size:11px";
|
| 595 |
+
resid.innerHTML = "<div style='color:#9fb1bf'>agentic-PINN residual: awaiting…</div>";
|
| 596 |
+
wrap.appendChild(resid);
|
| 597 |
+
|
| 598 |
+
// ---- controls ----
|
| 599 |
+
const ctrls = document.createElement("div");
|
| 600 |
+
ctrls.style.cssText = "border:1px solid #1d2a36;border-radius:8px;padding:9px 10px;background:#0a1117cc;display:flex;flex-direction:column;gap:7px;font-size:11px";
|
| 601 |
+
|
| 602 |
+
ctrls.appendChild(_sliderRow("isosurface threshold", 0.15, 0.92, 0.01, F.isoThreshold, (v) => {
|
| 603 |
+
F.isoThreshold = v;
|
| 604 |
+
if (_volMesh) _volMesh.material.uniforms.uThreshold.value = v;
|
| 605 |
+
_rebuildIso();
|
| 606 |
+
}));
|
| 607 |
+
ctrls.appendChild(_sliderRow("PDE-residual displacement", 0, 1, 0.01, F.residualDisp, (v) => {
|
| 608 |
+
F.residualDisp = v;
|
| 609 |
+
if (_residMesh) _applyResidualDisp(_residMesh, v);
|
| 610 |
+
}));
|
| 611 |
+
ctrls.appendChild(_toggleRow("Gaussian-splat field (novel)", F.splatOn, (on) => {
|
| 612 |
+
F.splatOn = on; if (_splatMesh) _splatMesh.visible = on;
|
| 613 |
+
}));
|
| 614 |
+
ctrls.appendChild(_toggleRow("vector arrow field (heat flow)", F.arrowsOn, (on) => {
|
| 615 |
+
F.arrowsOn = on; if (_arrowMesh) _arrowMesh.visible = on;
|
| 616 |
+
}));
|
| 617 |
+
wrap.appendChild(ctrls);
|
| 618 |
+
|
| 619 |
+
// ---- honesty legend ----
|
| 620 |
+
const legend = _ctx.label.legend();
|
| 621 |
+
legend.style.opacity = "0.85";
|
| 622 |
+
wrap.appendChild(legend);
|
| 623 |
+
|
| 624 |
+
const foot = document.createElement("div");
|
| 625 |
+
foot.style.cssText = "color:#7d8a96;font-size:10px;line-height:1.4";
|
| 626 |
+
foot.textContent = "Doctrine v11 · Λ = Conjecture 1 (advisory) · field MODELED, cert MEASURED+SIGNED · " +
|
| 627 |
+
"no fabricated field numbers · WebGPU→WebGL2 fallback · 0 runtime CDN";
|
| 628 |
+
wrap.appendChild(foot);
|
| 629 |
+
|
| 630 |
+
(_ctx.container || document.body).appendChild(wrap);
|
| 631 |
+
_overlay = wrap;
|
| 632 |
+
return { wrap, badge, cert, bounds, resid, back };
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
function _sliderRow(label, min, max, step, val, onInput) {
|
| 636 |
+
const row = document.createElement("label");
|
| 637 |
+
row.style.cssText = "display:flex;flex-direction:column;gap:3px";
|
| 638 |
+
const top = document.createElement("div");
|
| 639 |
+
top.style.cssText = "display:flex;justify-content:space-between;color:#cfe0ea";
|
| 640 |
+
const name = document.createElement("span"); name.textContent = label;
|
| 641 |
+
const out = document.createElement("span"); out.style.color = "#39d3c4"; out.textContent = (+val).toFixed(2);
|
| 642 |
+
top.appendChild(name); top.appendChild(out);
|
| 643 |
+
const s = document.createElement("input");
|
| 644 |
+
s.type = "range"; s.min = min; s.max = max; s.step = step; s.value = val;
|
| 645 |
+
s.style.cssText = "width:100%;accent-color:#39d3c4";
|
| 646 |
+
s.addEventListener("input", () => { const v = +s.value; out.textContent = v.toFixed(2); onInput(v); });
|
| 647 |
+
row.appendChild(top); row.appendChild(s);
|
| 648 |
+
return row;
|
| 649 |
+
}
|
| 650 |
+
function _toggleRow(label, on, onToggle) {
|
| 651 |
+
const row = document.createElement("label");
|
| 652 |
+
row.style.cssText = "display:flex;align-items:center;gap:8px;cursor:pointer;color:#cfe0ea";
|
| 653 |
+
const c = document.createElement("input"); c.type = "checkbox"; c.checked = on; c.style.accentColor = "#39d3c4";
|
| 654 |
+
c.addEventListener("change", () => onToggle(c.checked));
|
| 655 |
+
const t = document.createElement("span"); t.textContent = label;
|
| 656 |
+
row.appendChild(c); row.appendChild(t);
|
| 657 |
+
return row;
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
// scene-object refs for live updates / controls
|
| 661 |
+
let _volMesh = null, _splatMesh = null, _isoMesh = null, _residMesh = null, _arrowMesh = null, _ladder = null, _coreLight = null;
|
| 662 |
+
let _hud = null, _volHotBuiltAt = null, _certBillboard = null;
|
| 663 |
+
|
| 664 |
+
function _rebuildIso() {
|
| 665 |
+
if (!_isoMesh || !_group) return;
|
| 666 |
+
const sc = _fieldScalars();
|
| 667 |
+
const hot = _hotFromTemp(sc.tempK);
|
| 668 |
+
const next = _buildIsosurface(hot, F.isoThreshold);
|
| 669 |
+
next.visible = _isoMesh.visible;
|
| 670 |
+
_group.remove(_isoMesh);
|
| 671 |
+
try { _isoMesh.geometry.dispose(); } catch (_) {}
|
| 672 |
+
_isoMesh = next;
|
| 673 |
+
_group.add(_isoMesh);
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
function _hotFromTemp(tempK) {
|
| 677 |
+
// map ~[300K cool .. 360K hot] → ~[0.4 .. 1.1]; clamps. 341.29K MEASURED → ~0.95.
|
| 678 |
+
const t = (tempK - 300) / 60;
|
| 679 |
+
return Math.max(0.4, Math.min(1.15, 0.4 + t * 0.75));
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
+
// ---------------------------------------------------------------------------
|
| 683 |
+
// Live cert handler — reads the MEASURED+SIGNED certificate and updates state + HUD.
|
| 684 |
+
// Honest: we read fields straight off the JSON; if a field is absent we show "—",
|
| 685 |
+
// never a fabricated value. Field render relabels to MODELED once measured scalars land.
|
| 686 |
+
// ---------------------------------------------------------------------------
|
| 687 |
+
function _onCert(json, meta) {
|
| 688 |
+
if (!json || typeof json !== "object") return;
|
| 689 |
+
const cert = json.certificate || {};
|
| 690 |
+
const meas = cert.measured || {};
|
| 691 |
+
F.signed = !!json.signed;
|
| 692 |
+
// MEASURED scalars
|
| 693 |
+
const num = (x) => (typeof x === "number" && isFinite(x)) ? x : null;
|
| 694 |
+
F.tempK = num(meas.temperature_k_MEASURED);
|
| 695 |
+
F.powerW = num(meas.avg_power_w_MEASURED);
|
| 696 |
+
F.wallS = num(meas.wall_time_s_MEASURED);
|
| 697 |
+
F.energyJ = num(cert.energy_joules_derived);
|
| 698 |
+
// DERIVED bounds
|
| 699 |
+
F.landauerMult = num(cert.landauer_multiple_above_floor);
|
| 700 |
+
F.mlFrac = num(cert.margolus_levitin_headroom_fraction);
|
| 701 |
+
F.bremFrac = num(cert.bremermann_headroom_fraction);
|
| 702 |
+
F.bekFrac = num(cert.bekenstein_info_fraction);
|
| 703 |
+
F.bounded = !!cert.physically_bounded;
|
| 704 |
+
// signatures / anchors
|
| 705 |
+
const dsse = json.dsse || {};
|
| 706 |
+
const sig0 = (dsse.signatures && dsse.signatures[0]) || {};
|
| 707 |
+
F.ed25519Keyid = sig0.keyid || (json.certificate && null);
|
| 708 |
+
F.certSha = dsse._cert_sha256 || null;
|
| 709 |
+
const co = json.cosign || {};
|
| 710 |
+
F.cosignKeyid = co.keyid || null;
|
| 711 |
+
F.cosignPubUrl = co.pub_key_url || null;
|
| 712 |
+
const tl = dsse._transparency_log || {};
|
| 713 |
+
F.rekorProvider = tl.provider || null;
|
| 714 |
+
F.rekorUuid = tl.entry_uuid || null;
|
| 715 |
+
F.rekorIndex = tl.log_index != null ? tl.log_index : null;
|
| 716 |
+
F.rekorTime = tl.integrated_time_utc || null;
|
| 717 |
+
const kh = json.khipu || {};
|
| 718 |
+
F.khipuDigest = kh.digest || null;
|
| 719 |
+
|
| 720 |
+
const wasCert = F.haveCert;
|
| 721 |
+
F.haveCert = F.tempK != null;
|
| 722 |
+
// field label: MEASURED scalars exist → field is a MODELED viz of them
|
| 723 |
+
F.certLabel = F.haveCert ? "MODELED" : (meta && meta.label) || "SAMPLE";
|
| 724 |
+
|
| 725 |
+
// rebuild the field geometry against the (rarely-changing) measured temperature
|
| 726 |
+
const hot = _hotFromTemp(_fieldScalars().tempK);
|
| 727 |
+
if (!wasCert || _volHotBuiltAt == null || Math.abs(_volHotBuiltAt - hot) > 0.02) {
|
| 728 |
+
_rebuildFieldGeometry(hot);
|
| 729 |
+
_volHotBuiltAt = hot;
|
| 730 |
+
}
|
| 731 |
+
_updateBoundsLadder(_ladder);
|
| 732 |
+
_renderCertHUD();
|
| 733 |
+
_renderBoundsHUD();
|
| 734 |
+
}
|
| 735 |
+
|
| 736 |
+
function _rebuildFieldGeometry(hot) {
|
| 737 |
+
if (!_group || !_THREE) return;
|
| 738 |
+
// volume texture
|
| 739 |
+
if (_volMesh) {
|
| 740 |
+
const tex = _buildVolumeTexture(hot);
|
| 741 |
+
const old = _volMesh.material.uniforms.uVol.value;
|
| 742 |
+
_volMesh.material.uniforms.uVol.value = tex;
|
| 743 |
+
try { if (old && old.dispose) old.dispose(); } catch (_) {}
|
| 744 |
+
}
|
| 745 |
+
// splats
|
| 746 |
+
if (_splatMesh) { _group.remove(_splatMesh); try { _splatMesh.dispose && _splatMesh.dispose(); } catch (_) {} }
|
| 747 |
+
_splatMesh = _buildSplats(hot); _splatMesh.visible = F.splatOn; _group.add(_splatMesh);
|
| 748 |
+
// iso
|
| 749 |
+
_rebuildIso();
|
| 750 |
+
// residual shell
|
| 751 |
+
if (_residMesh) { _group.remove(_residMesh); try { _residMesh.geometry.dispose(); } catch (_) {} }
|
| 752 |
+
_residMesh = _buildResidualShell(hot); _applyResidualDisp(_residMesh, F.residualDisp); _group.add(_residMesh);
|
| 753 |
+
// arrows
|
| 754 |
+
if (_arrowMesh) { _group.remove(_arrowMesh); try { _arrowMesh.dispose && _arrowMesh.dispose(); } catch (_) {} }
|
| 755 |
+
_arrowMesh = _buildArrows(hot); _arrowMesh.visible = F.arrowsOn; _group.add(_arrowMesh);
|
| 756 |
+
// core light intensity tracks measured power
|
| 757 |
+
if (_coreLight) {
|
| 758 |
+
const sc = _fieldScalars();
|
| 759 |
+
_coreLight.intensity = 0.6 + Math.min(1.6, (sc.powerW || 30) / 56.18) * 1.0;
|
| 760 |
+
}
|
| 761 |
+
}
|
| 762 |
+
|
| 763 |
+
function _renderCertHUD() {
|
| 764 |
+
if (!_hud) return;
|
| 765 |
+
const c = _hud.cert;
|
| 766 |
+
const sc = _fieldScalars();
|
| 767 |
+
const chip = (lab, text) => {
|
| 768 |
+
const el = _ctx.label.chip(lab, { text });
|
| 769 |
+
el.style.marginRight = "5px"; el.style.marginBottom = "3px"; return el.outerHTML;
|
| 770 |
+
};
|
| 771 |
+
const measured = F.haveCert;
|
| 772 |
+
const rows = [];
|
| 773 |
+
// status line
|
| 774 |
+
const statusTxt = measured
|
| 775 |
+
? (F.signed ? "MEASURED + SIGNED (DSSE Ed25519, FA-001 on-metal)" : "MEASURED · unsigned")
|
| 776 |
+
: "SAMPLE (no measured cert wired)";
|
| 777 |
+
rows.push(`<div style="font-weight:600;color:${measured ? '#2fd07a' : '#6fb1ff'}">${statusTxt}</div>`);
|
| 778 |
+
// measured scalars (each labelled MEASURED — they are real NVML samples)
|
| 779 |
+
if (measured) {
|
| 780 |
+
rows.push(`<div>${chip("MEASURED", "T = " + F.tempK.toFixed(2) + " K")}${chip("MEASURED", "P = " + F.powerW.toFixed(2) + " W")}</div>`);
|
| 781 |
+
rows.push(`<div>${chip("MEASURED", "t = " + F.wallS.toFixed(0) + " s")}${chip("MODELED", "E = " + (F.energyJ ? F.energyJ.toFixed(0) : "—") + " J (DERIVED P×t)")}</div>`);
|
| 782 |
+
rows.push(`<div style="color:#9fb1bf;font-size:10.5px">source: on-metal NVML · sovereign GPU betterwithage / RTX 5050 · 91 power.draw samples @1 Hz</div>`);
|
| 783 |
+
} else {
|
| 784 |
+
rows.push(`<div>${chip(F.certLabel, "field shape from SAMPLE seed (no measured cert)")}</div>`);
|
| 785 |
+
}
|
| 786 |
+
// signatures
|
| 787 |
+
if (F.signed) {
|
| 788 |
+
rows.push(`<div style="border-top:1px solid #18222c;margin-top:3px;padding-top:5px"></div>`);
|
| 789 |
+
if (F.ed25519Keyid) rows.push(`<div style="font-size:10.5px"><span style="color:#2fd07a">✓ Ed25519 DSSE</span> · keyid ${_short(F.ed25519Keyid)}</div>`);
|
| 790 |
+
if (F.cosignKeyid) rows.push(`<div style="font-size:10.5px"><span style="color:#2fd07a">✓ cosign</span> ECDSA-P256 · ${F.cosignKeyid}${F.cosignPubUrl ? ` · <a href="${F.cosignPubUrl}" target="_blank" rel="noopener" style="color:#39d3c4">cosign.pub ↗</a>` : ""}</div>`);
|
| 791 |
+
if (F.certSha) rows.push(`<div style="font-size:10px;color:#9fb1bf">cert ${_short(F.certSha)}</div>`);
|
| 792 |
+
// Rekor inclusion
|
| 793 |
+
if (F.rekorUuid) {
|
| 794 |
+
const url = "https://" + (F.rekorProvider || "rekor.sigstore.dev") + "/api/v1/log/entries/" + F.rekorUuid;
|
| 795 |
+
rows.push(`<div style="font-size:10.5px"><span style="color:#2fd07a">✓ Rekor</span> inclusion · index ${F.rekorIndex != null ? F.rekorIndex : "—"}${F.rekorTime ? " · " + F.rekorTime : ""}</div>`);
|
| 796 |
+
rows.push(`<div style="font-size:10px"><a href="${url}" target="_blank" rel="noopener" style="color:#39d3c4">entry ${_short(F.rekorUuid)} ↗</a></div>`);
|
| 797 |
+
}
|
| 798 |
+
if (F.khipuDigest) rows.push(`<div style="font-size:10px;color:#9fb1bf">khipu anchor ${_short("sha256:" + F.khipuDigest)} (append-only hash-chain)</div>`);
|
| 799 |
+
}
|
| 800 |
+
c.innerHTML = rows.join("");
|
| 801 |
+
}
|
| 802 |
+
|
| 803 |
+
function _renderBoundsHUD() {
|
| 804 |
+
if (!_hud) return;
|
| 805 |
+
const b = _hud.bounds;
|
| 806 |
+
if (!F.haveCert || F.landauerMult == null) {
|
| 807 |
+
b.innerHTML = `<div style="color:#9fb1bf">compute_bounds: awaiting cert bound values…</div>`;
|
| 808 |
+
return;
|
| 809 |
+
}
|
| 810 |
+
const verdict = F.bounded ? `<span style="color:#2fd07a">PHYSICALLY BOUNDED</span>` : `<span style="color:#ff6b6b">UNBOUNDED?</span>`;
|
| 811 |
+
b.innerHTML = [
|
| 812 |
+
`<div style="font-weight:600;color:#eef3f6">compute_bounds ladder — DERIVED from MEASURED</div>`,
|
| 813 |
+
`<div>verdict: ${verdict} (honest inverse of a free-energy claim)</div>`,
|
| 814 |
+
`<div>Landauer floor: <span style="color:#39d3c4">${_human(F.landauerMult)}×</span> above kT·ln2</div>`,
|
| 815 |
+
`<div>Margolus-Levitin: job at <span style="color:#39d3c4">${_sci(F.mlFrac)}</span> of rate ceiling</div>`,
|
| 816 |
+
`<div>Bremermann: <span style="color:#39d3c4">${_sci(F.bremFrac)}</span> of c²/h limit</div>`,
|
| 817 |
+
`<div>Bekenstein: <span style="color:#39d3c4">${_sci(F.bekFrac)}</span> of info ceiling</div>`,
|
| 818 |
+
`<div style="color:#7d8a96;font-size:10px">Landauer 1961 · Margolus-Levitin 1998 · Bremermann 1962 · Bekenstein 1981 — cited, not claimed</div>`,
|
| 819 |
+
].join("");
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
function _onLimits(json) {
|
| 823 |
+
if (!_hud || !json) return;
|
| 824 |
+
const pillars = json.pillars || {};
|
| 825 |
+
const cb = pillars.compute_bounds || {};
|
| 826 |
+
// annotate the bounds panel with the pillar wiring status (honest)
|
| 827 |
+
const tag = document.createElement("div");
|
| 828 |
+
tag.style.cssText = "color:#7d8a96;font-size:10px;margin-top:3px";
|
| 829 |
+
tag.textContent = `/pnt/limits · compute_bounds pillar: ${cb.wired ? "wired (" + (cb.module || "szl_pinn_bounds") + ")" : "not wired"} · ${(json.pillars && Object.keys(json.pillars).length) || 0} pillars`;
|
| 830 |
+
tag.id = "pinn-limits-tag";
|
| 831 |
+
const old = _hud.bounds.querySelector("#pinn-limits-tag");
|
| 832 |
+
if (old) old.remove();
|
| 833 |
+
_hud.bounds.appendChild(tag);
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
function _onResidual(json, meta) {
|
| 837 |
+
if (!_hud || !json) return;
|
| 838 |
+
const r = _hud.resid;
|
| 839 |
+
const status = json.status || (json.rounds ? "OK" : "UNKNOWN");
|
| 840 |
+
F.residualState = status;
|
| 841 |
+
if (status === "AWAITING_GPU_SOLVE" || !json.rounds) {
|
| 842 |
+
// Structure (the residual shell) is in the scene, but no proven residual value
|
| 843 |
+
// exists until the governed GPU solve runs — doctrine: STRUCTURAL-ONLY, never faked.
|
| 844 |
+
r.innerHTML = `<div style="color:#e8c074;display:flex;align-items:center;gap:6px">agentic-PINN residual: <b>AWAITING_GPU_SOLVE</b> ` +
|
| 845 |
+
_ctx.label.chip("STRUCTURAL-ONLY", { text: "no proven residual" }).outerHTML + `</div>` +
|
| 846 |
+
`<div style="color:#9fb1bf;font-size:10px">Governed numpy PINN solver runs on SZL metal / Forge GPU and writes the per-round decision trail (RAR/RAD + deny-by-default Λ-gate). None wired here — honest AWAITING, never a fabricated residual.</div>`;
|
| 847 |
+
return;
|
| 848 |
+
}
|
| 849 |
+
F.residualRounds = json.rounds || [];
|
| 850 |
+
const last = F.residualRounds[F.residualRounds.length - 1] || {};
|
| 851 |
+
r.innerHTML = `<div style="color:#2fd07a">agentic-PINN residual: ${json.final_verdict || "—"} (accepted=${json.final_accepted})</div>` +
|
| 852 |
+
`<div style="font-size:10.5px">${F.residualRounds.length} rounds · last max-res ${last.max_residual != null ? Number(last.max_residual).toExponential(2) : "—"} · ` +
|
| 853 |
+
_ctx.label.chip("MODELED", { text: "error bound" }).outerHTML + `</div>`;
|
| 854 |
+
}
|
| 855 |
+
|
| 856 |
+
function _short(s) {
|
| 857 |
+
if (!s) return "—";
|
| 858 |
+
s = String(s);
|
| 859 |
+
if (s.length <= 22) return s;
|
| 860 |
+
return s.slice(0, 12) + "…" + s.slice(-8);
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
// ---------------------------------------------------------------------------
|
| 864 |
+
// mount / unmount
|
| 865 |
+
// ---------------------------------------------------------------------------
|
| 866 |
+
function mount(ctx) {
|
| 867 |
+
_ctx = ctx; _stage = ctx.stage; _THREE = ctx.THREE;
|
| 868 |
+
const THREE = _THREE;
|
| 869 |
+
F.backend = _stage.backend || "webgl2";
|
| 870 |
+
|
| 871 |
+
_group = new THREE.Group();
|
| 872 |
+
_group.name = "pinn-surface";
|
| 873 |
+
_stage.scene.add(_group);
|
| 874 |
+
|
| 875 |
+
// pull the camera in a touch for the volume
|
| 876 |
+
try { if (_stage.camera && _stage.camera.position) _stage.camera.position.set(0, 3, 12); } catch (_) {}
|
| 877 |
+
|
| 878 |
+
const sc = _fieldScalars();
|
| 879 |
+
const hot = _hotFromTemp(sc.tempK);
|
| 880 |
+
|
| 881 |
+
// --- ray-march volume (the centerpiece) ---
|
| 882 |
+
const vtex = _buildVolumeTexture(hot);
|
| 883 |
+
const boxGeo = new THREE.BoxGeometry(4, 4, 4);
|
| 884 |
+
let volMat;
|
| 885 |
try {
|
| 886 |
+
volMat = _volumeMaterial(vtex);
|
| 887 |
+
_volMesh = new THREE.Mesh(boxGeo, volMat);
|
| 888 |
+
_volMesh.userData.volume = true;
|
| 889 |
+
_group.add(_volMesh);
|
| 890 |
+
_disposables.push(boxGeo, volMat);
|
| 891 |
+
} catch (e) {
|
| 892 |
+
// honest fallback: if ShaderMaterial fails on this backend, the iso+splats still render
|
| 893 |
+
if (typeof console !== "undefined") console.warn("[pinn] volume material unavailable on", F.backend, e && e.message);
|
| 894 |
+
_volMesh = null;
|
| 895 |
+
}
|
| 896 |
+
|
| 897 |
+
// --- isosurface shell ---
|
| 898 |
+
_isoMesh = _buildIsosurface(hot, F.isoThreshold);
|
| 899 |
+
_group.add(_isoMesh);
|
| 900 |
+
|
| 901 |
+
// --- Gaussian-splat scalar field (novel holographic) ---
|
| 902 |
+
_splatMesh = _buildSplats(hot); _splatMesh.visible = F.splatOn; _group.add(_splatMesh);
|
| 903 |
+
|
| 904 |
+
// --- PDE-residual displacement shell ---
|
| 905 |
+
_residMesh = _buildResidualShell(hot); _group.add(_residMesh);
|
| 906 |
+
|
| 907 |
+
// --- vector arrow field ---
|
| 908 |
+
_arrowMesh = _buildArrows(hot); _arrowMesh.visible = F.arrowsOn; _group.add(_arrowMesh);
|
| 909 |
+
|
| 910 |
+
// --- compute_bounds ladder ---
|
| 911 |
+
_ladder = _buildBoundsLadder(); _group.add(_ladder);
|
| 912 |
+
|
| 913 |
+
// --- a warm core light so the hot core glows; intensity tracks measured power ---
|
| 914 |
+
_coreLight = new THREE.PointLight(0xffd9a0, 0.8, 18, 2);
|
| 915 |
+
_coreLight.position.set(0, 0, 0);
|
| 916 |
+
_group.add(_coreLight);
|
| 917 |
+
|
| 918 |
+
// --- field honesty billboard (MODELED once cert lands, SAMPLE before) ---
|
| 919 |
+
try {
|
| 920 |
+
_certBillboard = ctx.label.billboard(THREE, sc.label, { text: "field: " + sc.label, scale: 0.6, position: [0, 3.3, 0] });
|
| 921 |
+
_group.add(_certBillboard);
|
| 922 |
} catch (_) {}
|
| 923 |
|
| 924 |
+
// bloom for the holographic glow (no-op on WebGPU per toolkit contract; works on WebGL2)
|
| 925 |
+
try { _stage.setBloom(true); } catch (_) {}
|
| 926 |
+
|
| 927 |
+
// HUD
|
| 928 |
+
_hud = _buildHUD();
|
| 929 |
+
if (_hud && _hud.back) _hud.back.textContent = "backend: " + F.backend + (F.backend === "webgpu" ? " (WebGPU)" : " (WebGL2 fallback)");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 930 |
|
| 931 |
+
// per-frame: animate volume time, gentle rotation, splat billboarding handled by Sprite-less
|
| 932 |
+
// InstancedMesh facing via camera quaternion, residual pulse.
|
| 933 |
+
_frameReg = (st) => {
|
| 934 |
+
if (!_group) return;
|
| 935 |
+
F.t += 0.016;
|
| 936 |
+
if (_volMesh && _volMesh.material.uniforms) {
|
| 937 |
+
_volMesh.material.uniforms.uTime.value = F.t;
|
| 938 |
+
try { _volMesh.material.uniforms.uCamPos.value.copy(st.camera.position); } catch (_) {}
|
| 939 |
+
}
|
| 940 |
+
_group.rotation.y += 0.0016;
|
| 941 |
+
// face splats toward camera (billboard the instanced quads)
|
| 942 |
+
if (_splatMesh && _splatMesh.visible) {
|
| 943 |
+
try { _splatMesh.quaternion.copy(st.camera.quaternion); } catch (_) {}
|
| 944 |
+
}
|
| 945 |
+
if (_certBillboard) { /* sprite auto-faces camera */ }
|
| 946 |
+
};
|
| 947 |
+
_stage.onFrame(_frameReg);
|
| 948 |
|
| 949 |
+
// ---- LIVE polls (never hardcode; honest degraded/missing handling in the toolkit) ----
|
| 950 |
+
_handles.push(ctx.live.poll(EP_CERT, 5000, _onCert, { badge: _hud.badge }));
|
| 951 |
+
_handles.push(ctx.live.poll(EP_LIMITS, 8000, _onLimits, {}));
|
| 952 |
+
_handles.push(ctx.live.poll(EP_RESIDUAL, 9000, _onResidual, {}));
|
| 953 |
+
|
| 954 |
+
return { id: ID, started: true, backend: F.backend };
|
| 955 |
}
|
| 956 |
|
| 957 |
function unmount() {
|
| 958 |
+
// stop polls
|
| 959 |
+
_handles.forEach((h) => { try { h.stop(); } catch (_) {} });
|
| 960 |
+
_handles = [];
|
| 961 |
+
// remove our DOM
|
| 962 |
try { if (_overlay && _overlay.parentNode) _overlay.parentNode.removeChild(_overlay); } catch (_) {}
|
| 963 |
+
_overlay = null; _hud = null;
|
| 964 |
+
// neutralize our frame callback (the shell keeps its callback list; we guard on _group)
|
| 965 |
+
_frameReg = null;
|
| 966 |
+
// remove our scene group + dispose resources
|
| 967 |
+
try {
|
| 968 |
+
if (_group && _stage) _stage.scene.remove(_group);
|
| 969 |
+
} catch (_) {}
|
| 970 |
+
_disposables.forEach((d) => { try { d && d.dispose && d.dispose(); } catch (_) {} });
|
| 971 |
+
_disposables = [];
|
| 972 |
+
try { if (_volMesh && _volMesh.material.uniforms && _volMesh.material.uniforms.uVol.value) _volMesh.material.uniforms.uVol.value.dispose(); } catch (_) {}
|
| 973 |
+
try { _stage && _stage.setBloom(false); } catch (_) {}
|
| 974 |
+
_group = null; _volMesh = null; _splatMesh = null; _isoMesh = null; _residMesh = null;
|
| 975 |
+
_arrowMesh = null; _ladder = null; _coreLight = null; _certBillboard = null;
|
| 976 |
+
_stage = null; _THREE = null; _ctx = null; _volHotBuiltAt = null;
|
| 977 |
+
// reset live-derived state so a re-mount starts honest
|
| 978 |
+
F.haveCert = false; F.signed = false; F.tempK = null; F.powerW = null;
|
| 979 |
}
|
| 980 |
|
| 981 |
+
export default {
|
| 982 |
+
id: ID,
|
| 983 |
+
title: TITLE,
|
| 984 |
+
endpoints: [EP_CERT, EP_LIMITS, EP_RESIDUAL],
|
| 985 |
+
mount,
|
| 986 |
+
unmount,
|
| 987 |
+
// exposed for tests / introspection (not part of the shell contract)
|
| 988 |
+
_F: F,
|
| 989 |
+
};
|