import type { ReactNode } from "react"; import { MATERIAL_PRESETS, PLATE_PRESETS, type BoundaryCondition, type Excitation, type MethodDefinition, type PlateShape, type ProblemSpec, type ValidationIssue, } from "@plate-lab/core"; import { NumberField } from "./NumberField"; import { CflSlider } from "./CflSlider"; import type { WebGpuSupport } from "../hooks/useWebGpu"; export type ParameterSection = | "plate" | "material" | "edges" | "damping" | "excitation" | "probe" | "listener" | "output" | "modal" | "fd" | "implicit"; export const ALL_SECTIONS: ParameterSection[] = [ "plate", "material", "edges", "damping", "excitation", "probe", "listener", "output", "modal", "fd", "implicit", ]; export interface ParameterPanelProps { spec: ProblemSpec; onChange: (spec: ProblemSpec) => void; /** Sections to show; default all */ sections?: ParameterSection[]; /** Grey out settings the method ignores */ method?: MethodDefinition; issues?: ValidationIssue[]; /** Largest stable FD step, when known, for the CFL slider */ fdDtMax?: number; /** Sections open initially; default all */ open?: ParameterSection[]; /** The GPU option of the explicit scheme: whether it is on, and what this browser offers */ fdGpu?: { enabled: boolean; onChange: (v: boolean) => void; support: WebGpuSupport }; } /** Controls that edit the spec. Every control maps to one field of the `params` struct. */ export function ParameterPanel(props: ParameterPanelProps) { const { spec, onChange, sections = ALL_SECTIONS, method, issues = [], fdDtMax, open, fdGpu, } = props; const set = (patch: Partial) => onChange({ ...spec, ...patch }); const issue = (path: string) => issues.find((i) => i.path === path || i.path.startsWith(path + ".")); const invalid = (path: string) => issue(path)?.severity === "error"; const show = (s: ParameterSection) => sections.includes(s); const isOpen = (s: ParameterSection) => (open ? open.includes(s) : true); const e = spec.excitation; const materialPresetId = MATERIAL_PRESETS.find( (p) => p.material.name === spec.material.name && p.material.E === spec.material.E, )?.id; const platePresetId = PLATE_PRESETS.find( (p) => p.plate.shape === spec.plate.shape && p.plate.Lx === spec.plate.Lx && p.plate.Ly === spec.plate.Ly && p.plate.h === spec.plate.h, )?.id; const ellipse = spec.plate.shape === "ellipse"; return (
{show("plate") && (
{platePresetId && (
{PLATE_PRESETS.find((p) => p.id === platePresetId)?.note}
)}
{method && !method.supports.shape.includes(spec.plate.shape) && (
{method.name} has no {spec.plate.shape} solution; the finite-difference methods embed it in their grid.
)} set({ plate: { ...spec.plate, Lx: v } })} /> set({ plate: { ...spec.plate, Ly: v } })} /> set({ plate: { ...spec.plate, h: v } })} /> {ellipse && (
The ellipse fills the Lx by Ly box, and positions are still measured from the box's corner. The finite-difference grid spans the box; the edge becomes a staircase of nodes, first order in the grid spacing.
)}
)} {show("material") && (
{materialPresetId && (
{MATERIAL_PRESETS.find((p) => p.id === materialPresetId)?.source} Handbook values, not a certificate.
)} set({ material: { ...spec.material, E: v, name: customName(spec.material.name) } }) } /> set({ material: { ...spec.material, nu: v, name: customName(spec.material.name) } }) } /> set({ material: { ...spec.material, rho: v, name: customName(spec.material.name) } }) } />
)} {show("edges") && (
{method && !method.supports.bc.includes(spec.bc) && (
{method.name} has no {spec.bc} solution; the finite-difference methods can run this.
)}
All three are idealizations: a real clamp has finite stiffness, a real support some rotational restraint, a free plate rests on something that takes its rigid-body motion, which the solvers remove, and a stand holds a cymbal over a washer, not at a point.
)} {show("damping") && ( set({ damping: { ...spec.damping, sigma0: v } })} /> set({ damping: { ...spec.damping, sigma1: v } })} />
A mode with Laplacian eigenvalue λ decays at rate σ₀ + σ₁λ. Illustrative values; real losses depend on the mounting.
)} {show("excitation") && (
set({ excitation: { ...e, x: v } })} /> set({ excitation: { ...e, y: v } })} /> set({ excitation: { ...e, force: v } })} /> {e.type === "impulse" ? ( set({ excitation: { ...e, duration: v } })} /> ) : ( set({ excitation: { ...e, frequency: v } })} /> )} set({ excitation: { ...e, radius: v } })} />
A finite footprint stands in for the contact patch; a point load would give infinite stress under the hammer.
)} {show("probe") && ( set({ probe: { ...spec.probe, x: v } })} /> set({ probe: { ...spec.probe, y: v } })} />
Time series and spectra are recorded here. Finite differences snap it to the nearest grid node.
)} {show("listener") && ( set({ listener: { ...spec.listener, x: v } })} /> set({ listener: { ...spec.listener, y: v } })} /> set({ listener: { ...spec.listener, z: v } })} />
Rayleigh integral in an infinite baffle: an estimate of the waveform, not a calibrated level.
)} {show("output") && ( set({ output: { ...spec.output, fs: v } })} /> set({ output: { ...spec.output, duration: v } })} /> set({ output: { ...spec.output, nframes: v } })} /> set({ output: { ...spec.output, grid: [v, spec.output.grid[1]] } })} /> set({ output: { ...spec.output, grid: [spec.output.grid[0], v] } })} /> )} {show("modal") && ( set({ modal: { ...spec.modal, fmax: v } })} /> set({ modal: { ...spec.modal, max_modes: v } })} />
Truncation of the modal sum: displacement converges fast, stress near the strike slowly.
)} {show("fd") && ( set({ fd: { ...spec.fd, nx: v } })} /> set({ fd: { ...spec.fd, ny: v } })} /> set({ fd: { ...spec.fd, duration: v } })} />
Time step
set({ fd: { ...spec.fd, cfl: v } })} /> {fdGpu && }
)} {show("implicit") && ( set({ implicit: { ...spec.implicit, nx: v } })} /> set({ implicit: { ...spec.implicit, ny: v } })} /> set({ implicit: { ...spec.implicit, duration: v } })} /> set({ implicit: { ...spec.implicit, steps_per_sample: v } })} /> set({ implicit: { ...spec.implicit, theta: v } })} />
No stability limit for θ ≥ 1/4; the step sets accuracy instead. More steps per sample tune the high modes better and cost proportionally.
)}
); } /** * The explicit scheme's time loop has a WebGPU port. The option is off by * default: the double-precision loop in numbl is the reference, and the port is * the same arithmetic in single precision, many times faster. */ function GpuToggle({ enabled, onChange, support, }: { enabled: boolean; onChange: (v: boolean) => void; support: WebGpuSupport; }) { const available = support.status === "available"; return (
{support.status === "checking" && "Looking for WebGPU…"} {support.status === "unavailable" && `Not available here (${support.reason}); the loop runs in numbl.`} {support.status === "available" && (enabled ? `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.` : `Available on ${support.adapter}. The loop then runs in single precision, typically 10 to 50 times faster than in numbl.`)}
); } function customName(name: string): string { return name.endsWith(" (edited)") ? name : `${name} (edited)`; } function Group(props: { title: string; prefix: string; issues: ValidationIssue[]; open: boolean; disabled?: boolean; children: ReactNode; }) { const related = props.issues.filter( (i) => i.path === props.prefix || i.path.startsWith(props.prefix + "."), ); return (
{props.title} {related.some((i) => i.severity === "error") && ( check )} {props.disabled && ( not used by this method )} {props.children} {related.map((i) => (
{i.path}: {i.message}
))}
); }