import { test } from "node:test"; import assert from "node:assert/strict"; import { compileDesign, evaluatePlate, DesignError, removePlane, isFlat } from "../src/design.ts"; import { PRESETS } from "../src/presets.ts"; import { countInside, insideNode, nodeX, nodeY, nodeWeight, standRates, massProperties } from "../src/params.ts"; import { t60At, decayRate, fitSigma, fittedRate, LN1000, constantLoss, lossFromFunction, lossFromTable, LOSS_FREQS } from "../src/loss.ts"; const base = ` mm = 1e-3; design.name = 'test'; design.material.E = 200e9; design.material.nu = 0.3; design.material.rho = 7850; design.bounds.width = 0.2; design.bounds.height = 0.16; design.thickness = 1*mm; `; test("a minimal script compiles with defaults", () => { const d = compileDesign(base); assert.equal(d.name, "test"); assert.equal(d.thicknessConst, 1e-3); assert.equal(d.profileConst, 0); assert.equal(d.stand, null); assert.equal(d.hasInside, false); assert.ok(d.lossConst); assert.ok(Math.abs(t60At(d.lossConst!, 100) / (4 / 1.05) - 1) < 1e-3); }); test("missing fields and bad values are reported in the design's terms", () => { assert.throws(() => compileDesign("x = 1;"), (e: unknown) => e instanceof DesignError && /struct named design/.test((e as Error).message)); assert.throws(() => compileDesign(base.replace("design.thickness = 1*mm;", "")), /design.thickness is required/); assert.throws(() => compileDesign(base.replace("design.thickness = 1*mm;", "design.thickness = -1;")), /positive/); assert.throws(() => compileDesign(base + "design.inside = 3;"), /design.inside must be a function handle/); assert.throws(() => compileDesign(base + "design.loss.T60 = -2;"), /positive decay time/); assert.throws(() => compileDesign(base + "design.material.nu = 0.6;"), /below 0.5/); }); test("script errors carry the line", () => { assert.throws(() => compileDesign(base + "\nfoo(3);"), /line 8/); }); test("functions defined at the bottom are hoisted and usable in handles", () => { const src = base + ` design.thickness = @(x, y, r, theta) 1*mm + 0.5*mm * bump(r); function v = bump(r) v = exp(-(r/0.05).^2); end `; const d = compileDesign(src); const pl = evaluatePlate(d, 0.01); const g = pl.grid; const center = pl.h[Math.floor(g.ny / 2) * g.nx + Math.floor(g.nx / 2)]; assert.ok(center > 1.3e-3 && center <= 1.5e-3, `center thickness ${center}`); assert.ok(pl.h[0] < 1.01e-3); }); test("a non-vectorized handle is reported", () => { const d = compileDesign(base + "design.thickness = @(x, y, r, theta) [1 2 3]*mm;"); assert.throws(() => evaluatePlate(d, 0.01), /elementwise/); }); test("a disc with a hole is masked at the cell centers", () => { 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);"); const pl = evaluatePlate(d, 0.005); const g = pl.grid; const ci = Math.floor(g.nx / 2), cj = Math.floor(g.ny / 2); assert.equal(insideNode(g, ci, cj), false, "hole center is outside"); assert.equal(insideNode(g, 1, 1), false, "bounding-box corner is outside"); assert.equal(insideNode(g, ci, Math.round(cj + 0.06 / g.hy)), true); let area = 0; 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; const exact = Math.PI * (0.1 ** 2 - 0.02 ** 2); assert.ok(Math.abs(area / exact - 1) < 0.06, `area ratio ${area / exact}`); }); test("meshgrid orientation: x varies along i and y along j", () => { const d = compileDesign(base + "design.thickness = @(x, y, r, theta) 2*mm + x*mm*5 + y*mm*5;"); const pl = evaluatePlate(d, 0.02); const g = pl.grid; const at = (i: number, j: number) => pl.h[j * g.nx + i]; const expected = (i: number, j: number) => 2e-3 + nodeX(g, i) * 5e-3 + nodeY(g, j) * 5e-3; assert.ok(Math.abs(at(3, 2) - expected(3, 2)) < 1e-12); assert.ok(Math.abs(at(7, 5) - expected(7, 5)) < 1e-12); }); test("the profile's mean plane is removed", () => { const d = compileDesign(base + "design.profile = @(x, y, r, theta) 0.01 + 0.1*x - 0.05*y + 0.002*x.^2;"); const pl = evaluatePlate(d, 0.01); const g = pl.grid; let sw = 0, sx = 0, sy = 0; for (let j = 0; j < g.ny; j++) for (let i = 0; i < g.nx; i++) { if (!insideNode(g, i, j)) continue; const w = pl.w0[j * g.nx + i] * nodeWeight(g, i, j); sw += w; sx += w * nodeX(g, i); sy += w * nodeY(g, j); } assert.ok(Math.abs(sx) < 1e-9 && Math.abs(sy) < 1e-9 && Math.abs(sw) < 1e-9, `${sw} ${sx} ${sy}`); assert.equal(isFlat(pl), false); const f = new Float64Array(g.n); 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); removePlane(f, g); assert.ok(f.every((v) => Math.abs(v) < 1e-12)); }); test("the stand: pads sum to the annulus stiffness, preloads carry the weight", () => { const d = compileDesign(base + ` design.bounds.width = 0.3; design.bounds.height = 0.3; design.inside = @(x, y, r, theta) (r <= 0.15) & (r >= 0.0065); design.stand.pad.innerRadius = 7*mm; design.stand.pad.outerRadius = 20*mm; design.stand.pad.thickness = 10*mm; design.stand.pad.E = 1e6; design.stand.pad.lossFactor = 0.2; design.stand.preload = 0.5*mm; `); assert.ok(d.stand && d.stand.topPad); const pl = evaluatePlate(d, 0.005); const sr = standRates(pl)!; const annulus = Math.PI * (0.02 ** 2 - 0.007 ** 2); const kPad = (1e6 / 0.01) * annulus; assert.ok(Math.abs(sr.totalKw / (2 * kPad) - 1) < 1e-9, `total ${sr.totalKw} vs ${2 * kPad}`); const { mass } = massProperties(pl); assert.ok(Math.abs(sr.preloadBottom - (0.5e-3 + (mass * 9.81) / kPad)) < 1e-12); assert.equal(sr.preloadTop, 0.5e-3); assert.ok(Math.abs(sr.bounceHz - Math.sqrt(sr.totalKw / mass) / (2 * Math.PI)) < 1e-9); const d2 = compileDesign(base + "design.stand.topPad = false;"); const pl2 = evaluatePlate(d2, 0.01); const sr2 = standRates(pl2)!; assert.equal(sr2.preloadTop, 0); assert.ok(sr2.totalKw > 0); }); test("losses: constant, table, and handle forms", () => { const c = compileDesign(base + "design.loss.T60 = 2.5;"); assert.ok(Math.abs(t60At(c.lossConst!, 3000) - 2.5) < 1e-12); const t = compileDesign(base + "design.loss.T60 = [100 4; 10000 1];"); assert.ok(Math.abs(t60At(t.lossConst!, 100) / 4 - 1) < 1e-2); assert.ok(Math.abs(t60At(t.lossConst!, 1000) / 2 - 1) < 1e-3, "log-log midpoint"); assert.ok(Math.abs(t60At(t.lossConst!, 20) / 4 - 1) < 1e-9, "clamped below"); const h = compileDesign(base + "design.loss.T60 = @(f) 3 ./ (1 + f/1000);"); assert.equal(h.lossConst, null); const pl = evaluatePlate(h, 0.02); assert.ok(Math.abs(t60At(pl.loss, 1000) / 1.5 - 1) < 1e-3); assert.equal(pl.loss.freqs.length, LOSS_FREQS.length); assert.ok(Math.abs(decayRate(pl.loss, 1000) / (LN1000 / 1.5) - 1) < 1e-3); const bad = compileDesign(base + "design.loss.T60 = @(f) f - 5000;"); assert.throws(() => evaluatePlate(bad, 0.02), /must be positive/); }); test("the sigma fit reproduces a pure sigma0 + sigma1 k^2 curve", () => { const mat = { E: 200e9, nu: 0.3, rho: 7850 }, h = 1e-3; const kappa = Math.sqrt((mat.E * h ** 3) / (12 * (1 - mat.nu ** 2)) / (mat.rho * h)); const s0 = 1.2, s1 = 4e-4; const loss = lossFromFunction((f) => LN1000 / (s0 + (s1 * 2 * Math.PI * f) / kappa)); const fit = fitSigma(loss, mat, h); assert.ok(Math.abs(fit.sigma0 / s0 - 1) < 1e-3 && Math.abs(fit.sigma1 / s1 - 1) < 1e-3, `${fit.sigma0} ${fit.sigma1}`); assert.ok(Math.abs(fittedRate(fit, 2000) / (s0 + (s1 * 2 * Math.PI * 2000) / kappa) - 1) < 1e-3); const flat = fitSigma(constantLoss(2), mat, h); assert.ok(flat.sigma1 >= 0 && flat.sigma0 >= 0); const tab = lossFromTable([1000, 100], [1, 2]); // The kink of the table at 100 Hz is smoothed a little by the sampling on LOSS_FREQS. assert.ok(Math.abs(t60At(tab, 100) / 2 - 1) < 1e-2, "table sorted by frequency"); }); test("every preset compiles and evaluates on the default grid", () => { for (const p of PRESETS) { const d = compileDesign(p.source); const pl = evaluatePlate(d, 0.008); assert.ok(countInside(pl.grid) > 100, p.id); assert.ok(massProperties(pl).mass > 0.1, p.id); } });