/ proof-of-concept / steelsim41
Sign in
proof-of-concept / steelsim41
steelsim41 / src / design.ts
309 lines · 14.4 KBCodeBlameHistory
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 1// The cymbal design: a MATLAB-syntax script, run by numbl, that fills in a
2// struct named `design` with the material, the bounding box, the outline,
3// the thickness field, the stress-free profile, the mount, and the losses.
4// The outline, the thickness, and the profile are function handles
5// @(x, y, r, theta), vectorized over grid matrices (or plain numbers).
6// compileDesign runs the script and checks the struct; evaluatePlate runs it
7// again with a short trailer that samples the fields on meshgrid matrices of
8// the requested spacing, inside numbl, and reads the matrices back.
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 10// Lengths are meters throughout; `inch` and `mm` are helper functions. The
11// helpers a script may call are listed in mhelpers.ts.
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 13import { executeCode, type RuntimeValue, type RuntimeTensor, type RuntimeStruct } from "numbl";
14import { HELPER_FILES } from "./mhelpers.ts";
16 type Grid,
17 type Loss,
18 type Material,
19 type Mount,
20 type Plate,
21 insideNode,
22 makeGrid,
23 nodeWeight,
24 nodeX,
25 nodeY,
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 26 withCellMask,
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 29/** A compiled design: the checked contents of the `design` struct, plus the script that produced it. */
31 name: string;
32 material: Material;
33 Lx: number;
34 Ly: number;
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 35 /** Whether the script gave an outline; without one the plate is the whole bounding box. */
36 hasInside: boolean;
37 /** The thickness when it is a plain number; null for a function. */
38 thicknessConst: number | null;
39 /** The profile when it is a plain number (or absent); null for a function. */
40 profileConst: number | null;
42 loss: Loss;
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 43 source: string;
46export const DEFAULT_LOSS: Loss = { sigma0: 1.15, sigma1: 5e-4, sigmaIn: 20 };
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 48/** Message of an error thrown by the script or by the checks. */
49export class DesignError extends Error {}
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 51const FILE_NAME = "design.m";
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 53/** Runs a design script (with the helpers in scope) and returns its workspace variables. */
54export function runDesignScript(source: string): Record<string, RuntimeValue> {
55 try {
56 return executeCode(source, { displayResults: false, onOutput: () => {} }, HELPER_FILES, FILE_NAME).variableValues;
57 } catch (e) {
58 throw new DesignError(describeError(e, source));
59 }
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 62/** The message of a numbl error (a RuntimeError or a parser SyntaxError), with its line when it lies in the user's script. */
63function describeError(e: unknown, source: string): string {
64 if (!(e instanceof Error)) return String(e);
65 const { line, file } = e as { line?: unknown; file?: unknown };
66 const lines = source.split("\n").length;
67 // A line is only meaningful when the error arose in the script itself, not in a helper .m file.
68 const inScript = file === undefined || file === null || file === FILE_NAME;
69 const where = inScript && typeof line === "number" && line >= 1 && line <= lines ? `line ${line}: ` : "";
70 return `${where}${e.message}`;
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 73// ---------- Reading runtime values ----------
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 75function isStruct(v: RuntimeValue | undefined): v is RuntimeStruct {
76 return typeof v === "object" && v !== null && (v as { kind?: string }).kind === "struct";
78function isTensor(v: RuntimeValue | undefined): v is RuntimeTensor {
79 return typeof v === "object" && v !== null && (v as { kind?: string }).kind === "tensor";
81function isFunction(v: RuntimeValue | undefined): boolean {
82 return typeof v === "object" && v !== null && (v as { kind?: string }).kind === "function";
84function isEmpty(v: RuntimeValue | undefined): boolean {
85 return v === undefined || (isTensor(v) && v.data.length === 0);
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 88/** A scalar number from a runtime value, or null when it is not one. */
89function scalar(v: RuntimeValue | undefined): number | null {
90 if (typeof v === "number") return v;
91 if (typeof v === "boolean") return v ? 1 : 0;
92 if (isTensor(v) && v.data.length === 1 && (!v.imag || v.imag[0] === 0)) return v.data[0];
93 return null;
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 96function num(v: RuntimeValue | undefined, what: string, min = -Infinity): number {
97 const s = scalar(v);
98 if (s === null || !isFinite(s)) throw new DesignError(`${what} must be a finite number`);
99 if (s < min) throw new DesignError(`${what} must be at least ${min}`);
100 return s;
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 103function field(s: RuntimeStruct, name: string): RuntimeValue | undefined {
104 return s.fields.get(name);
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 107function text(v: RuntimeValue | undefined): string | null {
108 if (typeof v === "string") return v;
109 if (typeof v === "object" && v !== null && (v as { kind?: string }).kind === "char") return (v as { value: string }).value;
110 return null;
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 113// ---------- Compilation ----------
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 116 * Runs the script and checks the `design` struct it leaves in the
117 * workspace. The field functions are not called here; evaluatePlate samples
118 * them on the grid.
120export function compileDesign(source: string): Design {
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 121 const vars = runDesignScript(source);
122 const d = vars.design;
123 if (d === undefined) throw new DesignError("the script must fill in a struct named design (design.material, design.bounds, design.thickness, ...)");
124 if (!isStruct(d)) throw new DesignError("design must be a struct");
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 126 const nameV = field(d, "name");
127 const name = (nameV !== undefined ? text(nameV) : null)?.trim() || "Untitled design";
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 129 const m = field(d, "material");
130 if (!isStruct(m)) throw new DesignError("design.material must be a struct with fields E, nu, rho");
131 const material: Material = { E: num(field(m, "E"), "design.material.E", 1e6), nu: num(field(m, "nu"), "design.material.nu", 0), rho: num(field(m, "rho"), "design.material.rho", 1) };
132 if (material.nu >= 0.5) throw new DesignError("design.material.nu must be below 0.5");
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 134 const b = field(d, "bounds");
135 if (!isStruct(b)) throw new DesignError("design.bounds must be a struct with fields width, height");
136 const Lx = num(field(b, "width"), "design.bounds.width", 1e-3), Ly = num(field(b, "height"), "design.bounds.height", 1e-3);
138 const inside = field(d, "inside");
139 let hasInside = false;
140 if (!isEmpty(inside)) {
141 if (!isFunction(inside)) throw new DesignError("design.inside must be a function handle @(x, y, r, theta) returning true inside the plate, for example circle(D, hole)");
142 hasInside = true;
143 }
145 const th = field(d, "thickness");
146 if (th === undefined) throw new DesignError("design.thickness is required (a number in meters, or a function handle @(x, y, r, theta))");
147 let thicknessConst: number | null = null;
148 if (!isFunction(th)) {
149 thicknessConst = scalar(th);
150 if (thicknessConst === null || !isFinite(thicknessConst)) throw new DesignError("design.thickness must be a number or a function handle @(x, y, r, theta)");
151 if (thicknessConst <= 0) throw new DesignError("design.thickness must be positive");
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 154 const pr = field(d, "profile");
155 let profileConst: number | null = 0;
156 if (pr !== undefined && !isEmpty(pr)) {
157 if (isFunction(pr)) profileConst = null;
158 else {
159 profileConst = scalar(pr);
160 if (profileConst === null || !isFinite(profileConst)) throw new DesignError("design.profile must be a number or a function handle @(x, y, r, theta)");
161 }
162 }
164 let mount: Mount | null = null;
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 165 const mo = field(d, "mount");
166 if (mo !== undefined && !isEmpty(mo) && mo !== false) {
167 if (!isStruct(mo)) throw new DesignError("design.mount must be a struct with fields radius, stiffness, damping (and optionally x, y), or [] for a free plate");
168 const opt = (k: string, what: string, dflt: number, min = -Infinity) => (field(mo, k) === undefined ? dflt : num(field(mo, k), what, min));
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 170 x: opt("x", "design.mount.x", 0),
171 y: opt("y", "design.mount.y", 0),
172 radius: num(field(mo, "radius"), "design.mount.radius", 0),
173 stiffness: num(field(mo, "stiffness"), "design.mount.stiffness", 0),
174 damping: opt("damping", "design.mount.damping", 0, 0),
176 }
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 178 const lo = field(d, "loss");
179 const loss: Loss = { ...DEFAULT_LOSS };
180 if (lo !== undefined && !isEmpty(lo)) {
181 if (!isStruct(lo)) throw new DesignError("design.loss must be a struct with fields sigma0, sigma1, sigmaIn");
182 for (const k of ["sigma0", "sigma1", "sigmaIn"] as const) if (field(lo, k) !== undefined) loss[k] = num(field(lo, k), `design.loss.${k}`, 0);
183 }
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 185 return { name, material, Lx, Ly, hasInside, thicknessConst, profileConst, mount, loss, source };
188// ---------- Evaluation on a grid ----------
190/** Subtracts the weighted least-squares plane a + b x + c y from f over the plate nodes. */
191export function removePlane(f: Float64Array, g: Grid): void {
192 let sw = 0, sx = 0, sy = 0, sxx = 0, sxy = 0, syy = 0, sf = 0, sxf = 0, syf = 0;
193 for (let j = 0; j < g.ny; j++) {
194 for (let i = 0; i < g.nx; i++) {
195 const wgt = nodeWeight(g, i, j);
196 if (wgt === 0) continue;
197 const x = nodeX(g, i), y = nodeY(g, j), v = f[j * g.nx + i];
198 sw += wgt; sx += wgt * x; sy += wgt * y;
199 sxx += wgt * x * x; sxy += wgt * x * y; syy += wgt * y * y;
200 sf += wgt * v; sxf += wgt * x * v; syf += wgt * y * v;
201 }
202 }
203 if (sw === 0) return;
204 // Normal equations of the 3-parameter fit, solved by Cramer's rule.
205 const det = sw * (sxx * syy - sxy * sxy) - sx * (sx * syy - sxy * sy) + sy * (sx * sxy - sxx * sy);
206 if (Math.abs(det) < 1e-300) return;
207 const a = (sf * (sxx * syy - sxy * sxy) - sx * (sxf * syy - sxy * syf) + sy * (sxf * sxy - sxx * syf)) / det;
208 const b = (sw * (sxf * syy - sxy * syf) - sf * (sx * syy - sxy * sy) + sy * (sx * syf - sxf * sy)) / det;
209 const c = (sw * (sxx * syf - sxf * sxy) - sx * (sx * syf - sxf * sy) + sf * (sx * sxy - sxx * sy)) / det;
210 for (let j = 0; j < g.ny; j++) for (let i = 0; i < g.nx; i++) f[j * g.nx + i] -= a + b * nodeX(g, i) + c * nodeY(g, j);
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 213/** The trailer appended to the script to sample the fields on the grid (all names start with steelsim_). */
214function trailer(d: Design, g: Grid): string {
215 const fmt = (v: number) => v.toPrecision(17);
216 return [
217 "",
218 "% ---- appended by the simulator: sample the fields on the grid ----",
219 `steelsim_nx = ${g.nx}; steelsim_ny = ${g.ny};`,
220 `steelsim_hx = ${fmt(g.hx)}; steelsim_hy = ${fmt(g.hy)};`,
221 `[steelsim_X, steelsim_Y] = meshgrid(((0:steelsim_nx-1) - (steelsim_nx-1)/2) * steelsim_hx, ((0:steelsim_ny-1) - (steelsim_ny-1)/2) * steelsim_hy);`,
222 "steelsim_R = hypot(steelsim_X, steelsim_Y); steelsim_T = atan2(steelsim_Y, steelsim_X);",
223 "steelsim_H = steelsim_field(design.thickness, 'thickness', steelsim_X, steelsim_Y, steelsim_R, steelsim_T);",
224 d.profileConst === null
225 ? "steelsim_Z = steelsim_field(design.profile, 'profile', steelsim_X, steelsim_Y, steelsim_R, steelsim_T);"
226 : `steelsim_Z = ${fmt(d.profileConst)} * ones(steelsim_ny, steelsim_nx);`,
227 d.hasInside
228 ? [
229 `[steelsim_XC, steelsim_YC] = meshgrid(((0:steelsim_nx-2) - (steelsim_nx-2)/2) * steelsim_hx, ((0:steelsim_ny-2) - (steelsim_ny-2)/2) * steelsim_hy);`,
230 "steelsim_IN = steelsim_field(design.inside, 'inside', steelsim_XC, steelsim_YC, hypot(steelsim_XC, steelsim_YC), atan2(steelsim_YC, steelsim_XC));",
231 ].join("\n")
232 : "",
233 "",
234 ].join("\n");
237/** A real [rows, cols] matrix from the workspace as a Float64Array in column-major order. */
238function matrix(vars: Record<string, RuntimeValue>, name: string, rows: number, cols: number, what: string): Float64Array {
239 const v = vars[name];
240 if (!isTensor(v) || v.shape.length !== 2 || v.shape[0] !== rows || v.shape[1] !== cols) throw new DesignError(`${what} did not produce a ${rows} x ${cols} matrix`);
241 if (v.imag && v.imag.some((x) => x !== 0)) throw new DesignError(`${what} returned complex values`);
242 return v.data;
245/**
246 * Samples the design on a grid of about the requested spacing: the mask from
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 247 * the outline at the cell centers, the thickness at the nodes (which must be
248 * positive on the plate), and the profile at the nodes with its mean plane
249 * removed. Only the slopes of the profile enter the physics, so its mean is
250 * immaterial, but its tilt is not: the von Karman strains are not invariant
251 * under rotation, and a tilted stress-free shape would add a spurious
252 * membrane coupling.
254export function evaluatePlate(d: Design, spacing: number): Plate {
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 255 const full = makeGrid(d.Lx, d.Ly, spacing);
256 const { nx, ny } = full;
257 const vars = runDesignScript(d.source + trailer(d, full));
258 // meshgrid matrices are [ny, nx] in column-major order: entry (i, j) sits at i * ny + j.
259 const H = matrix(vars, "steelsim_H", ny, nx, "thickness");
260 const Z = matrix(vars, "steelsim_Z", ny, nx, "profile");
261 let g = full;
262 if (d.hasInside) {
263 const IN = matrix(vars, "steelsim_IN", ny - 1, nx - 1, "inside");
264 g = withCellMask(full, (ci, cj) => IN[ci * (ny - 1) + cj] !== 0);
266 const h = new Float64Array(g.n), w0 = new Float64Array(g.n);
267 let any = false;
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 268 for (let j = 0; j < ny; j++) {
269 for (let i = 0; i < nx; i++) {
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 270 if (!insideNode(g, i, j)) continue;
271 any = true;
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 272 const k = j * nx + i, m = i * ny + j;
273 const x = nodeX(g, i), y = nodeY(g, j);
274 const at = `(${x.toFixed(4)}, ${y.toFixed(4)})`;
275 if (!isFinite(H[m])) throw new DesignError(`thickness is ${H[m]} at ${at}`);
276 if (H[m] <= 0) throw new DesignError(`thickness must be positive; it is ${H[m]} at ${at}`);
277 if (!isFinite(Z[m])) throw new DesignError(`profile is ${Z[m]} at ${at}`);
278 h[k] = H[m];
279 w0[k] = Z[m];
281 }
282 if (!any) throw new DesignError("the outline contains no grid cells; enlarge it or reduce the grid spacing");
283 removePlane(w0, g);
8db04e0Design scripts in MATLAB syntax, run by numblJeremy Magland 284 for (let j = 0; j < ny; j++) for (let i = 0; i < nx; i++) if (!insideNode(g, i, j)) w0[j * nx + i] = 0;
1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 285 return { name: d.name, Lx: d.Lx, Ly: d.Ly, grid: g, mat: d.material, h, w0, mount: d.mount, loss: d.loss };
288/** Peak-to-peak height and the largest slope of the profile over the plate nodes. */
289export function profileStats(pl: Plate): { peakToPeak: number; maxSlope: number } {
290 const g = pl.grid, w0 = pl.w0;
291 let lo = Infinity, hi = -Infinity, maxSlope = 0;
292 const at = (i: number, j: number) => w0[j * g.nx + i];
293 const slope = (i: number, j: number, di: number, dj: number, hg: number) => {
294 const hasP = insideNode(g, i + di, j + dj), hasM = insideNode(g, i - di, j - dj);
295 if (hasP && hasM) return (at(i + di, j + dj) - at(i - di, j - dj)) / (2 * hg);
296 if (hasP) return (at(i + di, j + dj) - at(i, j)) / hg;
297 if (hasM) return (at(i, j) - at(i - di, j - dj)) / hg;
298 return 0;
299 };
300 for (let j = 0; j < g.ny; j++) {
301 for (let i = 0; i < g.nx; i++) {
302 if (!insideNode(g, i, j)) continue;
303 const v = at(i, j);
304 lo = Math.min(lo, v); hi = Math.max(hi, v);
305 maxSlope = Math.max(maxSlope, Math.hypot(slope(i, j, 1, 0, g.hx), slope(i, j, 0, 1, g.hy)));
306 }
307 }
308 return { peakToPeak: isFinite(hi - lo) ? hi - lo : 0, maxSlope };
↑↓ move↵openescclose