// The .cymsim file: write a short CPU run with h5wasm, read it back, and cross-check with the // Python package when it is installed (python -c "import cymsim" succeeds): Python loads the // file written here, and a file written by Python loads here. import test from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { spawnSync } from "node:child_process"; import { buildSetup } from "../src/model/setup.ts"; import { CpuSimulator } from "../src/cpu/simulator.ts"; import { AccelerationStore, DisplacementStore } from "../src/analysis/acceleration.ts"; import { readCymsim, writeCymsim, type CymsimRun, type H5Module } from "../src/io/cymsimFile.ts"; import { configToYaml, parseConfigYaml } from "../src/config/yaml.ts"; import { dispEveryFor } from "../src/model/simulator.ts"; import { fixture } from "./helpers.ts"; const h5: H5Module = (await import("h5wasm/node")).default as unknown as H5Module; await h5.ready; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "cymsimweb-")); const python = (() => { const r = spawnSync("python", ["-c", "import cymsim, h5py"], { encoding: "utf8" }); return r.status === 0; })(); async function shortRun(nSamples = 300): Promise { const cfg = structuredClone(fixture.config); cfg.output.record_acceleration = true; cfg.output.displacement_rate = 1000; const setup = await buildSetup(cfg), N = setup.mesh.n; const dispEvery = dispEveryFor(setup.fs, cfg.output.displacement_rate); const sim = new CpuSimulator(setup, { recordAcceleration: true, dispEvery }); const acc = new AccelerationStore(N, setup.fs), disp = new DisplacementStore(N, setup.fs / dispEvery); const front: number[] = [], back: number[] = []; for (let done = 0; done < nSamples; done += 100) { const r = await sim.runChunk(Math.min(100, nSamples - done)); front.push(...r.pressureFront); back.push(...r.pressureBack); acc.push(r.acceleration!, r.pressureFront.length, r.firstSample); r.dispFrames.forEach((w, i) => disp.push(w, r.dispSamples[i] / setup.fs)); } const d = await sim.diagnostics(), state = await sim.exportState(); const { mesh, shell } = setup; return { yaml: configToYaml(cfg), config: cfg, mesh: { xy: mesh.xy, z0: shell.z0Node, thickness: shell.hNode, normal: shell.normalNode, triangles: Int32Array.from(mesh.tri), area: mesh.area, boundary: Int32Array.from(mesh.boundary), nNodes: N, nElements: mesh.e }, listener: { rFront: setup.radiation.rFront, rBack: setup.radiation.rBack }, fs: setup.fs, dt: setup.dt, substeps: setup.substeps, duration: cfg.numerics.duration, nSamples: setup.nSamples, nStepsDone: state.step, complete: false, wallTime: 1.5, decimationSpan: 32, audioBaffled: Float32Array.from(front), audioUnbaffled: Float32Array.from(front.map((f, i) => f - back[i])), acceleration: acc, taps: setup.radiation.taps, displacement: { rate: disp.rate, times: disp.times, frames: disp.frames }, energy: [[d.time, d.kinetic, d.membrane, d.bending, d.exciters, d.maxWoverH]], hits: d.hits.map((h, k) => ({ time: setup.patches[k].time, kind: setup.patches[k].hit.exciter.kind, peak_force: h.peakForce, contact_time: h.contactTime, rebound_velocity: h.velocity, energy_in: 9.6, energy_out: 0.5 * setup.patches[k].mass * h.velocity ** 2 })), state, creator: "cymsimweb test", created: "", }; } test("a run written with h5wasm reads back unchanged", async () => { const run = await shortRun(); const file = path.join(tmp, "web.cymsim"); writeCymsim(h5, file, run, "cymsimweb 0.1.0"); const back = readCymsim(h5, file, (y) => parseConfigYaml(y).config!); assert.deepEqual(back.config, run.config); assert.equal(back.fs, run.fs); assert.equal(back.substeps, run.substeps); assert.ok(Math.abs(back.dt - run.dt) < 1e-18); assert.equal(back.mesh.nNodes, run.mesh.nNodes); assert.deepEqual(Array.from(back.mesh.triangles), Array.from(run.mesh.triangles)); assert.deepEqual(Array.from(back.mesh.normal), Array.from(run.mesh.normal)); assert.deepEqual(Array.from(back.audioBaffled), Array.from(run.audioBaffled)); assert.deepEqual(Array.from(back.audioUnbaffled), Array.from(run.audioUnbaffled)); assert.equal(back.acceleration!.nSamples, run.acceleration!.nSamples); assert.deepEqual(Array.from(back.acceleration!.read(0, 300)), Array.from(run.acceleration!.read(0, 300))); assert.deepEqual(Array.from(back.taps!), Array.from(run.taps!)); assert.equal(back.displacement!.frames.length, run.displacement!.frames.length); assert.deepEqual(back.displacement!.times, run.displacement!.times); assert.deepEqual(Array.from(back.displacement!.frames[2]), Array.from(run.displacement!.frames[2])); assert.deepEqual(back.energy, run.energy); assert.deepEqual(back.hits, run.hits); assert.equal(back.complete, false); assert.equal(back.nStepsDone, run.nStepsDone); assert.equal(back.state!.kind, "cpu"); assert.equal(back.state!.step, run.state!.step); assert.deepEqual(Array.from(back.state!.arrays.q), Array.from(run.state!.arrays.q)); assert.equal(back.creator, "cymsimweb 0.1.0"); // the state continues the run exactly const setup = await buildSetup(run.config); const sim = new CpuSimulator(setup, { recordAcceleration: true }); await sim.importState(back.state!); const r = await sim.runChunk(20); assert.equal(r.firstSample, run.audioBaffled.length); }); test("Python loads a file written here, and a Python file loads here", { skip: !python && "cymsim is not importable by python" }, async () => { const run = await shortRun(); const file = path.join(tmp, "web2.cymsim"); writeCymsim(h5, file, run, "cymsimweb 0.1.0"); const pyRead = spawnSync("python", ["-c", ` import json, numpy as np, cymsim r = cymsim.load_cymsim(${JSON.stringify(file)}) a = r.acceleration.as_float() print(json.dumps({"n": r.n_nodes, "complete": r.complete, "steps": r.n_steps_done, "audio": len(r.pressure_rayleigh), "p": float(r.pressure_rayleigh[100]), "acc": float(a[150, 3]), "scale": float(r.acceleration.scale[150]), "frames": r.displacement.n_frames, "kind": r.hits[0].kind, "peak": r.hits[0].peak_force, "creator": r.creator, "state": r.state is None, "yaml_ok": r.config.output.record_acceleration})) `], { encoding: "utf8" }); assert.equal(pyRead.status, 0, pyRead.stderr); const got = JSON.parse(pyRead.stdout.trim().split("\n").pop()!); assert.equal(got.n, run.mesh.nNodes); assert.equal(got.complete, false); assert.equal(got.steps, run.nStepsDone); assert.equal(got.audio, run.audioBaffled.length); assert.ok(Math.abs(got.p - run.audioBaffled[100]) < 1e-9); assert.ok(Math.abs(got.acc - run.acceleration!.read(150, 151)[3]) < 1e-3 * Math.abs(got.scale) + 1e-9); assert.equal(got.frames, run.displacement!.frames.length); assert.equal(got.kind, "mallet"); assert.ok(Math.abs(got.peak - run.hits[0].peak_force) < 1e-9); assert.equal(got.creator, "cymsimweb 0.1.0"); assert.equal(got.state, true, "Python must ignore a state written by the web app"); assert.equal(got.yaml_ok, true); // a file written by Python: a coarse run of the same configuration with the record on const pyFile = path.join(tmp, "py.cymsim"); const pyWrite = spawnSync("python", ["-c", ` import json, cymsim cfg = cymsim.SimulationConfig.from_dict(json.loads(${JSON.stringify(JSON.stringify(fixture.config))})) cfg.numerics.duration = 0.01; cfg.output.record_acceleration = True; cfg.output.displacement_rate = 1000 cymsim.Simulator(cfg).run(save_path=${JSON.stringify(pyFile)}) `], { encoding: "utf8" }); assert.equal(pyWrite.status, 0, pyWrite.stderr); const back = readCymsim(h5, pyFile, (y) => parseConfigYaml(y).config!); assert.equal(back.creator.split(" ")[0], "cymsim"); assert.equal(back.complete, true); assert.equal(back.config.output.record_acceleration, true); assert.equal(back.mesh.nNodes, run.mesh.nNodes); assert.equal(back.audioBaffled.length, Math.round(0.01 * back.fs)); assert.equal(back.acceleration!.nSamples, back.audioBaffled.length); assert.ok(back.displacement!.frames.length >= 10); assert.equal(back.hits[0].kind, "mallet"); assert.equal(back.state, null); // the two implementations agree on the record itself const n = Math.min(back.acceleration!.nSamples, run.acceleration!.nSamples); const a = back.acceleration!.read(0, n), b = run.acceleration!.read(0, n); let num = 0, den = 0; for (let i = 0; i < a.length; i++) { num += (a[i] - b[i]) ** 2; den += b[i] ** 2; } assert.ok(Math.sqrt(num / den) < 1e-3, `acceleration records differ by ${Math.sqrt(num / den)} rel rms`); });