/ proof-of-concept / plate-mechanics-sim
Sign in
proof-of-concept / plate-mechanics-sim
plate-mechanics-sim / packages / components / src / params / ParameterPanel.tsx
257 lines · 15.3 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,
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) => issues.find((i) => i.path === path || i.path.startsWith(path + "."));
60 const invalid = (path: string) => issue(path)?.severity === "error";
61 const show = (s: ParameterSection) => sections.includes(s);
62 const isOpen = (s: ParameterSection) => (open ? open.includes(s) : true);
63 const e = spec.excitation;
65 const materialPresetId = MATERIAL_PRESETS.find(
66 (p) => p.material.name === spec.material.name && p.material.E === spec.material.E,
67 )?.id;
68 const platePresetId = PLATE_PRESETS.find(
69 (p) => p.plate.Lx === spec.plate.Lx && p.plate.Ly === spec.plate.Ly && p.plate.h === spec.plate.h,
70 )?.id;
72 return (
73 <div className="pl-params">
74 {show("plate") && (
75 <Group title="Plate" issues={issues} prefix="plate" open={isOpen("plate")}>
76 <div className="pl-field pl-field-wide">
77 <label>Stock</label>
78 <select
79 className="pl-select"
80 value={platePresetId ?? ""}
81 onChange={(ev) => {
82 const p = PLATE_PRESETS.find((x) => x.id === ev.target.value);
83 if (p) set({ plate: { ...p.plate } });
84 }}
85 >
86 <option value="">custom</option>
87 {PLATE_PRESETS.map((p) => (
88 <option key={p.id} value={p.id}>
89 {p.name}
90 </option>
91 ))}
92 </select>
93 </div>
94 {platePresetId && <div className="pl-small pl-muted">{PLATE_PRESETS.find((p) => p.id === platePresetId)?.note}</div>}
95 <NumberField label="Length Lx" value={spec.plate.Lx} scale={1000} unit="mm" invalid={invalid("plate.Lx")} onChange={(v) => set({ plate: { ...spec.plate, Lx: v } })} />
96 <NumberField label="Width Ly" value={spec.plate.Ly} scale={1000} unit="mm" invalid={invalid("plate.Ly")} onChange={(v) => set({ plate: { ...spec.plate, Ly: v } })} />
97 <NumberField label="Thickness h" value={spec.plate.h} scale={1000} unit="mm" step={0.1} invalid={invalid("plate.h")} onChange={(v) => set({ plate: { ...spec.plate, h: v } })} />
98 </Group>
99 )}
100 {show("material") && (
101 <Group title="Material" issues={issues} prefix="material" open={isOpen("material")}>
102 <div className="pl-field pl-field-wide">
103 <label>Preset</label>
104 <select
105 className="pl-select"
106 value={materialPresetId ?? ""}
107 onChange={(ev) => {
108 const p = MATERIAL_PRESETS.find((x) => x.id === ev.target.value);
109 if (p) set({ material: { ...p.material }, damping: { ...p.damping } });
110 }}
111 >
112 <option value="">custom</option>
113 {MATERIAL_PRESETS.map((p) => (
114 <option key={p.id} value={p.id}>
115 {p.material.name}
116 </option>
117 ))}
118 </select>
119 </div>
120 {materialPresetId && (
121 <div className="pl-small pl-muted">{MATERIAL_PRESETS.find((p) => p.id === materialPresetId)?.source} Handbook values, not a certificate.</div>
122 )}
123 <NumberField label="Young's modulus E" value={spec.material.E} scale={1e-9} unit="GPa" invalid={invalid("material.E")} onChange={(v) => set({ material: { ...spec.material, E: v, name: customName(spec.material.name) } })} />
124 <NumberField label="Poisson's ratio ν" value={spec.material.nu} step={0.01} invalid={invalid("material.nu")} onChange={(v) => set({ material: { ...spec.material, nu: v, name: customName(spec.material.name) } })} />
125 <NumberField label="Density ρ" value={spec.material.rho} unit="kg/m³" invalid={invalid("material.rho")} onChange={(v) => set({ material: { ...spec.material, rho: v, name: customName(spec.material.name) } })} />
126 </Group>
127 )}
128 {show("edges") && (
129 <Group title="Edges" issues={issues} prefix="bc" open={isOpen("edges")}>
130 <div className="pl-field pl-field-wide">
131 <label>Condition</label>
132 <select className="pl-select" value={spec.bc} onChange={(ev) => set({ bc: ev.target.value as BoundaryCondition })}>
133 <option value="simply-supported">simply supported (w = 0, no moment)</option>
134 <option value="clamped">clamped (w = 0, no slope)</option>
135 </select>
136 </div>
137 {method && !method.supports.bc.includes(spec.bc) && (
138 <div className="pl-issue">{method.name} has no {spec.bc} solution; only the finite-difference method can run this.</div>
139 )}
140 <div className="pl-small pl-muted">Both are idealizations: a real clamp has finite stiffness and a real support some rotational restraint.</div>
141 </Group>
142 )}
143 {show("damping") && (
144 <Group title="Damping" issues={issues} prefix="damping" open={isOpen("damping")}>
145 <NumberField label="σ₀ (all frequencies)" value={spec.damping.sigma0} unit="1/s" step={0.1} invalid={invalid("damping")} onChange={(v) => set({ damping: { ...spec.damping, sigma0: v } })} />
146 <NumberField label="σ₁ (rises with frequency)" value={spec.damping.sigma1} unit="m²/s" step={0.001} invalid={invalid("damping")} onChange={(v) => set({ damping: { ...spec.damping, sigma1: v } })} />
147 <div className="pl-small pl-muted">A mode with Laplacian eigenvalue λ decays at rate σ₀ + σ₁λ. Illustrative values; real losses depend on the mounting.</div>
148 </Group>
149 )}
150 {show("excitation") && (
151 <Group title="Excitation" issues={issues} prefix="excitation" open={isOpen("excitation")}>
152 <div className="pl-field pl-field-wide">
153 <label>Type</label>
154 <select
155 className="pl-select"
156 value={e.type}
157 onChange={(ev) => {
158 const type = ev.target.value as Excitation["type"];
159 const next: Excitation =
160 type === "impulse"
161 ? { type, x: e.x, y: e.y, force: e.force, duration: 1e-3, radius: e.radius }
162 : { type, x: e.x, y: e.y, force: Math.min(e.force, 5), frequency: 500, radius: e.radius };
163 set({ excitation: next });
164 }}
165 >
166 <option value="impulse">hammer strike (half-sine pulse)</option>
167 <option value="harmonic">harmonic point force</option>
168 </select>
169 </div>
170 <NumberField label="Strike x" value={e.x} scale={1000} unit="mm" invalid={invalid("excitation")} onChange={(v) => set({ excitation: { ...e, x: v } })} />
171 <NumberField label="Strike y" value={e.y} scale={1000} unit="mm" invalid={invalid("excitation")} onChange={(v) => set({ excitation: { ...e, y: v } })} />
172 <NumberField label={e.type === "impulse" ? "Peak force" : "Force amplitude"} value={e.force} unit="N" invalid={invalid("excitation.force")} onChange={(v) => set({ excitation: { ...e, force: v } })} />
173 {e.type === "impulse" ? (
174 <NumberField label="Contact time" value={e.duration} scale={1000} unit="ms" step={0.1} invalid={invalid("excitation.duration")} onChange={(v) => set({ excitation: { ...e, duration: v } })} />
175 ) : (
176 <NumberField label="Frequency" value={e.frequency} unit="Hz" invalid={invalid("excitation.frequency")} onChange={(v) => set({ excitation: { ...e, frequency: v } })} />
177 )}
178 <NumberField label="Footprint radius" value={e.radius} scale={1000} unit="mm" step={1} invalid={invalid("excitation.radius")} onChange={(v) => set({ excitation: { ...e, radius: v } })} />
179 <div className="pl-small pl-muted">A finite footprint stands in for the contact patch; a point load would give infinite stress under the hammer.</div>
180 </Group>
181 )}
182 {show("probe") && (
183 <Group title="Probe" issues={issues} prefix="probe" open={isOpen("probe")}>
184 <NumberField label="x" value={spec.probe.x} scale={1000} unit="mm" invalid={invalid("probe")} onChange={(v) => set({ probe: { ...spec.probe, x: v } })} />
185 <NumberField label="y" value={spec.probe.y} scale={1000} unit="mm" invalid={invalid("probe")} onChange={(v) => set({ probe: { ...spec.probe, y: v } })} />
186 <div className="pl-small pl-muted">Time series and spectra are recorded here. Finite differences snap it to the nearest grid node.</div>
187 </Group>
188 )}
189 {show("listener") && (
190 <Group title="Listener (sound)" issues={issues} prefix="listener" open={isOpen("listener")}>
191 <NumberField label="x" value={spec.listener.x} scale={1000} unit="mm" onChange={(v) => set({ listener: { ...spec.listener, x: v } })} />
192 <NumberField label="y" value={spec.listener.y} scale={1000} unit="mm" onChange={(v) => set({ listener: { ...spec.listener, y: v } })} />
193 <NumberField label="Height z" value={spec.listener.z} unit="m" step={0.1} onChange={(v) => set({ listener: { ...spec.listener, z: v } })} />
194 <div className="pl-small pl-muted">Rayleigh integral in an infinite baffle: an estimate of the waveform, not a calibrated level.</div>
195 </Group>
196 )}
197 {show("output") && (
198 <Group title="Output" issues={issues} prefix="output" open={isOpen("output")}>
199 <NumberField label="Sample rate" value={spec.output.fs} unit="Hz" integer invalid={invalid("output.fs")} onChange={(v) => set({ output: { ...spec.output, fs: v } })} />
200 <NumberField label="Duration (modal, audio)" value={spec.output.duration} unit="s" step={0.1} invalid={invalid("output.duration")} onChange={(v) => set({ output: { ...spec.output, duration: v } })} />
201 <NumberField label="Animation frames" value={spec.output.nframes} integer invalid={invalid("output.nframes")} onChange={(v) => set({ output: { ...spec.output, nframes: v } })} />
202 <NumberField label="Display grid nx" value={spec.output.grid[0]} integer onChange={(v) => set({ output: { ...spec.output, grid: [v, spec.output.grid[1]] } })} />
203 <NumberField label="Display grid ny" value={spec.output.grid[1]} integer onChange={(v) => set({ output: { ...spec.output, grid: [spec.output.grid[0], v] } })} />
204 </Group>
205 )}
206 {show("modal") && (
207 <Group title="Modal method" issues={issues} prefix="modal" open={isOpen("modal")} disabled={method ? method.settingsKey !== "modal" : false}>
208 <NumberField label="Max frequency" value={spec.modal.fmax} unit="Hz" invalid={invalid("modal.fmax")} onChange={(v) => set({ modal: { ...spec.modal, fmax: v } })} />
209 <NumberField label="Max modes" value={spec.modal.max_modes} integer onChange={(v) => set({ modal: { ...spec.modal, max_modes: v } })} />
210 <div className="pl-small pl-muted">Truncation of the modal sum: displacement converges fast, stress near the strike slowly.</div>
211 </Group>
212 )}
213 {show("fd") && (
214 <Group title="Explicit finite differences" issues={issues} prefix="fd" open={isOpen("fd")} disabled={method ? method.settingsKey !== "fd" : false}>
215 <NumberField label="Grid nx" value={spec.fd.nx} integer min={5} invalid={invalid("fd")} onChange={(v) => set({ fd: { ...spec.fd, nx: v } })} />
216 <NumberField label="Grid ny" value={spec.fd.ny} integer min={5} invalid={invalid("fd")} onChange={(v) => set({ fd: { ...spec.fd, ny: v } })} />
217 <NumberField label="Duration" value={spec.fd.duration} scale={1000} unit="ms" step={0.5} onChange={(v) => set({ fd: { ...spec.fd, duration: v } })} />
218 <div className="pl-small" style={{ marginTop: 6 }}>Time step</div>
219 <CflSlider value={spec.fd.cfl} dtMax={fdDtMax} onChange={(v) => set({ fd: { ...spec.fd, cfl: v } })} />
220 </Group>
221 )}
222 {show("implicit") && (
223 <Group title="Implicit finite differences" issues={issues} prefix="implicit" open={isOpen("implicit")} disabled={method ? method.settingsKey !== "implicit" : false}>
224 <NumberField label="Grid nx" value={spec.implicit.nx} integer min={5} invalid={invalid("implicit")} onChange={(v) => set({ implicit: { ...spec.implicit, nx: v } })} />
225 <NumberField label="Grid ny" value={spec.implicit.ny} integer min={5} invalid={invalid("implicit")} onChange={(v) => set({ implicit: { ...spec.implicit, ny: v } })} />
226 <NumberField label="Duration" value={spec.implicit.duration} scale={1000} unit="ms" step={5} onChange={(v) => set({ implicit: { ...spec.implicit, duration: v } })} />
227 <NumberField label="Steps per sample" value={spec.implicit.steps_per_sample} step={0.5} min={0.25} invalid={invalid("implicit.steps_per_sample")} onChange={(v) => set({ implicit: { ...spec.implicit, steps_per_sample: v } })} />
228 <NumberField label="θ (time averaging)" value={spec.implicit.theta} step={0.05} min={0} max={1} invalid={invalid("implicit.theta")} onChange={(v) => set({ implicit: { ...spec.implicit, theta: v } })} />
229 <div className="pl-small pl-muted">No stability limit for θ ≥ 1/4; the step sets accuracy instead. More steps per sample tune the high modes better and cost proportionally.</div>
230 </Group>
231 )}
232 </div>
233 );
236function customName(name: string): string {
237 return name.endsWith(" (edited)") ? name : `${name} (edited)`;
240function Group(props: { title: string; prefix: string; issues: ValidationIssue[]; open: boolean; disabled?: boolean; children: ReactNode }) {
241 const related = props.issues.filter((i) => i.path === props.prefix || i.path.startsWith(props.prefix + "."));
242 return (
243 <details className={`pl-group${props.disabled ? " pl-disabled" : ""}`} open={props.open}>
244 <summary>
245 {props.title}
246 {related.some((i) => i.severity === "error") && <span className="pl-badge pl-badge-error" style={{ marginLeft: 6 }}>check</span>}
247 {props.disabled && <span className="pl-badge pl-badge-info" style={{ marginLeft: 6 }}>not used by this method</span>}
248 </summary>
249 {props.children}
250 {related.map((i) => (
251 <div key={i.path + i.message} className={`pl-issue${i.severity === "warning" ? " pl-issue-warning" : ""}`}>
252 {i.path}: {i.message}
253 </div>
254 ))}
255 </details>
256 );
↑↓ move↵openescclose