import { useMemo } from "react"; import { methodById, type MethodId, type PlateResult, type ProblemSpec } from "@plate-dyn/core"; import { AudioPlayer, LinePlot, useSharedAxis } from "@plate-dyn/components"; import type { KeptResult } from "../App"; export interface ComparisonViewProps { results: Partial>; /** The settings on the bench now, to say whether the kept results match them */ current: ProblemSpec; } const RR_COLOR = "#1f5fa8"; const FEM_COLOR = "#d1495b"; /** * The last result of each method, side by side: the modes, the contact, the * spectra, the sound. The two are independent solutions of the same object, * so where they agree the agreement means something, and where they differ * the difference has a cause the tables name. */ export function ComparisonView({ results, current }: ComparisonViewProps) { const rr = results.rr; const fem = results.fem; const axis = useSharedAxis(); const spectrumAxis = useSharedAxis(); const forceAxis = useSharedAxis(); const sameInputs = rr && fem && sameProblem(rr.spec, fem.spec); const rrStale = rr && !sameProblem(rr.spec, current); const femStale = fem && !sameProblem(fem.spec, current); const rows = useMemo(() => (rr && fem ? modeRows(rr.result, fem.result) : []), [rr, fem]); if (!rr || !fem) return (

Two results are needed

{rr ? "Rayleigh-Ritz has run" : "Rayleigh-Ritz has not run"};{" "} {fem ? "the finite-element method has run" : "the finite-element method has not run"}. Press Run both, or run each method in turn, and the comparison lines up the last result of each.

); const a = rr.result; const b = fem.result; const ca = a.contact; const cb = b.contact; const pct = (x: number, y: number) => (x === 0 ? "–" : `${(((y - x) / x) * 100).toFixed(2)} %`); const audioT = (r: PlateResult) => Float64Array.from({ length: r.audio.p.length }, (_, i) => i / r.audio.fs); return (
{!sameInputs && (
The two results come from different settings (the plate, the hammer, the strike or the render differ), so this is not a like-for-like comparison. Run both on one set of settings.
)} {sameInputs && (rrStale || femStale) && (
The settings changed after these runs; the comparison is of the earlier ones.
)}
{methodById("rr").name}: {a.info.size_note}. {methodById("fem").name}: {b.info.size_note}. The two share the plate, the hammer, the damping constants and the radiation model, and nothing else: different plate theory, different discretization, different eigensolver, different contact.
The contact
Quantity Rayleigh-Ritz Finite elements Difference
Contact time {(ca.tc * 1e6).toPrecision(4)} µs {(cb.tc * 1e6).toPrecision(4)} µs {pct(ca.tc, cb.tc)}
Peak force {ca.peak_force.toPrecision(4)} N {cb.peak_force.toPrecision(4)} N {pct(ca.peak_force, cb.peak_force)}
Impulse {(ca.impulse * 1e3).toPrecision(4)} mN s {(cb.impulse * 1e3).toPrecision(4)} mN s {pct(ca.impulse, cb.impulse)}
Plate under the hammer at separation {ca.plate_exit_v.toPrecision(3)} m/s {cb.plate_exit_v.toPrecision(3)} m/s {pct(ca.plate_exit_v, cb.plate_exit_v)}
Hammer at separation not modelled {cb.hammer_exit_v.toPrecision(3)} m/s into the plate
Effective restitution {ca.effective_restitution} (nominal) {cb.effective_restitution.toPrecision(3)}
Audible modes {a.info.n_audible} {b.info.n_audible}
Modes stage {fmtS(a.info.modes_wall_s)} {fmtS(b.info.modes_wall_s)}
The resonances agree to a percent or two although the theories, bases and eigensolvers differ; the contact numbers differ sharply for a reason that can be named: the pulse is the force on a rigid target, while the coupled model lets this very light plate move away under the hammer, so the contact lasts longer, the force is far lower and less momentum is transferred.
Modes, in order
{rows.map((r) => ( ))}
# RR f [Hz] FEM f [Hz] Difference RR |at the strike| FEM |at the strike| RR Q FEM Q
{r.k + 1} {r.fa === null ? "" : r.fa.toFixed(1)} {r.fb === null ? "" : r.fb.toFixed(1)} {r.fa !== null && r.fb !== null ? pct(r.fa, r.fb) : ""} {r.ha === null ? "" : r.ha.toPrecision(3)} {r.hb === null ? "" : r.hb.toPrecision(3)} {r.qa === null ? "" : r.qa.toFixed(0)} {r.qb === null ? "" : r.qb.toFixed(0)}
Audible modes matched by order; the rigid-body modes of the finite-element method are left out. The finite-element frequencies converge from above as its mesh is refined (the element locks in shear), so at a coarse mesh they run high. Degenerate pairs of a square plate can swap places between the two lists.
The absolute levels differ with the impulse; the exports below are each peak-normalized, so compare pitch, spectral balance and decay, not loudness.
Rayleigh-Ritz
Finite elements
); } interface ModeRow { k: number; fa: number | null; fb: number | null; ha: number | null; hb: number | null; qa: number | null; qb: number | null; } function modeRows(a: PlateResult, b: PlateResult): ModeRow[] { const pick = (r: PlateResult) => { const idx: number[] = []; r.modes.audible.forEach((ok, k) => ok && idx.push(k)); return idx; }; const ia = pick(a); const ib = pick(b); const n = Math.max(ia.length, ib.length); const rows: ModeRow[] = []; for (let k = 0; k < n; k++) { const ka = ia[k]; const kb = ib[k]; rows.push({ k, fa: ka === undefined ? null : a.modes.freq[ka], fb: kb === undefined ? null : b.modes.freq[kb], ha: ka === undefined ? null : Math.abs(a.modes.hit[ka]), hb: kb === undefined ? null : Math.abs(b.modes.hit[kb]), qa: ka === undefined ? null : a.modes.q[ka], qb: kb === undefined ? null : b.modes.q[kb], }); } return rows; } /** Whether two specs describe the same physical problem (the numerics may differ). */ function sameProblem(x: ProblemSpec, y: ProblemSpec): boolean { const strip = (s: ProblemSpec) => JSON.stringify({ ...s, rr: undefined, fem: undefined }); return strip(x) === strip(y); } function fmtS(s: number): string { return s < 1 ? `${(s * 1000).toFixed(0)} ms` : `${s.toFixed(2)} s`; }