/ proof-of-concept / steelsim31
Sign in
proof-of-concept / steelsim31
steelsim31 / tests / shape.test.ts
139 lines · 6.9 KBBlameHistoryRaw
1// Tests of the initial-shape features (no GPU needed): the text encoding, the
2// removal of the mean plane, the geometry of the individual features, and
3// the random configurations.
5import { test } from "node:test";
6import assert from "node:assert/strict";
7import { defaultParams, makeGrid, nodeWeight, nodeX, nodeY, type Grid, type PhysParams } from "../src/params.ts";
8import { encodeFeatures, initialShape, parseFeatures, randomFeatures, shapeStats, type ShapeFeature } from "../src/shape.ts";
10function withFeatures(features: ShapeFeature[], extra: Partial<PhysParams> = {}): PhysParams {
11 return { ...defaultParams, ...extra, features };
14/** Mass-weighted mean, x-moment, and y-moment of a field over the sheet, normalized by the total weight. */
15function planeMoments(w0: ArrayLike<number>, g: Grid): [number, number, number] {
16 let sw = 0, s = 0, sx = 0, sy = 0;
17 for (let j = 0; j < g.ny; j++) {
18 for (let i = 0; i < g.nx; i++) {
19 const wgt = nodeWeight(g, i, j);
20 const v = w0[j * g.nx + i];
21 sw += wgt; s += wgt * v; sx += wgt * nodeX(g, i) * v; sy += wgt * nodeY(g, j) * v;
22 }
23 }
24 return [s / sw, sx / sw, sy / sw];
27const everything = "bow:x,0.003,0.4;bow:y,-0.001,0;twist:0.0008;dent:0.3,0.7,0.03,0.0015;crease:0.6,0.4,25,-1.5,0.015;noise:11,0.0005,0.05";
29test("features encode to text and back", () => {
30 const f = parseFeatures(everything);
31 assert.ok(f);
32 assert.equal(f.length, 6);
33 assert.equal(encodeFeatures(f), everything);
34 assert.deepEqual(parseFeatures("none"), []);
35 assert.deepEqual(parseFeatures(""), []);
36 assert.equal(parseFeatures("nonsense"), null);
37 // An unreadable entry is skipped, the rest is kept.
38 assert.equal(encodeFeatures(parseFeatures("bow:z,1,0;twist:0.001")!), "twist:0.001");
39 assert.equal(encodeFeatures(defaultParams.features), "bow:x,0.003,0");
40});
42test("the mean plane is removed on the rectangle and on the ellipse", () => {
43 for (const shape of ["rectangle", "ellipse"] as const) {
44 const p = withFeatures(parseFeatures(everything)!, { shape });
45 const g = makeGrid(p);
46 const w0 = initialShape(p, g);
47 const [m, mx, my] = planeMoments(w0, g);
48 const { peakToPeak } = shapeStats(w0, g);
49 assert.ok(peakToPeak > 3e-3, `${shape}: the shape is not flat (${peakToPeak})`);
50 assert.ok(Math.abs(m) < 1e-6 * peakToPeak, `${shape}: mean ${m}`);
51 assert.ok(Math.abs(mx) < 1e-6 * peakToPeak * p.Lx, `${shape}: x tilt ${mx}`);
52 assert.ok(Math.abs(my) < 1e-6 * peakToPeak * p.Ly, `${shape}: y tilt ${my}`);
53 // Nodes outside the sheet carry no shape.
54 for (let j = 0; j < g.ny; j++) for (let i = 0; i < g.nx; i++) if (nodeWeight(g, i, j) === 0) assert.equal(w0[j * g.nx + i], 0);
55 }
56});
58test("a bow across the width sags by the given amount and is flat along the height", () => {
59 const p = withFeatures([{ type: "bow", axis: "x", sag: 3e-3, offset: 0 }]);
60 const g = makeGrid(p);
61 const w0 = initialShape(p, g);
62 const mid = Math.floor(g.ny / 2), ci = (g.nx - 1) / 2;
63 const center = w0[mid * g.nx + Math.floor(ci)], edge = w0[mid * g.nx];
64 assert.ok(Math.abs(center - edge - 3e-3 * (1 - ((Math.floor(ci) - ci) * 2 / (g.nx - 1)) ** 2)) < 1e-9, "sag");
65 for (let j = 0; j < g.ny; j++) assert.ok(Math.abs(w0[j * g.nx + 3] - w0[3]) < 1e-12, "cylindrical");
66 // The apex offset moves the crest toward the right edge.
67 const q = withFeatures([{ type: "bow", axis: "x", sag: 3e-3, offset: 0.5 }]);
68 const v = initialShape(q, g);
69 let best = 0;
70 for (let i = 0; i < g.nx; i++) if (v[mid * g.nx + i] > v[mid * g.nx + best]) best = i;
71 assert.ok(best > ci, "crest right of center");
72});
74test("a crease tilts the two sides by half the fold angle", () => {
75 const fold = 2;
76 const p = withFeatures([{ type: "crease", x: 0.5, y: 0.5, angle: 90, fold, width: 0 }]);
77 const g = makeGrid(p);
78 const w0 = initialShape(p, g);
79 const { maxSlope } = shapeStats(w0, g);
80 const expected = Math.tan((fold * Math.PI) / 360);
81 assert.ok(Math.abs(maxSlope - expected) < 0.05 * expected, `slope ${maxSlope} vs ${expected}`);
82 // A positive fold is a ridge: the line is the highest part after the plane is removed.
83 const mid = Math.floor(g.ny / 2);
84 const onLine = w0[mid * g.nx + Math.floor((g.nx - 1) / 2)];
85 assert.ok(onLine > w0[mid * g.nx] && onLine > w0[mid * g.nx + g.nx - 1], "ridge");
86});
88test("a dent has the requested height and radius", () => {
89 const p = withFeatures([{ type: "dent", x: 0.5, y: 0.5, radius: 0.04, height: 2e-3 }], { spacing: 0.004 });
90 const g = makeGrid(p);
91 const w0 = initialShape(p, g);
92 const ci = Math.floor((g.nx - 1) / 2), cj = Math.floor((g.ny - 1) / 2);
93 const center = w0[cj * g.nx + ci];
94 const far = w0[cj * g.nx];
95 assert.ok(Math.abs(center - far - 2e-3) < 1e-4, `height ${center - far}`);
96 const atRadius = w0[cj * g.nx + ci + Math.round(0.04 / g.hx)];
97 assert.ok(Math.abs((atRadius - far) / (center - far) - Math.exp(-1)) < 0.03, "1/e at the radius");
98});
100test("the random field has the requested RMS and depends on the seed but not on the grid", () => {
101 const p = withFeatures([{ type: "noise", seed: 5, rms: 1e-3, length: 0.06 }]);
102 const g = makeGrid(p);
103 const w0 = initialShape(p, g);
104 let sw = 0, s2 = 0;
105 for (let j = 0; j < g.ny; j++) for (let i = 0; i < g.nx; i++) { const wgt = nodeWeight(g, i, j); sw += wgt; s2 += wgt * w0[j * g.nx + i] ** 2; }
106 const rms = Math.sqrt(s2 / sw);
107 // The plane removal takes a little of the variance away.
108 assert.ok(rms > 0.8e-3 && rms <= 1.0e-3 + 1e-9, `rms ${rms}`);
109 const other = initialShape(withFeatures([{ type: "noise", seed: 6, rms: 1e-3, length: 0.06 }]), g);
110 let diff = 0;
111 for (let k = 0; k < g.n; k++) diff = Math.max(diff, Math.abs(other[k] - w0[k]));
112 assert.ok(diff > 0.5e-3, "another seed gives another field");
113 // The same seed on a finer grid gives the same field at the shared nodes.
114 const fine = makeGrid({ ...p, spacing: 0.004 });
115 const wf = initialShape(p, fine);
116 assert.equal(fine.nx, 2 * g.nx - 1);
117 let err = 0;
118 for (let j = 0; j < g.ny; j++) for (let i = 0; i < g.nx; i++) err = Math.max(err, Math.abs(wf[2 * j * fine.nx + 2 * i] - w0[j * g.nx + i]));
119 assert.ok(err < 2e-5, `grid dependence ${err}`);
120});
122test("random configurations are readable and stay within the shallow-shell range", () => {
123 let seed = 12345;
124 const rnd = () => ((seed = (seed * 1664525 + 1013904223) >>> 0) / 4294967296);
125 const types = new Set<string>();
126 for (let trial = 0; trial < 40; trial++) {
127 const features = randomFeatures(defaultParams, 3e-3, rnd);
128 for (const f of features) types.add(f.type);
129 assert.equal(features[0].type, "bow");
130 const text = encodeFeatures(features);
131 assert.equal(encodeFeatures(parseFeatures(text)!), text);
132 const p = withFeatures(features);
133 const g = makeGrid(p);
134 const { peakToPeak, maxSlope } = shapeStats(initialShape(p, g), g);
135 assert.ok(peakToPeak > 0.5e-3 && peakToPeak < 15e-3, `peak to peak ${peakToPeak}`);
136 assert.ok(maxSlope < 0.1, `slope ${maxSlope}`);
137 }
138 for (const t of ["bow", "twist", "dent", "crease", "noise"]) assert.ok(types.has(t), `${t} was drawn`);
139});
↑↓ move↵openescclose