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