/ proof-of-concept / cymbalsim
Sign in
proof-of-concept / cymbalsim
cymbalsim / tests / design.test.ts
165 lines · 8.0 KBBlameHistoryRaw
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";
5import { countInside, insideNode, nodeX, nodeY, nodeWeight, standRates, massProperties } from "../src/params.ts";
6import { t60At, decayRate, fitSigma, fittedRate, LN1000, constantLoss, lossFromFunction, lossFromTable, LOSS_FREQS } from "../src/loss.ts";
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.stand, null);
22 assert.equal(d.hasInside, false);
23 assert.ok(d.lossConst);
24 assert.ok(Math.abs(t60At(d.lossConst!, 100) / (4 / 1.05) - 1) < 1e-3);
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/);
32 assert.throws(() => compileDesign(base + "design.loss.T60 = -2;"), /positive decay time/);
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});
102test("the stand: pads sum to the annulus stiffness, preloads carry the weight", () => {
103 const d = compileDesign(base + `
104design.bounds.width = 0.3; design.bounds.height = 0.3;
105design.inside = @(x, y, r, theta) (r <= 0.15) & (r >= 0.0065);
106design.stand.pad.innerRadius = 7*mm; design.stand.pad.outerRadius = 20*mm; design.stand.pad.thickness = 10*mm;
107design.stand.pad.E = 1e6; design.stand.pad.lossFactor = 0.2; design.stand.preload = 0.5*mm;
108`);
109 assert.ok(d.stand && d.stand.topPad);
110 const pl = evaluatePlate(d, 0.005);
111 const sr = standRates(pl)!;
112 const annulus = Math.PI * (0.02 ** 2 - 0.007 ** 2);
113 const kPad = (1e6 / 0.01) * annulus;
114 assert.ok(Math.abs(sr.totalKw / (2 * kPad) - 1) < 1e-9, `total ${sr.totalKw} vs ${2 * kPad}`);
115 const { mass } = massProperties(pl);
116 assert.ok(Math.abs(sr.preloadBottom - (0.5e-3 + (mass * 9.81) / kPad)) < 1e-12);
117 assert.equal(sr.preloadTop, 0.5e-3);
118 assert.ok(Math.abs(sr.bounceHz - Math.sqrt(sr.totalKw / mass) / (2 * Math.PI)) < 1e-9);
119 const d2 = compileDesign(base + "design.stand.topPad = false;");
120 const pl2 = evaluatePlate(d2, 0.01);
121 const sr2 = standRates(pl2)!;
122 assert.equal(sr2.preloadTop, 0);
123 assert.ok(sr2.totalKw > 0);
124});
126test("losses: constant, table, and handle forms", () => {
127 const c = compileDesign(base + "design.loss.T60 = 2.5;");
128 assert.ok(Math.abs(t60At(c.lossConst!, 3000) - 2.5) < 1e-12);
129 const t = compileDesign(base + "design.loss.T60 = [100 4; 10000 1];");
130 assert.ok(Math.abs(t60At(t.lossConst!, 100) / 4 - 1) < 1e-2);
131 assert.ok(Math.abs(t60At(t.lossConst!, 1000) / 2 - 1) < 1e-3, "log-log midpoint");
132 assert.ok(Math.abs(t60At(t.lossConst!, 20) / 4 - 1) < 1e-9, "clamped below");
133 const h = compileDesign(base + "design.loss.T60 = @(f) 3 ./ (1 + f/1000);");
134 assert.equal(h.lossConst, null);
135 const pl = evaluatePlate(h, 0.02);
136 assert.ok(Math.abs(t60At(pl.loss, 1000) / 1.5 - 1) < 1e-3);
137 assert.equal(pl.loss.freqs.length, LOSS_FREQS.length);
138 assert.ok(Math.abs(decayRate(pl.loss, 1000) / (LN1000 / 1.5) - 1) < 1e-3);
139 const bad = compileDesign(base + "design.loss.T60 = @(f) f - 5000;");
140 assert.throws(() => evaluatePlate(bad, 0.02), /must be positive/);
141});
143test("the sigma fit reproduces a pure sigma0 + sigma1 k^2 curve", () => {
144 const mat = { E: 200e9, nu: 0.3, rho: 7850 }, h = 1e-3;
145 const kappa = Math.sqrt((mat.E * h ** 3) / (12 * (1 - mat.nu ** 2)) / (mat.rho * h));
146 const s0 = 1.2, s1 = 4e-4;
147 const loss = lossFromFunction((f) => LN1000 / (s0 + (s1 * 2 * Math.PI * f) / kappa));
148 const fit = fitSigma(loss, mat, h);
149 assert.ok(Math.abs(fit.sigma0 / s0 - 1) < 1e-3 && Math.abs(fit.sigma1 / s1 - 1) < 1e-3, `${fit.sigma0} ${fit.sigma1}`);
150 assert.ok(Math.abs(fittedRate(fit, 2000) / (s0 + (s1 * 2 * Math.PI * 2000) / kappa) - 1) < 1e-3);
151 const flat = fitSigma(constantLoss(2), mat, h);
152 assert.ok(flat.sigma1 >= 0 && flat.sigma0 >= 0);
153 const tab = lossFromTable([1000, 100], [1, 2]);
154 // The kink of the table at 100 Hz is smoothed a little by the sampling on LOSS_FREQS.
155 assert.ok(Math.abs(t60At(tab, 100) / 2 - 1) < 1e-2, "table sorted by frequency");
156});
158test("every preset compiles and evaluates on the default grid", () => {
159 for (const p of PRESETS) {
160 const d = compileDesign(p.source);
161 const pl = evaluatePlate(d, 0.008);
162 assert.ok(countInside(pl.grid) > 100, p.id);
163 assert.ok(massProperties(pl).mass > 0.1, p.id);
164 }
165});
↑↓ move↵openescclose