/ proof-of-concept / plate-mechanics-sim
Sign in
proof-of-concept / plate-mechanics-sim
plate-mechanics-sim / packages / components / src / params / ParameterPanel.tsx
664 lines · 22.4 KBCodeBlameHistory
2546961Plate mechanics lab: Kirchhoff-Love plates solved by numbl in the browserJeremy Magland 1import type { ReactNode } from "react";
2import {
3 MATERIAL_PRESETS,
4 PLATE_PRESETS,
5 type BoundaryCondition,
6 type Excitation,
7 type MethodDefinition,
10 type ValidationIssue,
11} from "@plate-lab/core";
12import { NumberField } from "./NumberField";
13import { CflSlider } from "./CflSlider";
81f3b30Elliptical plates, a WebGPU time loop for the explicit method, and WAV exportJeremy Magland 14import type { WebGpuSupport } from "../hooks/useWebGpu";
16export type ParameterSection =
17 | "plate"
18 | "material"
19 | "edges"
20 | "damping"
21 | "excitation"
22 | "probe"
23 | "listener"
24 | "output"
25 | "modal"
26 | "fd"
27 | "implicit";
29export const ALL_SECTIONS: ParameterSection[] = [
30 "plate",
31 "material",
32 "edges",
33 "damping",
34 "excitation",
35 "probe",
36 "listener",
37 "output",
38 "modal",
39 "fd",
40 "implicit",
41];
43export interface ParameterPanelProps {
44 spec: ProblemSpec;
45 onChange: (spec: ProblemSpec) => void;
46 /** Sections to show; default all */
47 sections?: ParameterSection[];
48 /** Grey out settings the method ignores */
49 method?: MethodDefinition;
50 issues?: ValidationIssue[];
51 /** Largest stable FD step, when known, for the CFL slider */
52 fdDtMax?: number;
53 /** Sections open initially; default all */
54 open?: ParameterSection[];
81f3b30Elliptical plates, a WebGPU time loop for the explicit method, and WAV exportJeremy Magland 55 /** The GPU option of the explicit scheme: whether it is on, and what this browser offers */
56 fdGpu?: { enabled: boolean; onChange: (v: boolean) => void; support: WebGpuSupport };
59/** Controls that edit the spec. Every control maps to one field of the `params` struct. */
60export function ParameterPanel(props: ParameterPanelProps) {
62 spec,
63 onChange,
64 sections = ALL_SECTIONS,
65 method,
66 issues = [],
67 fdDtMax,
68 open,
69 fdGpu,
70 } = props;
2546961Plate mechanics lab: Kirchhoff-Love plates solved by numbl in the browserJeremy Magland 71 const set = (patch: Partial<ProblemSpec>) => onChange({ ...spec, ...patch });
78d40e9Free edges for both finite-difference methodsJeremy Magland 72 const issue = (path: string) =>
73 issues.find((i) => i.path === path || i.path.startsWith(path + "."));
2546961Plate mechanics lab: Kirchhoff-Love plates solved by numbl in the browserJeremy Magland 74 const invalid = (path: string) => issue(path)?.severity === "error";
75 const show = (s: ParameterSection) => sections.includes(s);
76 const isOpen = (s: ParameterSection) => (open ? open.includes(s) : true);
77 const e = spec.excitation;
79 const materialPresetId = MATERIAL_PRESETS.find(
80 (p) => p.material.name === spec.material.name && p.material.E === spec.material.E,
81 )?.id;
82 const platePresetId = PLATE_PRESETS.find(
81f3b30Elliptical plates, a WebGPU time loop for the explicit method, and WAV exportJeremy Magland 84 p.plate.shape === spec.plate.shape &&
85 p.plate.Lx === spec.plate.Lx &&
86 p.plate.Ly === spec.plate.Ly &&
87 p.plate.h === spec.plate.h,
81f3b30Elliptical plates, a WebGPU time loop for the explicit method, and WAV exportJeremy Magland 89 const ellipse = spec.plate.shape === "ellipse";
91 return (
92 <div className="pl-params">
93 {show("plate") && (
94 <Group title="Plate" issues={issues} prefix="plate" open={isOpen("plate")}>
95 <div className="pl-field pl-field-wide">
96 <label>Stock</label>
97 <select
98 className="pl-select"
99 value={platePresetId ?? ""}
100 onChange={(ev) => {
101 const p = PLATE_PRESETS.find((x) => x.id === ev.target.value);
102 if (p) set({ plate: { ...p.plate } });
103 }}
104 >
105 <option value="">custom</option>
106 {PLATE_PRESETS.map((p) => (
107 <option key={p.id} value={p.id}>
108 {p.name}
109 </option>
110 ))}
111 </select>
112 </div>
78d40e9Free edges for both finite-difference methodsJeremy Magland 113 {platePresetId && (
114 <div className="pl-small pl-muted">
115 {PLATE_PRESETS.find((p) => p.id === platePresetId)?.note}
116 </div>
117 )}
81f3b30Elliptical plates, a WebGPU time loop for the explicit method, and WAV exportJeremy Magland 118 <div className="pl-field pl-field-wide">
119 <label>Shape</label>
120 <select
121 className="pl-select"
122 value={spec.plate.shape}
123 onChange={(ev) =>
124 set({ plate: { ...spec.plate, shape: ev.target.value as PlateShape } })
125 }
126 >
127 <option value="rectangle">rectangle</option>
128 <option value="ellipse">ellipse (a disk when Lx = Ly)</option>
129 </select>
130 </div>
131 {method && !method.supports.shape.includes(spec.plate.shape) && (
132 <div className="pl-issue">
133 {method.name} has no {spec.plate.shape} solution; the finite-difference methods embed
134 it in their grid.
135 </div>
136 )}
78d40e9Free edges for both finite-difference methodsJeremy Magland 137 <NumberField
81f3b30Elliptical plates, a WebGPU time loop for the explicit method, and WAV exportJeremy Magland 138 label={ellipse ? "Diameter along x, Lx" : "Length Lx"}
78d40e9Free edges for both finite-difference methodsJeremy Magland 139 value={spec.plate.Lx}
140 scale={1000}
141 unit="mm"
142 invalid={invalid("plate.Lx")}
143 onChange={(v) => set({ plate: { ...spec.plate, Lx: v } })}
144 />
145 <NumberField
81f3b30Elliptical plates, a WebGPU time loop for the explicit method, and WAV exportJeremy Magland 146 label={ellipse ? "Diameter along y, Ly" : "Width Ly"}
78d40e9Free edges for both finite-difference methodsJeremy Magland 147 value={spec.plate.Ly}
148 scale={1000}
149 unit="mm"
150 invalid={invalid("plate.Ly")}
151 onChange={(v) => set({ plate: { ...spec.plate, Ly: v } })}
152 />
153 <NumberField
154 label="Thickness h"
155 value={spec.plate.h}
156 scale={1000}
157 unit="mm"
158 step={0.1}
159 invalid={invalid("plate.h")}
160 onChange={(v) => set({ plate: { ...spec.plate, h: v } })}
161 />
163 <div className="pl-small pl-muted">
164 The ellipse fills the Lx by Ly box, and positions are still measured from the box's
165 corner. The finite-difference grid spans the box; the edge becomes a staircase of
166 nodes, first order in the grid spacing.
167 </div>
168 )}
170 )}
171 {show("material") && (
172 <Group title="Material" issues={issues} prefix="material" open={isOpen("material")}>
173 <div className="pl-field pl-field-wide">
174 <label>Preset</label>
175 <select
176 className="pl-select"
177 value={materialPresetId ?? ""}
178 onChange={(ev) => {
179 const p = MATERIAL_PRESETS.find((x) => x.id === ev.target.value);
180 if (p) set({ material: { ...p.material }, damping: { ...p.damping } });
181 }}
182 >
183 <option value="">custom</option>
184 {MATERIAL_PRESETS.map((p) => (
185 <option key={p.id} value={p.id}>
186 {p.material.name}
187 </option>
188 ))}
189 </select>
190 </div>
191 {materialPresetId && (
78d40e9Free edges for both finite-difference methodsJeremy Magland 192 <div className="pl-small pl-muted">
193 {MATERIAL_PRESETS.find((p) => p.id === materialPresetId)?.source} Handbook values, not
194 a certificate.
195 </div>
78d40e9Free edges for both finite-difference methodsJeremy Magland 197 <NumberField
198 label="Young's modulus E"
199 value={spec.material.E}
200 scale={1e-9}
201 unit="GPa"
202 invalid={invalid("material.E")}
203 onChange={(v) =>
204 set({ material: { ...spec.material, E: v, name: customName(spec.material.name) } })
205 }
206 />
207 <NumberField
208 label="Poisson's ratio ν"
209 value={spec.material.nu}
210 step={0.01}
211 invalid={invalid("material.nu")}
212 onChange={(v) =>
213 set({ material: { ...spec.material, nu: v, name: customName(spec.material.name) } })
214 }
215 />
216 <NumberField
217 label="Density ρ"
218 value={spec.material.rho}
219 unit="kg/m³"
220 invalid={invalid("material.rho")}
221 onChange={(v) =>
222 set({ material: { ...spec.material, rho: v, name: customName(spec.material.name) } })
223 }
224 />
226 )}
227 {show("edges") && (
228 <Group title="Edges" issues={issues} prefix="bc" open={isOpen("edges")}>
229 <div className="pl-field pl-field-wide">
230 <label>Condition</label>
232 className="pl-select"
233 value={spec.bc}
234 onChange={(ev) => set({ bc: ev.target.value as BoundaryCondition })}
235 >
2546961Plate mechanics lab: Kirchhoff-Love plates solved by numbl in the browserJeremy Magland 236 <option value="simply-supported">simply supported (w = 0, no moment)</option>
237 <option value="clamped">clamped (w = 0, no slope)</option>
78d40e9Free edges for both finite-difference methodsJeremy Magland 238 <option value="free">free (no moment, no shear): the free-free plate</option>
240 free edges, pinned at the centre: a cymbal on its stand
241 </option>
243 </div>
244 {method && !method.supports.bc.includes(spec.bc) && (
78d40e9Free edges for both finite-difference methodsJeremy Magland 245 <div className="pl-issue">
246 {method.name} has no {spec.bc} solution; the finite-difference methods can run this.
247 </div>
78d40e9Free edges for both finite-difference methodsJeremy Magland 249 <div className="pl-small pl-muted">
250 All three are idealizations: a real clamp has finite stiffness, a real support some
81f3b30Elliptical plates, a WebGPU time loop for the explicit method, and WAV exportJeremy Magland 251 rotational restraint, a free plate rests on something that takes its rigid-body motion,
252 which the solvers remove, and a stand holds a cymbal over a washer, not at a point.
255 )}
256 {show("damping") && (
257 <Group title="Damping" issues={issues} prefix="damping" open={isOpen("damping")}>
78d40e9Free edges for both finite-difference methodsJeremy Magland 258 <NumberField
259 label="σ₀ (all frequencies)"
260 value={spec.damping.sigma0}
261 unit="1/s"
262 step={0.1}
263 invalid={invalid("damping")}
264 onChange={(v) => set({ damping: { ...spec.damping, sigma0: v } })}
265 />
266 <NumberField
267 label="σ₁ (rises with frequency)"
268 value={spec.damping.sigma1}
269 unit="m²/s"
270 step={0.001}
271 invalid={invalid("damping")}
272 onChange={(v) => set({ damping: { ...spec.damping, sigma1: v } })}
273 />
274 <div className="pl-small pl-muted">
275 A mode with Laplacian eigenvalue λ decays at rate σ₀ + σ₁λ. Illustrative values; real
276 losses depend on the mounting.
277 </div>
279 )}
280 {show("excitation") && (
281 <Group title="Excitation" issues={issues} prefix="excitation" open={isOpen("excitation")}>
282 <div className="pl-field pl-field-wide">
283 <label>Type</label>
284 <select
285 className="pl-select"
286 value={e.type}
287 onChange={(ev) => {
288 const type = ev.target.value as Excitation["type"];
289 const next: Excitation =
290 type === "impulse"
291 ? { type, x: e.x, y: e.y, force: e.force, duration: 1e-3, radius: e.radius }
293 type,
294 x: e.x,
295 y: e.y,
296 force: Math.min(e.force, 5),
297 frequency: 500,
298 radius: e.radius,
299 };
301 }}
302 >
303 <option value="impulse">hammer strike (half-sine pulse)</option>
304 <option value="harmonic">harmonic point force</option>
305 </select>
306 </div>
78d40e9Free edges for both finite-difference methodsJeremy Magland 307 <NumberField
308 label="Strike x"
309 value={e.x}
310 scale={1000}
311 unit="mm"
312 invalid={invalid("excitation")}
313 onChange={(v) => set({ excitation: { ...e, x: v } })}
314 />
315 <NumberField
316 label="Strike y"
317 value={e.y}
318 scale={1000}
319 unit="mm"
320 invalid={invalid("excitation")}
321 onChange={(v) => set({ excitation: { ...e, y: v } })}
322 />
323 <NumberField
324 label={e.type === "impulse" ? "Peak force" : "Force amplitude"}
325 value={e.force}
326 unit="N"
327 invalid={invalid("excitation.force")}
328 onChange={(v) => set({ excitation: { ...e, force: v } })}
329 />
78d40e9Free edges for both finite-difference methodsJeremy Magland 331 <NumberField
332 label="Contact time"
333 value={e.duration}
334 scale={1000}
335 unit="ms"
336 step={0.1}
337 invalid={invalid("excitation.duration")}
338 onChange={(v) => set({ excitation: { ...e, duration: v } })}
339 />
78d40e9Free edges for both finite-difference methodsJeremy Magland 341 <NumberField
342 label="Frequency"
343 value={e.frequency}
344 unit="Hz"
345 invalid={invalid("excitation.frequency")}
346 onChange={(v) => set({ excitation: { ...e, frequency: v } })}
347 />
78d40e9Free edges for both finite-difference methodsJeremy Magland 349 <NumberField
350 label="Footprint radius"
351 value={e.radius}
352 scale={1000}
353 unit="mm"
354 step={1}
355 invalid={invalid("excitation.radius")}
356 onChange={(v) => set({ excitation: { ...e, radius: v } })}
357 />
358 <div className="pl-small pl-muted">
359 A finite footprint stands in for the contact patch; a point load would give infinite
360 stress under the hammer.
361 </div>
363 )}
364 {show("probe") && (
365 <Group title="Probe" issues={issues} prefix="probe" open={isOpen("probe")}>
78d40e9Free edges for both finite-difference methodsJeremy Magland 366 <NumberField
367 label="x"
368 value={spec.probe.x}
369 scale={1000}
370 unit="mm"
371 invalid={invalid("probe")}
372 onChange={(v) => set({ probe: { ...spec.probe, x: v } })}
373 />
374 <NumberField
375 label="y"
376 value={spec.probe.y}
377 scale={1000}
378 unit="mm"
379 invalid={invalid("probe")}
380 onChange={(v) => set({ probe: { ...spec.probe, y: v } })}
381 />
382 <div className="pl-small pl-muted">
383 Time series and spectra are recorded here. Finite differences snap it to the nearest
384 grid node.
385 </div>
387 )}
388 {show("listener") && (
389 <Group title="Listener (sound)" issues={issues} prefix="listener" open={isOpen("listener")}>
78d40e9Free edges for both finite-difference methodsJeremy Magland 390 <NumberField
391 label="x"
392 value={spec.listener.x}
393 scale={1000}
394 unit="mm"
395 onChange={(v) => set({ listener: { ...spec.listener, x: v } })}
396 />
397 <NumberField
398 label="y"
399 value={spec.listener.y}
400 scale={1000}
401 unit="mm"
402 onChange={(v) => set({ listener: { ...spec.listener, y: v } })}
403 />
404 <NumberField
405 label="Height z"
406 value={spec.listener.z}
407 unit="m"
408 step={0.1}
409 onChange={(v) => set({ listener: { ...spec.listener, z: v } })}
410 />
411 <div className="pl-small pl-muted">
412 Rayleigh integral in an infinite baffle: an estimate of the waveform, not a calibrated
413 level.
414 </div>
416 )}
417 {show("output") && (
418 <Group title="Output" issues={issues} prefix="output" open={isOpen("output")}>
78d40e9Free edges for both finite-difference methodsJeremy Magland 419 <NumberField
420 label="Sample rate"
421 value={spec.output.fs}
422 unit="Hz"
423 integer
424 invalid={invalid("output.fs")}
425 onChange={(v) => set({ output: { ...spec.output, fs: v } })}
426 />
427 <NumberField
428 label="Duration (modal, audio)"
429 value={spec.output.duration}
430 unit="s"
431 step={0.1}
432 invalid={invalid("output.duration")}
433 onChange={(v) => set({ output: { ...spec.output, duration: v } })}
434 />
435 <NumberField
436 label="Animation frames"
437 value={spec.output.nframes}
438 integer
439 invalid={invalid("output.nframes")}
440 onChange={(v) => set({ output: { ...spec.output, nframes: v } })}
441 />
442 <NumberField
443 label="Display grid nx"
444 value={spec.output.grid[0]}
445 integer
446 onChange={(v) => set({ output: { ...spec.output, grid: [v, spec.output.grid[1]] } })}
447 />
448 <NumberField
449 label="Display grid ny"
450 value={spec.output.grid[1]}
451 integer
452 onChange={(v) => set({ output: { ...spec.output, grid: [spec.output.grid[0], v] } })}
453 />
455 )}
456 {show("modal") && (
458 title="Modal method"
459 issues={issues}
460 prefix="modal"
461 open={isOpen("modal")}
462 disabled={method ? method.settingsKey !== "modal" : false}
463 >
464 <NumberField
465 label="Max frequency"
466 value={spec.modal.fmax}
467 unit="Hz"
468 invalid={invalid("modal.fmax")}
469 onChange={(v) => set({ modal: { ...spec.modal, fmax: v } })}
470 />
471 <NumberField
472 label="Max modes"
473 value={spec.modal.max_modes}
474 integer
475 onChange={(v) => set({ modal: { ...spec.modal, max_modes: v } })}
476 />
477 <div className="pl-small pl-muted">
478 Truncation of the modal sum: displacement converges fast, stress near the strike slowly.
479 </div>
481 )}
482 {show("fd") && (
484 title="Explicit finite differences"
485 issues={issues}
486 prefix="fd"
487 open={isOpen("fd")}
488 disabled={method ? method.settingsKey !== "fd" : false}
489 >
490 <NumberField
491 label="Grid nx"
492 value={spec.fd.nx}
493 integer
494 min={5}
495 invalid={invalid("fd")}
496 onChange={(v) => set({ fd: { ...spec.fd, nx: v } })}
497 />
498 <NumberField
499 label="Grid ny"
500 value={spec.fd.ny}
501 integer
502 min={5}
503 invalid={invalid("fd")}
504 onChange={(v) => set({ fd: { ...spec.fd, ny: v } })}
505 />
506 <NumberField
507 label="Duration"
508 value={spec.fd.duration}
509 scale={1000}
510 unit="ms"
511 step={0.5}
512 onChange={(v) => set({ fd: { ...spec.fd, duration: v } })}
513 />
514 <div className="pl-small" style={{ marginTop: 6 }}>
515 Time step
516 </div>
517 <CflSlider
518 value={spec.fd.cfl}
519 dtMax={fdDtMax}
520 onChange={(v) => set({ fd: { ...spec.fd, cfl: v } })}
521 />
81f3b30Elliptical plates, a WebGPU time loop for the explicit method, and WAV exportJeremy Magland 522 {fdGpu && <GpuToggle {...fdGpu} />}
524 )}
525 {show("implicit") && (
527 title="Implicit finite differences"
528 issues={issues}
529 prefix="implicit"
530 open={isOpen("implicit")}
531 disabled={method ? method.settingsKey !== "implicit" : false}
532 >
533 <NumberField
534 label="Grid nx"
535 value={spec.implicit.nx}
536 integer
537 min={5}
538 invalid={invalid("implicit")}
539 onChange={(v) => set({ implicit: { ...spec.implicit, nx: v } })}
540 />
541 <NumberField
542 label="Grid ny"
543 value={spec.implicit.ny}
544 integer
545 min={5}
546 invalid={invalid("implicit")}
547 onChange={(v) => set({ implicit: { ...spec.implicit, ny: v } })}
548 />
549 <NumberField
550 label="Duration"
551 value={spec.implicit.duration}
552 scale={1000}
553 unit="ms"
554 step={5}
555 onChange={(v) => set({ implicit: { ...spec.implicit, duration: v } })}
556 />
557 <NumberField
558 label="Steps per sample"
559 value={spec.implicit.steps_per_sample}
560 step={0.5}
561 min={0.25}
562 invalid={invalid("implicit.steps_per_sample")}
563 onChange={(v) => set({ implicit: { ...spec.implicit, steps_per_sample: v } })}
564 />
565 <NumberField
566 label="θ (time averaging)"
567 value={spec.implicit.theta}
568 step={0.05}
569 min={0}
570 max={1}
571 invalid={invalid("implicit.theta")}
572 onChange={(v) => set({ implicit: { ...spec.implicit, theta: v } })}
573 />
574 <div className="pl-small pl-muted">
575 No stability limit for θ ≥ 1/4; the step sets accuracy instead. More steps per sample
576 tune the high modes better and cost proportionally.
577 </div>
579 )}
580 </div>
581 );
585 * The explicit scheme's time loop has a WebGPU port. The option is off by
586 * default: the double-precision loop in numbl is the reference, and the port is
587 * the same arithmetic in single precision, many times faster.
588 */
589function GpuToggle({
590 enabled,
591 onChange,
592 support,
593}: {
594 enabled: boolean;
595 onChange: (v: boolean) => void;
596 support: WebGpuSupport;
597}) {
598 const available = support.status === "available";
599 return (
600 <div className="pl-gpu">
601 <label className={`pl-check${available ? "" : " pl-disabled"}`}>
602 <input
603 type="checkbox"
604 checked={enabled && available}
605 disabled={!available}
606 onChange={(ev) => onChange(ev.target.checked)}
607 />
608 Run the time loop on the GPU (WebGPU)
609 </label>
610 <div className="pl-small pl-muted">
611 {support.status === "checking" && "Looking for WebGPU…"}
612 {support.status === "unavailable" &&
613 `Not available here (${support.reason}); the loop runs in numbl.`}
614 {support.status === "available" &&
615 (enabled
616 ? `plate_fd_loop.m runs as its WebGPU port on ${support.adapter}, in single precision; setup, stresses, sound and spectra stay in numbl. Editing plate_fd_loop.m sends the loop back to numbl.`
617 : `Available on ${support.adapter}. The loop then runs in single precision, typically 10 to 50 times faster than in numbl.`)}
618 </div>
619 </div>
620 );
2546961Plate mechanics lab: Kirchhoff-Love plates solved by numbl in the browserJeremy Magland 623function customName(name: string): string {
624 return name.endsWith(" (edited)") ? name : `${name} (edited)`;
78d40e9Free edges for both finite-difference methodsJeremy Magland 627function Group(props: {
628 title: string;
629 prefix: string;
630 issues: ValidationIssue[];
631 open: boolean;
632 disabled?: boolean;
633 children: ReactNode;
634}) {
635 const related = props.issues.filter(
636 (i) => i.path === props.prefix || i.path.startsWith(props.prefix + "."),
637 );
639 <details className={`pl-group${props.disabled ? " pl-disabled" : ""}`} open={props.open}>
640 <summary>
641 {props.title}
78d40e9Free edges for both finite-difference methodsJeremy Magland 642 {related.some((i) => i.severity === "error") && (
643 <span className="pl-badge pl-badge-error" style={{ marginLeft: 6 }}>
644 check
645 </span>
646 )}
647 {props.disabled && (
648 <span className="pl-badge pl-badge-info" style={{ marginLeft: 6 }}>
649 not used by this method
650 </span>
651 )}
653 {props.children}
654 {related.map((i) => (
656 key={i.path + i.message}
657 className={`pl-issue${i.severity === "warning" ? " pl-issue-warning" : ""}`}
658 >
660 </div>
661 ))}
662 </details>
663 );
↑↓ move↵openescclose