/ proof-of-concept / steelsim41
Sign in
proof-of-concept / steelsim41
steelsim41 / tests / design.test.ts
188 lines · 9.6 KBCodeBlameHistory
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 1// Tests of the design script: the MATLAB-syntax helpers, compilation and
2// the checks, the evaluation on the grid, and the presets.
4import { test } from "node:test";
5import assert from "node:assert/strict";
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 6import { DesignError, compileDesign, evaluatePlate, profileStats, removePlane, runDesignScript } from "../src/design.ts";
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 7import { PRESETS } from "../src/presets.ts";
8import { countInside, insideNode, mountPatch, nodeWeight, nodeX, nodeY, thicknessRange } from "../src/params.ts";
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 10const minimal = `design.material.E = 170e9; design.material.nu = 0.29; design.material.rho = 7900;
11design.bounds.width = 0.3; design.bounds.height = 0.2;
12design.thickness = 1e-3;`;
14/** A scalar from the workspace. */
15function num(vars: Record<string, unknown>, name: string): number {
16 const v = vars[name] as number | { data: Float64Array };
17 return typeof v === "number" ? v : v.data[0];
19function data(vars: Record<string, unknown>, name: string): Float64Array {
20 return (vars[name] as { data: Float64Array }).data;
23test("a minimal script compiles with defaults filled in", () => {
24 const d = compileDesign(minimal);
25 assert.equal(d.name, "Untitled design");
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 26 assert.equal(d.hasInside, false);
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 27 assert.equal(d.mount, null);
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 28 assert.equal(d.thicknessConst, 1e-3);
29 assert.equal(d.profileConst, 0);
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 30 assert.deepEqual(d.loss, { sigma0: 1.15, sigma1: 5e-4, sigmaIn: 20 });
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 31 assert.ok(Math.abs(d.Lx - 0.3) < 1e-12 && Math.abs(d.Ly - 0.2) < 1e-12);
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 34test("the helpers are in scope, vectorized, and in SI", () => {
35 const v = runDesignScript(`
36a = 10*inch; b = 100*mm;
37t1 = lerp(2*mm, 1*mm, clamp(0.05/0.1, 0, 1));
38t2 = lerp(2*mm, 1*mm, clamp(0.2/0.1, 0, 1));
39p0 = dome(0, 0.05, 0.01) + bump(0, 0.02, 0.001) + bow(0, 0.254, 0.002);
40p1 = dome(0.06, 0.05, 0.01) + bump(0.06, 0.02, 0.001);
41s = [smoothstep(0, 1, -1), smoothstep(0, 1, 2), smoothstep(0, 1, 0.5)];
42f = circle(0.2, 0.03);
43[X, Y] = meshgrid(-0.1:0.05:0.1, -0.1:0.05:0.1);
44inside = f(X, Y, hypot(X, Y), atan2(Y, X));
45dd = dents(X, Y, [0 0.05], [0 0], 0.02, [1e-3 -1e-3]);
46u = seededrand(5, 3); u2 = seededrand(5, 3);
47`);
48 assert.ok(Math.abs(num(v, "a") - 0.254) < 1e-12 && Math.abs(num(v, "b") - 0.1) < 1e-12);
49 assert.ok(Math.abs(num(v, "t1") - 1.5e-3) < 1e-12);
50 assert.ok(Math.abs(num(v, "t2") - 1e-3) < 1e-12);
51 assert.ok(Math.abs(num(v, "p0") - (0.01 + 0.001 + 0.002)) < 1e-12);
52 assert.ok(Math.abs(num(v, "p1") - 0.001 * Math.exp(-9)) < 1e-15);
53 assert.deepEqual(Array.from(data(v, "s")), [0, 1, 0.5]);
54 const inside = data(v, "inside"); // 5 x 5, column-major; center at index 12, corner at 0, hole at the center
55 assert.equal(inside[12], 0, "the center lies in the hole");
56 assert.equal(inside[0], 0, "the corner lies outside");
57 assert.equal(inside[2], 1, "the point (-0.1, 0) lies on the rim");
58 const dd = data(v, "dd");
59 assert.ok(Math.abs(dd[12] - (1e-3 - 1e-3 * Math.exp(-(0.05 ** 2) / 0.02 ** 2))) < 1e-15, "two dents of opposite sign");
60 assert.deepEqual(Array.from(data(v, "u")), Array.from(data(v, "u2")), "seededrand is deterministic");
61 assert.ok(data(v, "u").every((x) => x >= 0 && x < 1));
64test("scripts that do not describe a plate are rejected with a message", () => {
65 const bad: [string, RegExp][] = [
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 66 [`x = 5;`, /struct named design/],
67 [`design = 5;`, /design must be a struct/],
68 [`x = (1 + ;`, /line 1/],
69 [`error('boom');`, /boom/],
70 [`design.bounds.width = 0.3; design.bounds.height = 0.2; design.thickness = 1e-3;`, /material/],
71 [`design.material.E = 170e9; design.material.nu = 0.6; design.material.rho = 7900; design.bounds.width = 0.3; design.bounds.height = 0.2; design.thickness = 1e-3;`, /nu/],
72 [`design.material.E = 170e9; design.material.nu = 0.3; design.material.rho = 7900; design.bounds.width = 0.3; design.thickness = 1e-3;`, /bounds.height/],
73 [`design.material.E = 170e9; design.material.nu = 0.3; design.material.rho = 7900; design.bounds.width = 0.3; design.bounds.height = 0.2;`, /thickness/],
74 [`${minimal} design.thickness = 'thin';`, /thickness must be a number or a function handle/],
75 [`${minimal} design.thickness = -1;`, /thickness must be positive/],
76 [`${minimal} design.inside = 5;`, /inside must be a function handle/],
77 [`${minimal} design.mount.stiffness = 1e4;`, /mount.radius/],
78 [`${minimal} design.loss = 3;`, /loss must be a struct/],
80 for (const [src, re] of bad) assert.throws(() => compileDesign(src), (e: unknown) => e instanceof DesignError && re.test(e.message), src);
81});
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 83test("evaluation rejects fields that are not positive, not finite, not vectorized, or that throw", () => {
84 const zero = compileDesign(`${minimal} design.thickness = @(x, y, r, theta) x;`);
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 85 assert.throws(() => evaluatePlate(zero, 0.02), /thickness must be positive/);
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 86 const nan = compileDesign(`${minimal} design.profile = @(x, y, r, theta) 0*x./0;`);
87 assert.throws(() => evaluatePlate(nan, 0.02), /profile is NaN/);
88 const shape = compileDesign(`${minimal} design.profile = @(x, y, r, theta) [1 2 3];`);
89 assert.throws(() => evaluatePlate(shape, 0.02), /profile returned a 1 x 3 array/);
90 const thrower = compileDesign(`${minimal} design.inside = @(x, y, r, theta) no_such_function(x);`);
91 assert.throws(() => evaluatePlate(thrower, 0.02), /inside: .*no_such_function/);
92 const empty = compileDesign(`${minimal} design.inside = @(x, y, r, theta) false & x;`);
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 93 assert.throws(() => evaluatePlate(empty, 0.02), /no grid cells/);
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 94 const scalarFn = evaluatePlate(compileDesign(`${minimal} design.thickness = @(x, y, r, theta) 2e-3;`), 0.02);
95 assert.equal(thicknessRange(scalarFn).min, 2e-3, "a handle returning a scalar is spread over the grid");
98test("a circle with a hole excludes the center and the corners", () => {
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 99 const d = compileDesign(`design.material.E = 170e9; design.material.nu = 0.3; design.material.rho = 7900;
100design.bounds.width = 0.2; design.bounds.height = 0.2; design.thickness = 1e-3;
101design.inside = circle(0.2, 0.03);`);
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 102 const pl = evaluatePlate(d, 0.01);
103 const g = pl.grid;
104 assert.equal(g.nx, 21);
105 const c = (g.nx - 1) / 2;
106 assert.ok(!insideNode(g, c, c), "center node lies in the hole");
107 assert.ok(!insideNode(g, 0, 0), "corner node lies outside the disc");
108 assert.ok(insideNode(g, c, 3), "a node on the radius belongs to the disc");
109 let weight = 0;
110 for (let j = 0; j < g.ny; j++) for (let i = 0; i < g.nx; i++) weight += nodeWeight(g, i, j);
111 const area = weight * g.hx * g.hy;
112 const exact = Math.PI * (0.1 ** 2 - 0.015 ** 2);
113 assert.ok(Math.abs(area / exact - 1) < 0.05, `the ${countInside(g)} nodes cover ${area} vs ${exact}`);
114 assert.equal(thicknessRange(pl).max, 1e-3);
115});
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 117test("fields are sampled at the right nodes (meshgrid orientation)", () => {
118 const d = compileDesign(`${minimal} design.thickness = @(x, y, r, theta) 10 + x + 10*y;`);
119 const pl = evaluatePlate(d, 0.05);
120 const g = pl.grid;
121 assert.ok(g.nx !== g.ny, "the grid is not square, so a transposed sampling would show");
122 for (let j = 0; j < g.ny; j++) for (let i = 0; i < g.nx; i++) {
123 const want = 10 + nodeX(g, i) + 10 * nodeY(g, j);
124 assert.ok(Math.abs(pl.h[j * g.nx + i] - want) < 1e-12, `node ${i}, ${j}: ${pl.h[j * g.nx + i]} vs ${want}`);
125 }
126});
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 128test("the profile is made plane-free and the mount patch is found", () => {
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 129 const d = compileDesign(`${minimal}
130design.profile = @(x, y, r, theta) 0.01 + 0.05*x - 0.02*y + 0.001*x.^2;
131design.mount.radius = 0.02; design.mount.stiffness = 1e4; design.mount.damping = 1;`);
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 132 const pl = evaluatePlate(d, 0.01);
133 const g = pl.grid;
134 let s = 0, sx = 0, sy = 0;
135 for (let j = 0; j < g.ny; j++) {
136 for (let i = 0; i < g.nx; i++) {
137 const wgt = nodeWeight(g, i, j), v = pl.w0[j * g.nx + i];
138 s += wgt * v; sx += wgt * nodeX(g, i) * v; sy += wgt * nodeY(g, j) * v;
139 }
140 }
141 assert.ok(Math.abs(s) < 1e-12 && Math.abs(sx) < 1e-12 && Math.abs(sy) < 1e-12, "mean plane removed");
142 const stats = profileStats(pl);
143 assert.ok(stats.peakToPeak > 0 && stats.peakToPeak < 1e-4, `peak to peak ${stats.peakToPeak}`);
144 const mp = mountPatch(pl)!;
145 assert.ok(mp.nodes.length >= 9 && mp.nodes.length <= 21, `mount patch has ${mp.nodes.length} nodes`);
146 assert.ok(mp.weight > 0);
147});
149test("removePlane leaves a plane-free field unchanged", () => {
150 const g = { nx: 7, ny: 5, hx: 0.05, hy: 0.05, n: 35 };
151 const f = new Float64Array(g.n);
152 for (let j = 0; j < g.ny; j++) for (let i = 0; i < g.nx; i++) f[j * g.nx + i] = nodeX(g, i) ** 2 - 0.0125;
153 const before = Float64Array.from(f);
154 removePlane(f, g);
155 // The parabola x^2 has a nonzero mean but no tilt; only the mean should move.
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 156 const shift = f[0] - before[0];
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 157 for (let k = 0; k < g.n; k++) assert.ok(Math.abs(f[k] - before[k] - shift) < 1e-12);
158});
160test("noise2d is deterministic with unit RMS", () => {
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 161 const v = runDesignScript(`
162a = noise2d(7, 0.05); b = noise2d(7, 0.05); c = noise2d(8, 0.05);
163[X, Y] = meshgrid(-0.5:0.01:0.49, -0.5:0.01:0.49);
164A = a(X, Y); B = b(X, Y); C = c(X, Y);
165same = isequal(A, B); different = ~isequal(A, C);
166rms = sqrt(mean(A(:).^2));
167`);
168 assert.equal(v.same, true);
169 assert.equal(v.different, true);
170 const rms = num(v, "rms");
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 171 assert.ok(Math.abs(rms - 1) < 0.2, `rms ${rms}`);
172});
174test("every preset compiles and evaluates on the default grid", () => {
175 for (const p of PRESETS) {
176 const d = compileDesign(p.source);
177 const pl = evaluatePlate(d, 0.008);
178 const hr = thicknessRange(pl);
179 assert.ok(hr.min > 0 && hr.max < 5e-3, `${p.id}: thickness ${hr.min}..${hr.max}`);
180 assert.ok(countInside(pl.grid) > 100, `${p.id}: ${countInside(pl.grid)} nodes`);
181 const stats = profileStats(pl);
182 assert.ok(stats.peakToPeak < 0.05, `${p.id}: peak to peak ${stats.peakToPeak}`);
183 if (pl.mount) {
184 const mp = mountPatch(pl)!;
185 assert.ok(mp.nodes.length > 1, `${p.id}: mount patch has ${mp.nodes.length} nodes`);
186 }
187 }
188});
↑↓ move↵openescclose