import { onPlate, type ProblemSpec } from "./types"; export interface ValidationIssue { /** Dotted path into the spec, e.g. "plate.thickness" */ path: string; message: string; severity: "error" | "warning"; } /** * Structural checks a control panel can show before running anything. The * `.m` code repeats the checks that matter for the numerics, so a spec that * passes here can still be rejected by the solver with a legible message. */ export function validateSpec(spec: ProblemSpec): ValidationIssue[] { const out: ValidationIssue[] = []; const err = (path: string, message: string) => out.push({ path, message, severity: "error" }); const warn = (path: string, message: string) => out.push({ path, message, severity: "warning" }); const { plate, material, hole, thread, hammer, strike, probe, mic, render, rr, fem } = spec; if (!(plate.width > 0)) err("plate.width", "must be positive"); if (!(plate.height > 0)) err("plate.height", "must be positive"); if (!(plate.thickness > 0)) err("plate.thickness", "must be positive"); if (plate.thickness > 0.1 * Math.min(plate.width, plate.height)) warn( "plate.thickness", "thickness above a tenth of the shorter side: the Kirchhoff-Love method is unreliable; the Mindlin-Reissner method carries the shear", ); if (!(material.E > 0)) err("material.E", "must be positive"); if (!(material.nu >= 0 && material.nu < 0.5)) err("material.nu", "must lie in [0, 0.5)"); if (!(material.rho > 0)) err("material.rho", "must be positive"); if (!(material.Q > 0)) err("material.Q", "must be positive"); if (!(hole.diameter > 0)) err("hole.diameter", "must be positive"); if (!(hole.edge_offset > hole.diameter / 2)) err("hole.edge_offset", "the hole must lie inside the plate: offset above the hole's radius"); if (!(hole.edge_offset < plate.height / 2)) err("hole.edge_offset", "the hole must lie above the plate's centre"); if (!(thread.length > 0)) err("thread.length", "must be positive"); if (thread.dashpot < 0) err("thread.dashpot", "cannot be negative"); if (!(hammer.mass > 0)) err("hammer.mass", "must be positive"); if (!(hammer.tip_radius > 0)) err("hammer.tip_radius", "must be positive"); if (!(hammer.speed > 0)) err("hammer.speed", "must be positive"); if (!(hammer.E > 0)) err("hammer.E", "must be positive"); if (!(hammer.nu >= 0 && hammer.nu < 0.5)) err("hammer.nu", "must lie in [0, 0.5)"); if (!(hammer.restitution >= 0 && hammer.restitution <= 1)) err("hammer.restitution", "must lie in [0, 1]"); if (hammer.speed > 2) warn( "hammer.speed", "a hard strike would dent aluminium; the model is linear and does not know", ); if (!onPlate(plate, strike.x, strike.y)) err("strike", "strike point lies outside the plate"); if (!onPlate(plate, probe.x, probe.y)) err("probe", "probe lies outside the plate"); if (!(mic.distance > 0)) err("mic.distance", "must be positive"); if (!(mic.air_density > 0)) err("mic.air_density", "must be positive"); if (!(mic.sound_speed > 0)) err("mic.sound_speed", "must be positive"); if (!(render.fs > 0)) err("render.fs", "sample rate must be positive"); if (!(render.duration > 0)) err("render.duration", "duration must be positive"); if (render.duration > 10) warn("render.duration", "a long render is slow to synthesize"); if (!(render.modes >= 1)) err("render.modes", "keep at least one mode"); if (render.nframes < 2) err("render.nframes", "need at least 2 frames"); if (!(render.frame_span > 0)) err("render.frame_span", "must be positive"); if (render.frame_span > render.duration) err("render.frame_span", "the frames cannot outlast the render"); if (!(render.normalize_dbfs <= 0)) err("render.normalize_dbfs", "a peak above 0 dBFS clips"); if (!Number.isInteger(rr.basis_order) || rr.basis_order < 2) err("rr.basis_order", "the basis needs order 2 or more"); if (rr.basis_order > 24) warn("rr.basis_order", "a high order costs time and conditioning"); if (!Number.isInteger(rr.quadrature_order) || rr.quadrature_order < rr.basis_order + 2) err("rr.quadrature_order", "use at least basis order + 2 quadrature points per side"); if (!Number.isInteger(rr.grid) || rr.grid < 5) err("rr.grid", "the display grid needs 5 nodes or more"); if (!Number.isInteger(fem.mesh_x) || fem.mesh_x < 4) err("fem.mesh_x", "at least 4 elements"); if (!Number.isInteger(fem.mesh_y) || fem.mesh_y < 4) err("fem.mesh_y", "at least 4 elements"); if (fem.mesh_x * fem.mesh_y > 100 * 100) warn("fem", "more than 10,000 elements: the eigensolve will take minutes"); return out; }