d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 1// GPU-versus-CPU test: serves the app with Vite's dev server, opens it in a
2// headless Chromium with WebGPU through Playwright, and reads the result of the
3// in-page self-test (src/selftest.ts), which compares one step of the WGSL
4// kernel with the float64 reference.
6import { test } from "node:test";
7import assert from "node:assert/strict";
8import { fileURLToPath } from "node:url";
9import { dirname, join } from "node:path";
10import { createServer } from "vite";
11import { chromium, type Page } from "playwright";
13const root = join(dirname(fileURLToPath(import.meta.url)), "..");
15interface SelfTestCase {
16 name: string;
17 accError: { u: number; v: number; w: number };
18 positionError: number;
19 nodes: number;
20 dt: number;
21}
22interface SelfTestResult { done: boolean; passed?: boolean; cases?: SelfTestCase[]; error?: string }
24interface Status {
25 phase: string;
26 t: number;
27 totalSteps: number;
28 wmax: number;
29 peakPressure: number;
30 samples: number;
31 frames: number;
32 stepsPerSec: number;
33 dt: number;
34 nx: number;
35 ny: number;
36 stick: { maxForce: number } | null;
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 37 hits: { done: number; total: number } | null;
40const ARGS = ["--enable-unsafe-webgpu", "--enable-features=Vulkan", "--use-angle=vulkan", "--ignore-gpu-blocklist", "--no-sandbox"];
42/** Serves the app and opens `query` in a headless Chromium; `body` gets the page and the list of page errors. */
43async function withPage(query: string, body: (page: Page, errors: string[]) => Promise<void>): Promise<void> {
44 const server = await createServer({ root, logLevel: "silent", server: { port: 0, strictPort: false } });
45 await server.listen();
46 const url = server.resolvedUrls?.local[0];
47 assert.ok(url, "dev server URL");
48 const browser = await chromium.launch({ headless: true, args: ARGS });
49 try {
50 const page = await browser.newPage();
51 const errors: string[] = [];
52 page.on("pageerror", (e) => errors.push(e.message));
53 page.on("console", (msg) => { if (msg.type() === "error" || msg.type() === "warning") errors.push(`console.${msg.type()}: ${msg.text()}`); });
54 await page.goto(`${url}?${query}`);
55 await body(page, errors);
56 } finally {
57 await browser.close();
58 await server.close();
59 }
60}
62test("one GPU step matches the float64 reference on flat, bowed, and elliptical sheets", { timeout: 120_000 }, async () => {
63 await withPage("selftest=1", async (page, errors) => {
64 let result: SelfTestResult = { done: false };
65 for (let i = 0; i < 120 && !result.done; i++) {
66 await page.waitForTimeout(500);
67 result = await page.evaluate<SelfTestResult>("window.steelsim ? window.steelsim.selftest() : { done: false }");
68 }
69 assert.deepEqual(errors, [], "page errors");
70 assert.ok(result.done, "self-test did not finish");
71 assert.equal(result.error, undefined, `self-test error: ${result.error}`);
72 for (const c of result.cases ?? []) {
73 console.log(` ${c.name}: ${c.nodes} nodes, dt ${(c.dt * 1e6).toFixed(2)} us, acc errors ${c.accError.u.toExponential(1)} / ${c.accError.v.toExponential(1)} / ${c.accError.w.toExponential(1)}, position ${c.positionError.toExponential(1)}`);
74 }
75 assert.ok(result.passed, `self-test failed: ${JSON.stringify(result.cases)}`);
76 });
77});
79/** Runs a short simulation to completion and returns its final status. */
80async function runToCompletion(query: string): Promise<Status> {
81 let st: Status | null = null;
82 await withPage(`autostart=1&${query}`, async (page, errors) => {
83 for (let i = 0; i < 600; i++) {
84 await page.waitForTimeout(250);
85 st = await page.evaluate<Status | null>("window.steelsim ? window.steelsim.status() : null");
86 const noGpu = await page.evaluate<string | null>("window.steelsim.noGpu()");
87 assert.equal(noGpu, null, `no WebGPU: ${noGpu}`);
88 if (st && st.phase === "done") break;
89 }
90 assert.deepEqual(errors, [], "page errors");
91 });
92 assert.ok(st, "no status");
93 return st!;
94}
96test("a short push-and-release run completes with finite output", { timeout: 180_000 }, async () => {
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 97 const st = await runToCompletion("spacing=0.012&duration=0.2&excitation=push&rampTime=0.05&holdMax=0.1&rigidProjection=1");
d8f9e54Clean rebuild of the steel sheet simulatorJeremy Magland 98 console.log(` push: ${st.nx}x${st.ny}, dt ${(st.dt * 1e6).toFixed(2)} us, ${(st.stepsPerSec / 1e3).toFixed(0)} k steps/s, peak ${st.peakPressure.toFixed(3)} Pa, max |w| ${(st.wmax * 1e3).toFixed(2)} mm`);
99 assert.equal(st.phase, "done");
100 assert.ok(st.samples >= 0.2 * 48000 - 1, "recorded the requested duration");
101 assert.ok(isFinite(st.wmax) && st.wmax < 0.05, "displacement stays bounded");
102 assert.ok(st.peakPressure > 1e-3 && isFinite(st.peakPressure), "the sheet radiates");
103});
105test("a short drumstick strike on an elliptical sheet completes with finite output", { timeout: 180_000 }, async () => {
106 const st = await runToCompletion("spacing=0.012&duration=0.2&excitation=strike&shape=ellipse");
107 console.log(` strike: ${st.nx}x${st.ny}, ${(st.stepsPerSec / 1e3).toFixed(0)} k steps/s, peak ${st.peakPressure.toFixed(3)} Pa, max contact force ${st.stick?.maxForce.toFixed(1)} N`);
108 assert.equal(st.phase, "done");
109 assert.ok(isFinite(st.wmax) && st.wmax < 0.05, "displacement stays bounded");
110 assert.ok(st.peakPressure > 1e-3, "the sheet radiates");
111 assert.ok(st.stick && st.stick.maxForce > 1, "the stick hit the sheet");
112});
a2a1b68Drumstick by default, hit sequences, 170 GPa steelJeremy Magland 113
114test("a short series of two strikes completes with finite output", { timeout: 180_000 }, async () => {
115 const st = await runToCompletion("spacing=0.012&duration=0.2&hitSequence=crescendo&hitCount=2&hitInterval=0.1&strikeVelocityMin=1");
116 console.log(` series: hits ${st.hits?.done} of ${st.hits?.total}, peak ${st.peakPressure.toFixed(3)} Pa, max contact force ${st.stick?.maxForce.toFixed(1)} N`);
117 assert.equal(st.phase, "done");
118 assert.deepEqual(st.hits, { done: 2, total: 2 });
119 assert.ok(isFinite(st.wmax) && st.wmax < 0.05, "displacement stays bounded");
120 assert.ok(st.stick && st.stick.maxForce > 1, "the stick hit the sheet");
121});