import { onPlate, type ProblemSpec } from "./types"; export interface ValidationIssue { /** Dotted path into the spec, e.g. "plate.h" */ 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, excitation, probe, output, fd, modal, damping, implicit } = spec; if (!(plate.Lx > 0)) err("plate.Lx", "must be positive"); if (!(plate.Ly > 0)) err("plate.Ly", "must be positive"); if (!(plate.h > 0)) err("plate.h", "must be positive"); if (plate.h > 0.1 * Math.min(plate.Lx, plate.Ly)) warn("plate.h", "thickness above a tenth of the shorter side: thin-plate theory is unreliable"); 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 (damping.sigma0 < 0 || damping.sigma1 < 0) err("damping", "loss rates cannot be negative"); const where = plate.shape === "ellipse" ? "outside the ellipse" : "outside the plate"; if (!onPlate(plate, excitation.x, excitation.y)) err("excitation", `strike point lies ${where}`); if (!onPlate(plate, probe.x, probe.y)) err("probe", `probe lies ${where}`); if (!(excitation.radius > 0)) err("excitation.radius", "footprint radius must be positive"); if (excitation.type === "impulse" && !(excitation.duration > 0)) err("excitation.duration", "contact time must be positive"); if (excitation.type === "harmonic" && !(excitation.frequency > 0)) err("excitation.frequency", "drive frequency must be positive"); if (!(output.fs > 0)) err("output.fs", "sample rate must be positive"); if (!(output.duration > 0)) err("output.duration", "duration must be positive"); if (output.nframes < 2) err("output.nframes", "need at least 2 frames"); if (modal.fmax > output.fs / 2) warn("modal.fmax", "modes above half the sample rate alias in the probe series and audio"); if (fd.nx < 5 || fd.ny < 5) err("fd", "finite-difference grid needs at least 5 nodes per side"); if (fd.cfl > 1) warn("fd.cfl", "above the stability limit: the finite-difference run will blow up"); if (implicit.nx < 5 || implicit.ny < 5) err("implicit", "implicit grid needs at least 5 nodes per side"); if (!(implicit.steps_per_sample > 0)) err("implicit.steps_per_sample", "must be positive"); if (implicit.theta < 0.25) warn("implicit.theta", "below 1/4 the implicit scheme is only conditionally stable"); if (implicit.steps_per_sample < 1) warn( "implicit.steps_per_sample", "fewer than one step per sample detunes modes near half the sample rate by tens of percent", ); const dxFd = plate.Lx / (fd.nx - 1); const dyFd = plate.Ly / (fd.ny - 1); if (excitation.radius < Math.max(dxFd, dyFd)) warn( "excitation.radius", "footprint smaller than a finite-difference cell: the load lands on one node", ); return out; }