/ proof-of-concept / cymsimweb
Sign in
proof-of-concept / cymsimweb
cymsimweb / tests / cymsimfile.test.ts
123 lines · 8.4 KBCodeBlameHistory
13f4dbdexport .cymsim fileJeremy Magland 1// The .cymsim file: write a short CPU run with h5wasm, read it back, and cross-check with the
2// Python package when it is installed (python -c "import cymsim" succeeds): Python loads the
3// file written here, and a file written by Python loads here.
4import test from "node:test";
5import assert from "node:assert/strict";
6import fs from "node:fs";
7import os from "node:os";
8import path from "node:path";
9import { spawnSync } from "node:child_process";
10import { buildSetup } from "../src/model/setup.ts";
11import { CpuSimulator } from "../src/cpu/simulator.ts";
12import { AccelerationStore, DisplacementStore } from "../src/analysis/acceleration.ts";
13import { readCymsim, writeCymsim, type CymsimRun, type H5Module } from "../src/io/cymsimFile.ts";
14import { configToYaml, parseConfigYaml } from "../src/config/yaml.ts";
15import { dispEveryFor } from "../src/model/simulator.ts";
16import { fixture } from "./helpers.ts";
18const h5: H5Module = (await import("h5wasm/node")).default as unknown as H5Module;
19await h5.ready;
20const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "cymsimweb-"));
21const python = (() => { const r = spawnSync("python", ["-c", "import cymsim, h5py"], { encoding: "utf8" }); return r.status === 0; })();
23async function shortRun(nSamples = 300): Promise<CymsimRun> {
24 const cfg = structuredClone(fixture.config);
25 cfg.output.record_acceleration = true; cfg.output.displacement_rate = 1000;
26 const setup = await buildSetup(cfg), N = setup.mesh.n;
27 const dispEvery = dispEveryFor(setup.fs, cfg.output.displacement_rate);
28 const sim = new CpuSimulator(setup, { recordAcceleration: true, dispEvery });
29 const acc = new AccelerationStore(N, setup.fs), disp = new DisplacementStore(N, setup.fs / dispEvery);
30 const front: number[] = [], back: number[] = [];
31 for (let done = 0; done < nSamples; done += 100) {
32 const r = await sim.runChunk(Math.min(100, nSamples - done));
33 front.push(...r.pressureFront); back.push(...r.pressureBack);
34 acc.push(r.acceleration!, r.pressureFront.length, r.firstSample);
35 r.dispFrames.forEach((w, i) => disp.push(w, r.dispSamples[i] / setup.fs));
36 }
37 const d = await sim.diagnostics(), state = await sim.exportState();
38 const { mesh, shell } = setup;
39 return {
40 yaml: configToYaml(cfg), config: cfg,
41 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 },
42 listener: { rFront: setup.radiation.rFront, rBack: setup.radiation.rBack },
43 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,
44 audioBaffled: Float32Array.from(front), audioUnbaffled: Float32Array.from(front.map((f, i) => f - back[i])),
45 acceleration: acc, taps: setup.radiation.taps, displacement: { rate: disp.rate, times: disp.times, frames: disp.frames },
46 energy: [[d.time, d.kinetic, d.membrane, d.bending, d.exciters, d.maxWoverH]],
47 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 })),
48 state, creator: "cymsimweb test", created: "",
49 };
52test("a run written with h5wasm reads back unchanged", async () => {
53 const run = await shortRun();
54 const file = path.join(tmp, "web.cymsim");
55 writeCymsim(h5, file, run, "cymsimweb 0.1.0");
56 const back = readCymsim(h5, file, (y) => parseConfigYaml(y).config!);
57 assert.deepEqual(back.config, run.config);
58 assert.equal(back.fs, run.fs); assert.equal(back.substeps, run.substeps); assert.ok(Math.abs(back.dt - run.dt) < 1e-18);
59 assert.equal(back.mesh.nNodes, run.mesh.nNodes); assert.deepEqual(Array.from(back.mesh.triangles), Array.from(run.mesh.triangles));
60 assert.deepEqual(Array.from(back.mesh.normal), Array.from(run.mesh.normal));
61 assert.deepEqual(Array.from(back.audioBaffled), Array.from(run.audioBaffled));
62 assert.deepEqual(Array.from(back.audioUnbaffled), Array.from(run.audioUnbaffled));
63 assert.equal(back.acceleration!.nSamples, run.acceleration!.nSamples);
64 assert.deepEqual(Array.from(back.acceleration!.read(0, 300)), Array.from(run.acceleration!.read(0, 300)));
65 assert.deepEqual(Array.from(back.taps!), Array.from(run.taps!));
66 assert.equal(back.displacement!.frames.length, run.displacement!.frames.length);
67 assert.deepEqual(back.displacement!.times, run.displacement!.times);
68 assert.deepEqual(Array.from(back.displacement!.frames[2]), Array.from(run.displacement!.frames[2]));
69 assert.deepEqual(back.energy, run.energy); assert.deepEqual(back.hits, run.hits);
70 assert.equal(back.complete, false); assert.equal(back.nStepsDone, run.nStepsDone);
71 assert.equal(back.state!.kind, "cpu"); assert.equal(back.state!.step, run.state!.step);
72 assert.deepEqual(Array.from(back.state!.arrays.q), Array.from(run.state!.arrays.q));
73 assert.equal(back.creator, "cymsimweb 0.1.0");
74 // the state continues the run exactly
75 const setup = await buildSetup(run.config);
76 const sim = new CpuSimulator(setup, { recordAcceleration: true });
77 await sim.importState(back.state!);
78 const r = await sim.runChunk(20);
79 assert.equal(r.firstSample, run.audioBaffled.length);
80});
82test("Python loads a file written here, and a Python file loads here", { skip: !python && "cymsim is not importable by python" }, async () => {
83 const run = await shortRun();
84 const file = path.join(tmp, "web2.cymsim");
85 writeCymsim(h5, file, run, "cymsimweb 0.1.0");
86 const pyRead = spawnSync("python", ["-c", `
87import json, numpy as np, cymsim
88r = cymsim.load_cymsim(${JSON.stringify(file)})
89a = r.acceleration.as_float()
90print(json.dumps({"n": r.n_nodes, "complete": r.complete, "steps": r.n_steps_done, "audio": len(r.pressure_rayleigh),
91 "p": float(r.pressure_rayleigh[100]), "acc": float(a[150, 3]), "scale": float(r.acceleration.scale[150]), "frames": r.displacement.n_frames,
92 "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}))
93`], { encoding: "utf8" });
94 assert.equal(pyRead.status, 0, pyRead.stderr);
95 const got = JSON.parse(pyRead.stdout.trim().split("\n").pop()!);
96 assert.equal(got.n, run.mesh.nNodes); assert.equal(got.complete, false); assert.equal(got.steps, run.nStepsDone);
97 assert.equal(got.audio, run.audioBaffled.length); assert.ok(Math.abs(got.p - run.audioBaffled[100]) < 1e-9);
98 assert.ok(Math.abs(got.acc - run.acceleration!.read(150, 151)[3]) < 1e-3 * Math.abs(got.scale) + 1e-9);
99 assert.equal(got.frames, run.displacement!.frames.length); assert.equal(got.kind, "mallet");
100 assert.ok(Math.abs(got.peak - run.hits[0].peak_force) < 1e-9); assert.equal(got.creator, "cymsimweb 0.1.0");
101 assert.equal(got.state, true, "Python must ignore a state written by the web app"); assert.equal(got.yaml_ok, true);
103 // a file written by Python: a coarse run of the same configuration with the record on
104 const pyFile = path.join(tmp, "py.cymsim");
105 const pyWrite = spawnSync("python", ["-c", `
106import json, cymsim
107cfg = cymsim.SimulationConfig.from_dict(json.loads(${JSON.stringify(JSON.stringify(fixture.config))}))
108cfg.numerics.duration = 0.01; cfg.output.record_acceleration = True; cfg.output.displacement_rate = 1000
109cymsim.Simulator(cfg).run(save_path=${JSON.stringify(pyFile)})
110`], { encoding: "utf8" });
111 assert.equal(pyWrite.status, 0, pyWrite.stderr);
112 const back = readCymsim(h5, pyFile, (y) => parseConfigYaml(y).config!);
113 assert.equal(back.creator.split(" ")[0], "cymsim"); assert.equal(back.complete, true);
114 assert.equal(back.config.output.record_acceleration, true); assert.equal(back.mesh.nNodes, run.mesh.nNodes);
115 assert.equal(back.audioBaffled.length, Math.round(0.01 * back.fs)); assert.equal(back.acceleration!.nSamples, back.audioBaffled.length);
116 assert.ok(back.displacement!.frames.length >= 10); assert.equal(back.hits[0].kind, "mallet"); assert.equal(back.state, null);
117 // the two implementations agree on the record itself
118 const n = Math.min(back.acceleration!.nSamples, run.acceleration!.nSamples);
119 const a = back.acceleration!.read(0, n), b = run.acceleration!.read(0, n);
120 let num = 0, den = 0;
121 for (let i = 0; i < a.length; i++) { num += (a[i] - b[i]) ** 2; den += b[i] ** 2; }
122 assert.ok(Math.sqrt(num / den) < 1e-3, `acceleration records differ by ${Math.sqrt(num / den)} rel rms`);
123});
↑↓ move↵openescclose