1/**
2 * Browser validation: the fp32 WebGPU solver against the f64 CPU solver.
3 * Runs identical seeded simulations on both backends and compares fields.
4 * Results are posted to window.__RESULTS__ for the headless runner.
5 */
6import { GpuBackend, CpuBackend, requestShtDevice } from '../src/solver/backend.ts';
7import { Simulation, gridForLmax } from '../src/solver/simulation.ts';
8import { models, defaultParams } from '../src/solver/models.ts';
9import { randomSpectrum } from '../src/sht/reference.ts';
11declare global {
12 interface Window {
13 __RESULTS__?: { ok: boolean; fatal?: string; lines: string[] };
14 }
15}
17const logEl = document.getElementById('log')!;
18const lines: string[] = [];
19let failures = 0;
21function log(s: string): void {
22 lines.push(s);
23 logEl.textContent = lines.join('\n');
24 console.log(s);
25}
27function check(name: string, ok: boolean, detail: string): void {
28 log(`${ok ? 'PASS' : 'FAIL'} ${name} ${detail}`);
29 if (!ok) failures++;
30}
32function relL2(a: ArrayLike<number>, b: ArrayLike<number>): number {
33 let num = 0;
34 let den = 0;
35 for (let i = 0; i < a.length; i++) {
36 const d = a[i] - b[i];
37 num += d * d;
38 den += b[i] * b[i];
39 }
40 return Math.sqrt(num / Math.max(den, 1e-300));
41}
43async function main(): Promise<void> {
44 const device = await requestShtDevice();
46 // --- transform cross-check: GPU vs CPU on a random spectrum ---
47 {
48 const lmax = 31;
49 const { nlat, nphi } = gridForLmax(lmax, 1);
50 const cfg = { lmax, mmax: lmax, nlat, nphi };
51 const gpu = await GpuBackend.create(device, cfg);
52 const cpu = new CpuBackend(cfg);
53 const q = randomSpectrum(cfg, 42);
54 const q64 = new Float64Array(q);
55 const sGpu = await gpu.synth(q64);
56 const sCpu = await cpu.synth(q64);
57 const errSynth = relL2(sGpu, sCpu);
58 const aGpu = await gpu.analys(new Float64Array(sCpu));
59 const aCpu = await cpu.analys(new Float64Array(sCpu));
60 const errAnalys = relL2(aGpu, aCpu);
61 check('transforms: GPU vs CPU', errSynth < 1e-4 && errAnalys < 1e-4,
62 `synth ${errSynth.toExponential(2)}, analys ${errAnalys.toExponential(2)}`);
63 gpu.destroy();
64 }
66 // --- solver cross-check: identical seeded runs on both backends ---
67 {
68 const schnak = models[0];
69 const params = defaultParams(schnak);
70 const lmax = 31;
71 const { nlat, nphi } = gridForLmax(lmax, schnak.pdeg);
72 const cfg = { lmax, mmax: lmax, nlat, nphi };
73 const gpu = await GpuBackend.create(device, cfg);
74 const cpu = new CpuBackend(cfg);
75 const simGpu = new Simulation(gpu, schnak, { ...params });
76 const simCpu = new Simulation(cpu, schnak, { ...params });
77 await simGpu.init(7);
78 await simCpu.init(7);
79 const nsteps = 10;
80 const t0 = performance.now();
81 for (let s = 0; s < nsteps; s++) await simGpu.step();
82 const gpuMs = (performance.now() - t0) / nsteps;
83 for (let s = 0; s < nsteps; s++) await simCpu.step();
84 let worst = 0;
85 for (let k = 0; k < simGpu.nspecies; k++) {
86 worst = Math.max(worst, relL2(simGpu.V[k], simCpu.V[k]));
87 }
88 let nan = false;
89 for (let k = 0; k < simGpu.nspecies; k++) {
90 for (const v of simGpu.V[k]) if (!Number.isFinite(v)) nan = true;
91 }
92 check('solver: GPU vs CPU after 10 steps', worst < 2e-3 && !nan,
93 `worst rel L2 ${worst.toExponential(2)}${nan ? ', NaN!' : ''} (${gpuMs.toFixed(1)} ms/step GPU)`);
94 gpu.destroy();
95 }
97 // --- longer GPU-only run stays finite and patterned ---
98 {
99 const schnak = models[0];
100 const params = defaultParams(schnak);
101 const lmax = 63;
102 const { nlat, nphi } = gridForLmax(lmax, schnak.pdeg);
103 const gpu = await GpuBackend.create(device, { lmax, mmax: lmax, nlat, nphi });
104 const sim = new Simulation(gpu, schnak, params);
105 await sim.init(3);
106 const nsteps = 100;
107 const t0 = performance.now();
108 for (let s = 0; s < nsteps; s++) await sim.step();
109 const ms = (performance.now() - t0) / nsteps;
110 let lo = Infinity;
111 let hi = -Infinity;
112 for (const v of sim.V[0]) {
113 if (v < lo) lo = v;
114 if (v > hi) hi = v;
115 }
116 const finite = Number.isFinite(lo) && Number.isFinite(hi);
117 check('solver: 100 steps at lmax 63 stay finite', finite && lo > -10 && hi < 10,
118 `u range [${lo.toFixed(4)}, ${hi.toFixed(4)}], ${ms.toFixed(1)} ms/step`);
119 gpu.destroy();
120 }
122 window.__RESULTS__ = { ok: failures === 0, lines };
123 log(failures === 0 ? 'ALL PASS' : `${failures} FAILURE(S)`);
124}
126main().catch((e) => {
127 const msg = e instanceof Error ? `${e.message}\n${e.stack ?? ''}` : String(e);
128 log(`fatal: ${msg}`);
129 window.__RESULTS__ = { ok: false, fatal: msg, lines };
130});