/ proof-of-concept / plate-dynamics-lab
Sign in
proof-of-concept / plate-dynamics-lab
plate-dynamics-lab / packages / core / src / spec / validate.ts
129 lines · 6.5 KBCodeBlameHistory
80c0b6dPlate dynamics lab: plates in realizable mounts under gravity, explicit finite differences on WebGPUJeremy Magland 1import { centroid, onPlate, type ProblemSpec } from "./types";
3export interface ValidationIssue {
4 /** Dotted path into the spec, e.g. "plate.h" */
5 path: string;
6 message: string;
7 severity: "error" | "warning";
8}
10/**
11 * Structural checks a control panel can show before running anything. The
12 * `.m` code repeats the checks that matter for the numerics, so a spec that
13 * passes here can still be rejected by the solver with a legible message.
14 */
15export function validateSpec(spec: ProblemSpec): ValidationIssue[] {
16 const out: ValidationIssue[] = [];
17 const err = (path: string, message: string) => out.push({ path, message, severity: "error" });
18 const warn = (path: string, message: string) => out.push({ path, message, severity: "warning" });
20 const { plate, material, excitation, probe, output, fd, damping, mount, gravity, listener } =
21 spec;
22 if (!(plate.Lx > 0)) err("plate.Lx", "must be positive");
23 if (!(plate.Ly > 0)) err("plate.Ly", "must be positive");
24 if (!(plate.h > 0)) err("plate.h", "must be positive");
25 if (plate.h > 0.1 * Math.min(plate.Lx, plate.Ly))
26 warn("plate.h", "thickness above a tenth of the shorter side: thin-plate theory is unreliable");
27 if (!(material.E > 0)) err("material.E", "must be positive");
28 if (!(material.nu >= 0 && material.nu < 0.5)) err("material.nu", "must lie in [0, 0.5)");
29 if (!(material.rho > 0)) err("material.rho", "must be positive");
30 if (damping.sigma0 < 0 || damping.sigma1 < 0) err("damping", "loss rates cannot be negative");
31 if (!(gravity.g >= 0)) err("gravity.g", "cannot be negative");
33 const where = plate.shape === "ellipse" ? "outside the ellipse" : "outside the plate";
34 if (!onPlate(plate, excitation.x, excitation.y)) err("excitation", `strike point lies ${where}`);
35 if (!onPlate(plate, probe.x, probe.y)) err("probe", `probe lies ${where}`);
36 if (!(excitation.radius > 0)) err("excitation.radius", "footprint radius must be positive");
37 if (excitation.type === "impulse" && !(excitation.duration > 0))
38 err("excitation.duration", "contact time must be positive");
39 if (excitation.type === "release") {
40 if (!(excitation.displacement > 0)) err("excitation.displacement", "pull must be positive");
41 if (excitation.displacement > 0.05 * Math.min(plate.Lx, plate.Ly))
42 warn(
43 "excitation.displacement",
44 "a pull beyond a twentieth of the plate's size is outside small-angle theory",
45 );
46 }
48 const cm = centroid(plate);
49 const near = (x: number, y: number, r: number) => Math.hypot(x - cm.x, y - cm.y) < r;
50 switch (mount.type) {
51 case "thread":
52 if (!onPlate(plate, mount.x, mount.y)) err("mount", `the hole lies ${where}`);
53 else if (near(mount.x, mount.y, 0.05 * Math.min(plate.Lx, plate.Ly)))
54 err(
55 "mount",
56 "the hole is at the centre of mass; the plate would hang in no particular orientation",
57 );
58 if (!(mount.length > 0)) err("mount.length", "thread length must be positive");
59 if (!(mount.thread.diameter > 0) || !(mount.thread.G > 0))
60 err("mount.thread", "thread diameter and shear modulus must be positive");
61 break;
62 case "threads": {
63 if (!onPlate(plate, mount.x1, mount.y1)) err("mount", `hole 1 lies ${where}`);
64 if (!onPlate(plate, mount.x2, mount.y2)) err("mount", `hole 2 lies ${where}`);
65 if (!(mount.length > 0)) err("mount.length", "thread length must be positive");
66 const ex = mount.x2 - mount.x1;
67 const ey = mount.y2 - mount.y1;
68 const s2 = Math.hypot(ex, ey);
69 if (s2 < 1e-6) err("mount", "the two holes coincide");
70 else {
71 const sc = ((cm.x - mount.x1) * ex + (cm.y - mount.y1) * ey) / (s2 * s2);
72 if (!(sc > 0 && sc < 1))
73 err("mount", "the centre of mass is not between the holes: one thread would go slack");
74 }
75 break;
76 }
77 case "pads":
78 if (mount.pads.length === 0) err("mount", "need at least one pad");
79 mount.pads.forEach((p, k) => {
80 if (!onPlate(plate, p.x, p.y)) err("mount", `pad ${k + 1} is centred ${where}`);
81 if (!(p.rx > 0) || !(p.ry > 0)) err("mount", `pad ${k + 1} needs positive half-widths`);
82 });
83 if (!(mount.k > 0)) err("mount.k", "pad stiffness must be positive");
84 if (mount.c < 0) err("mount.c", "pad damping cannot be negative");
85 if (mount.pads.length < 2)
86 warn(
87 "mount",
88 "a plate on one pad tips unless the pad is wide; the static solve will say if it does",
89 );
90 break;
91 case "cymbal":
92 if (!(mount.r_felt > mount.r_rod) || !(mount.r_rod >= 0))
93 err("mount", "the felt's outer radius must exceed the rod's");
94 if (mount.r_felt > 0.5 * Math.min(plate.Lx, plate.Ly))
95 err("mount", "the felt is larger than the plate");
96 if (!(mount.k > 0)) err("mount.k", "felt stiffness must be positive");
97 if (mount.c < 0) err("mount.c", "felt damping cannot be negative");
98 if (mount.preload < 0) err("mount.preload", "preload cannot be negative");
99 break;
100 case "vise": {
101 const side = mount.edge.startsWith("x") ? plate.Lx : plate.Ly;
102 if (!(mount.depth > 0)) err("mount.depth", "jaw depth must be positive");
103 if (mount.depth > 0.5 * side) err("mount.depth", "the jaws hold more than half the plate");
104 if (!(mount.tilt >= 0 && mount.tilt <= 180))
105 err("mount.tilt", "tilt runs from 0 to 180 degrees");
106 break;
107 }
108 case "pinched":
109 if (!onPlate(plate, mount.x, mount.y)) err("mount", `the pinch point lies ${where}`);
110 if (!(mount.radius > 0)) err("mount.radius", "patch radius must be positive");
111 if (!(mount.k > 0)) err("mount.k", "grip stiffness must be positive");
112 if (mount.c < 0) err("mount.c", "grip damping cannot be negative");
113 if (!(mount.tilt >= 0 && mount.tilt <= 90))
114 err("mount.tilt", "tilt runs from 0 to 90 degrees");
115 break;
116 }
118 if (!(listener.z !== 0)) err("listener.z", "the listener cannot be in the plane of the plate");
119 if (!(output.fs > 0)) err("output.fs", "sample rate must be positive");
120 if (output.nframes < 2) err("output.nframes", "need at least 2 frames");
121 if (fd.nx < 5 || fd.ny < 5) err("fd", "finite-difference grid needs at least 5 nodes per side");
122 if (!(fd.duration > 0)) err("fd.duration", "duration must be positive");
123 if (fd.cfl > 1) warn("fd.cfl", "above the stability limit: the run will blow up");
124 const dxFd = plate.Lx / (fd.nx - 1);
125 const dyFd = plate.Ly / (fd.ny - 1);
126 if (excitation.radius < Math.max(dxFd, dyFd))
127 warn("excitation.radius", "footprint smaller than a grid cell: the load lands on one node");
128 return out;
↑↓ move↵openescclose