5778036cymbalsim: scripted plate and cymbal designer with a modal linear model and a von Karman GPU modelJeremy Magland 1import { test } from "node:test";
2import assert from "node:assert/strict";
3import { compileDesign, evaluatePlate, DesignError, removePlane, isFlat } from "../src/design.ts";
4import { PRESETS } from "../src/presets.ts";
2481a63Remove the standJeremy Magland 5import { countInside, insideNode, nodeX, nodeY, nodeWeight, massProperties } from "../src/params.ts";
2a1a6daDerive the losses from physical constants instead of a prescribed decay curveJeremy Magland 6import { ROOM_TEMPERATURE } from "../src/params.ts";
5778036cymbalsim: scripted plate and cymbal designer with a modal linear model and a von Karman GPU modelJeremy Magland 7
8const base = `
9mm = 1e-3;
10design.name = 'test';
11design.material.E = 200e9; design.material.nu = 0.3; design.material.rho = 7850;
12design.bounds.width = 0.2; design.bounds.height = 0.16;
13design.thickness = 1*mm;
14`;
16test("a minimal script compiles with defaults", () => {
17 const d = compileDesign(base);
18 assert.equal(d.name, "test");
19 assert.equal(d.thicknessConst, 1e-3);
20 assert.equal(d.profileConst, 0);
21 assert.equal(d.hasInside, false);
2a1a6daDerive the losses from physical constants instead of a prescribed decay curveJeremy Magland 22 assert.equal(d.material.eta, 0, "no internal friction unless the script gives a loss factor");
23 assert.equal(d.material.thermal, null, "no thermoelastic damping without the thermal constants");
24 assert.equal(d.temperature, ROOM_TEMPERATURE);
5778036cymbalsim: scripted plate and cymbal designer with a modal linear model and a von Karman GPU modelJeremy Magland 25});
27test("missing fields and bad values are reported in the design's terms", () => {
28 assert.throws(() => compileDesign("x = 1;"), (e: unknown) => e instanceof DesignError && /struct named design/.test((e as Error).message));
29 assert.throws(() => compileDesign(base.replace("design.thickness = 1*mm;", "")), /design.thickness is required/);
30 assert.throws(() => compileDesign(base.replace("design.thickness = 1*mm;", "design.thickness = -1;")), /positive/);
31 assert.throws(() => compileDesign(base + "design.inside = 3;"), /design.inside must be a function handle/);
2a1a6daDerive the losses from physical constants instead of a prescribed decay curveJeremy Magland 32 assert.throws(() => compileDesign(base + "design.loss.T60 = 2;"), /design.loss is no longer used/);
5778036cymbalsim: scripted plate and cymbal designer with a modal linear model and a von Karman GPU modelJeremy Magland 33 assert.throws(() => compileDesign(base + "design.material.nu = 0.6;"), /below 0.5/);
34});
36test("script errors carry the line", () => {
37 assert.throws(() => compileDesign(base + "\nfoo(3);"), /line 8/);
38});
40test("functions defined at the bottom are hoisted and usable in handles", () => {
41 const src = base + `
42design.thickness = @(x, y, r, theta) 1*mm + 0.5*mm * bump(r);
43function v = bump(r)
44v = exp(-(r/0.05).^2);
45end
46`;
47 const d = compileDesign(src);
48 const pl = evaluatePlate(d, 0.01);
49 const g = pl.grid;
50 const center = pl.h[Math.floor(g.ny / 2) * g.nx + Math.floor(g.nx / 2)];
51 assert.ok(center > 1.3e-3 && center <= 1.5e-3, `center thickness ${center}`);
52 assert.ok(pl.h[0] < 1.01e-3);
53});
55test("a non-vectorized handle is reported", () => {
56 const d = compileDesign(base + "design.thickness = @(x, y, r, theta) [1 2 3]*mm;");
57 assert.throws(() => evaluatePlate(d, 0.01), /elementwise/);
58});
60test("a disc with a hole is masked at the cell centers", () => {
61 const d = compileDesign(base + "design.bounds.width = 0.2; design.bounds.height = 0.2; design.inside = @(x, y, r, theta) (r <= 0.1) & (r >= 0.02);");
62 const pl = evaluatePlate(d, 0.005);
63 const g = pl.grid;
64 const ci = Math.floor(g.nx / 2), cj = Math.floor(g.ny / 2);
65 assert.equal(insideNode(g, ci, cj), false, "hole center is outside");
66 assert.equal(insideNode(g, 1, 1), false, "bounding-box corner is outside");
67 assert.equal(insideNode(g, ci, Math.round(cj + 0.06 / g.hy)), true);
68 let area = 0;
69 for (let j = 0; j < g.ny; j++) for (let i = 0; i < g.nx; i++) area += nodeWeight(g, i, j) * g.hx * g.hy;
70 const exact = Math.PI * (0.1 ** 2 - 0.02 ** 2);
71 assert.ok(Math.abs(area / exact - 1) < 0.06, `area ratio ${area / exact}`);
72});
74test("meshgrid orientation: x varies along i and y along j", () => {
75 const d = compileDesign(base + "design.thickness = @(x, y, r, theta) 2*mm + x*mm*5 + y*mm*5;");
76 const pl = evaluatePlate(d, 0.02);
77 const g = pl.grid;
78 const at = (i: number, j: number) => pl.h[j * g.nx + i];
79 const expected = (i: number, j: number) => 2e-3 + nodeX(g, i) * 5e-3 + nodeY(g, j) * 5e-3;
80 assert.ok(Math.abs(at(3, 2) - expected(3, 2)) < 1e-12);
81 assert.ok(Math.abs(at(7, 5) - expected(7, 5)) < 1e-12);
82});
84test("the profile's mean plane is removed", () => {
85 const d = compileDesign(base + "design.profile = @(x, y, r, theta) 0.01 + 0.1*x - 0.05*y + 0.002*x.^2;");
86 const pl = evaluatePlate(d, 0.01);
87 const g = pl.grid;
88 let sw = 0, sx = 0, sy = 0;
89 for (let j = 0; j < g.ny; j++) for (let i = 0; i < g.nx; i++) {
90 if (!insideNode(g, i, j)) continue;
91 const w = pl.w0[j * g.nx + i] * nodeWeight(g, i, j);
92 sw += w; sx += w * nodeX(g, i); sy += w * nodeY(g, j);
93 }
94 assert.ok(Math.abs(sx) < 1e-9 && Math.abs(sy) < 1e-9 && Math.abs(sw) < 1e-9, `${sw} ${sx} ${sy}`);
95 assert.equal(isFlat(pl), false);
96 const f = new Float64Array(g.n);
97 for (let j = 0; j < g.ny; j++) for (let i = 0; i < g.nx; i++) f[j * g.nx + i] = 1 + 2 * nodeX(g, i) - 3 * nodeY(g, j);
98 removePlane(f, g);
99 assert.ok(f.every((v) => Math.abs(v) < 1e-12));
100});
2a1a6daDerive the losses from physical constants instead of a prescribed decay curveJeremy Magland 102test("the loss constants of the material are read and checked", () => {
103 const d = compileDesign(base + "design.material.lossFactor = 2e-4; design.material.alpha = 12e-6; design.material.cp = 460; design.material.k = 50; design.temperature = 300;");
104 assert.equal(d.material.eta, 2e-4);
105 assert.deepEqual(d.material.thermal, { alpha: 12e-6, cp: 460, k: 50 });
106 assert.equal(d.temperature, 300);
107 const pl = evaluatePlate(d, 0.02);
108 assert.equal(pl.temperature, 300);
109 assert.equal(pl.mat.eta, 2e-4);
110 assert.throws(() => compileDesign(base + "design.material.alpha = 12e-6;"), /all three thermal constants/);
111 assert.throws(() => compileDesign(base + "design.material.lossFactor = -1;"), /at least 0/);
112 assert.throws(() => compileDesign(base + "design.material.lossFactor = 0.5;"), /must be small/);
113 assert.throws(() => compileDesign(base + "design.temperature = 0;"), /at least 1/);
5778036cymbalsim: scripted plate and cymbal designer with a modal linear model and a von Karman GPU modelJeremy Magland 114});
116test("every preset compiles and evaluates on the default grid", () => {
117 for (const p of PRESETS) {
118 const d = compileDesign(p.source);
119 const pl = evaluatePlate(d, 0.008);
120 assert.ok(countInside(pl.grid) > 100, p.id);
121 assert.ok(massProperties(pl).mass > 0.1, p.id);
122 }
123});