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, massProperties } from "../src/params.ts"; import { ROOM_TEMPERATURE } from "../src/params.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.hasInside, false); assert.equal(d.material.eta, 0, "no internal friction unless the script gives a loss factor"); assert.equal(d.material.thermal, null, "no thermoelastic damping without the thermal constants"); assert.equal(d.temperature, ROOM_TEMPERATURE); }); 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;"), /design.loss is no longer used/); 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 loss constants of the material are read and checked", () => { 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;"); assert.equal(d.material.eta, 2e-4); assert.deepEqual(d.material.thermal, { alpha: 12e-6, cp: 460, k: 50 }); assert.equal(d.temperature, 300); const pl = evaluatePlate(d, 0.02); assert.equal(pl.temperature, 300); assert.equal(pl.mat.eta, 2e-4); assert.throws(() => compileDesign(base + "design.material.alpha = 12e-6;"), /all three thermal constants/); assert.throws(() => compileDesign(base + "design.material.lossFactor = -1;"), /at least 0/); assert.throws(() => compileDesign(base + "design.material.lossFactor = 0.5;"), /must be small/); assert.throws(() => compileDesign(base + "design.temperature = 0;"), /at least 1/); }); 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); } });