import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import path from "node:path"; import { NodeRunner } from "../src/node"; import { LIBRARY_FILES, buildModesBundle, buildRunBundle, buildStrikeBundle, cloneSpec, generateParamsBlock, isPlainStruct, methodById, parsePlateResult, scenarioPreset, type MethodId, type PlainStruct, type PlainValue, type PlateResult, type ProblemSpec, type RunResult, } from "../src/index"; export const runner = new NodeRunner(); /** * The reference problem: the centre hit of the two Python simulators, with a * short render (the reference WAV excerpts are 50 ms) and the finite-element * mesh set per test. The strike point and the mesh are what the tests vary. */ export function testSpec(scenario = "center-hit"): ProblemSpec { const s = cloneSpec(scenarioPreset(scenario).spec); s.render.duration = 0.1; s.render.nframes = 6; s.render.frame_span = 0.001; s.fem.mesh_x = 16; s.fem.mesh_y = 16; return s; } /** Run a method as its two stages and return the typed result with the raw modes. */ export async function solve( spec: ProblemSpec, method: MethodId, ): Promise<{ modes: PlainValue; result: PlateResult; modesRun: RunResult; strikeRun: RunResult }> { const m = methodById(method); const modesRun = await runner.run({ bundle: buildModesBundle(spec, m) }); if (!modesRun.ok) throw new Error(`${method} modes failed: ${modesRun.error}\n${modesRun.output.join("")}`); const strikeRun = await runner.run({ bundle: buildStrikeBundle(spec, m), inputs: { modes: modesRun.value }, }); if (!strikeRun.ok) throw new Error(`${method} strike failed: ${strikeRun.error}\n${strikeRun.output.join("")}`); return { modes: modesRun.value, result: parsePlateResult(strikeRun.value), modesRun, strikeRun }; } /** Run a method as one script (the generated main.m). */ export async function solveSingle(spec: ProblemSpec, method: MethodId): Promise { const run = await runner.run({ bundle: buildRunBundle(spec, methodById(method)) }); if (!run.ok) throw new Error(`${method} failed: ${run.error}\n${run.output.join("")}`); return parsePlateResult(run.value); } /** Run a custom script after the params block and return the struct it leaves in `out`. */ export async function runScript(spec: ProblemSpec, body: string): Promise { const main = [generateParamsBlock(spec), "", body, ""].join("\n"); const run = await runner.run({ bundle: { files: [{ name: "script.m", source: main }, ...LIBRARY_FILES], entry: "script.m", resultVariable: "out", }, }); if (!run.ok) throw new Error(`script failed: ${run.error}\n${run.output.join("")}`); if (!isPlainStruct(run.value)) throw new Error("script did not leave a struct in out"); return run.value; } export function scalar(s: PlainStruct, name: string): number { const v: PlainValue | undefined = s.fields[name]; if (typeof v === "number") return v; if (typeof v === "boolean") return v ? 1 : 0; if (v && typeof v === "object" && v.kind === "tensor" && v.data.length >= 1) return v.data[0]; throw new Error(`${name} is not a scalar`); } export function vector(s: PlainStruct, name: string): Float64Array { const v: PlainValue | undefined = s.fields[name]; if (typeof v === "number") return Float64Array.of(v); if (v && typeof v === "object" && v.kind === "tensor") return v.data; throw new Error(`${name} is not a vector`); } export function relL2(a: ArrayLike, b: ArrayLike): number { let num = 0; let den = 0; for (let i = 0; i < a.length; i++) { num += (a[i] - b[i]) ** 2; den += a[i] ** 2; } return Math.sqrt(num / Math.max(den, 1e-300)); } export function maxAbs(a: ArrayLike): number { let m = 0; for (let i = 0; i < a.length; i++) m = Math.max(m, Math.abs(a[i])); return m; } // ---- the reference diagnostics of the Python simulators (sim-docs) export interface ReferenceMode { mode: number; frequency_hz: number; q_total: number; hit_participation: number; radiation_participation: number; support_participation?: number; } export interface Reference { model: string; derived: Record; modes: ReferenceMode[]; } const fixtures = path.join(path.dirname(fileURLToPath(import.meta.url)), "fixtures"); export function reference(name: string): Reference { return JSON.parse(readFileSync(path.join(fixtures, `${name}.json`), "utf8")) as Reference; } /** The first 50 ms of the reference WAV, 24-bit samples as fractions of full scale. */ export function referenceWav(name: string): { fs: number; samples: number[] } { return JSON.parse(readFileSync(path.join(fixtures, `${name}_wav.json`), "utf8")) as { fs: number; samples: number[]; }; } /** Frequencies of the audible modes of a result, in order. */ export function audibleFreqs(r: PlateResult): number[] { return Array.from(r.modes.freq).filter((_, k) => r.modes.audible[k]); } /** Indices of the audible modes of a result whose neighbours are at least `gap` Hz away (no degeneracy). */ export function isolatedAudible(r: PlateResult, gap = 5): number[] { const f = r.modes.freq; const out: number[] = []; for (let k = 0; k < f.length; k++) { if (!r.modes.audible[k]) continue; const near = (k > 0 && Math.abs(f[k] - f[k - 1]) < gap) || (k + 1 < f.length && Math.abs(f[k + 1] - f[k]) < gap); if (!near) out.push(k); } return out; } export { cloneSpec };