/ proof-of-concept / plate-dynamics-lab-2
Sign in
proof-of-concept / plate-dynamics-lab-2
plate-dynamics-lab-2 / apps / lab / src / views / ComparisonView.tsx
295 lines · 10.3 KBBlameHistoryRaw
1import { useMemo } from "react";
2import { methodById, type MethodId, type PlateResult, type ProblemSpec } from "@plate-dyn/core";
3import { AudioPlayer, LinePlot, useSharedAxis } from "@plate-dyn/components";
4import type { KeptResult } from "../App";
6export interface ComparisonViewProps {
7 results: Partial<Record<MethodId, KeptResult>>;
8 /** The settings on the bench now, to say whether the kept results match them */
9 current: ProblemSpec;
12const RR_COLOR = "#1f5fa8";
13const FEM_COLOR = "#d1495b";
15/**
16 * The last result of each method, side by side: the modes, the contact, the
17 * spectra, the sound. The two are independent solutions of the same object,
18 * so where they agree the agreement means something, and where they differ
19 * the difference has a cause the tables name.
20 */
21export function ComparisonView({ results, current }: ComparisonViewProps) {
22 const rr = results.rr;
23 const fem = results.fem;
24 const axis = useSharedAxis();
25 const spectrumAxis = useSharedAxis();
26 const forceAxis = useSharedAxis();
27 const sameInputs = rr && fem && sameProblem(rr.spec, fem.spec);
28 const rrStale = rr && !sameProblem(rr.spec, current);
29 const femStale = fem && !sameProblem(fem.spec, current);
30 const rows = useMemo(() => (rr && fem ? modeRows(rr.result, fem.result) : []), [rr, fem]);
32 if (!rr || !fem)
33 return (
34 <div className="empty">
35 <h2>Two results are needed</h2>
36 <p className="pl-muted">
37 {rr ? "Rayleigh-Ritz has run" : "Rayleigh-Ritz has not run"};{" "}
38 {fem ? "the finite-element method has run" : "the finite-element method has not run"}.
39 Press <strong>Run both</strong>, or run each method in turn, and the comparison lines up
40 the last result of each.
41 </p>
42 </div>
43 );
45 const a = rr.result;
46 const b = fem.result;
47 const ca = a.contact;
48 const cb = b.contact;
49 const pct = (x: number, y: number) => (x === 0 ? "–" : `${(((y - x) / x) * 100).toFixed(2)} %`);
50 const audioT = (r: PlateResult) =>
51 Float64Array.from({ length: r.audio.p.length }, (_, i) => i / r.audio.fs);
53 return (
54 <div className="results">
55 {!sameInputs && (
56 <div className="banner">
57 <span>
58 The two results come from different settings (the plate, the hammer, the strike or the
59 render differ), so this is not a like-for-like comparison. Run both on one set of
60 settings.
61 </span>
62 </div>
63 )}
64 {sameInputs && (rrStale || femStale) && (
65 <div className="banner">
66 <span>The settings changed after these runs; the comparison is of the earlier ones.</span>
67 </div>
68 )}
69 <div className="pl-small pl-muted">
70 {methodById("rr").name}: {a.info.size_note}. {methodById("fem").name}: {b.info.size_note}.
71 The two share the plate, the hammer, the damping constants and the radiation model, and
72 nothing else: different plate theory, different discretization, different eigensolver,
73 different contact.
74 </div>
76 <div className="pl-plot-title">The contact</div>
77 <table className="modes-table">
78 <thead>
79 <tr>
80 <th>Quantity</th>
81 <th>Rayleigh-Ritz</th>
82 <th>Finite elements</th>
83 <th>Difference</th>
84 </tr>
85 </thead>
86 <tbody>
87 <tr>
88 <td>Contact time</td>
89 <td>{(ca.tc * 1e6).toPrecision(4)} µs</td>
90 <td>{(cb.tc * 1e6).toPrecision(4)} µs</td>
91 <td>{pct(ca.tc, cb.tc)}</td>
92 </tr>
93 <tr>
94 <td>Peak force</td>
95 <td>{ca.peak_force.toPrecision(4)} N</td>
96 <td>{cb.peak_force.toPrecision(4)} N</td>
97 <td>{pct(ca.peak_force, cb.peak_force)}</td>
98 </tr>
99 <tr>
100 <td>Impulse</td>
101 <td>{(ca.impulse * 1e3).toPrecision(4)} mN s</td>
102 <td>{(cb.impulse * 1e3).toPrecision(4)} mN s</td>
103 <td>{pct(ca.impulse, cb.impulse)}</td>
104 </tr>
105 <tr>
106 <td>Plate under the hammer at separation</td>
107 <td>{ca.plate_exit_v.toPrecision(3)} m/s</td>
108 <td>{cb.plate_exit_v.toPrecision(3)} m/s</td>
109 <td>{pct(ca.plate_exit_v, cb.plate_exit_v)}</td>
110 </tr>
111 <tr>
112 <td>Hammer at separation</td>
113 <td>not modelled</td>
114 <td>{cb.hammer_exit_v.toPrecision(3)} m/s into the plate</td>
115 <td></td>
116 </tr>
117 <tr>
118 <td>Effective restitution</td>
119 <td>{ca.effective_restitution} (nominal)</td>
120 <td>{cb.effective_restitution.toPrecision(3)}</td>
121 <td></td>
122 </tr>
123 <tr>
124 <td>Audible modes</td>
125 <td>{a.info.n_audible}</td>
126 <td>{b.info.n_audible}</td>
127 <td></td>
128 </tr>
129 <tr>
130 <td>Modes stage</td>
131 <td>{fmtS(a.info.modes_wall_s)}</td>
132 <td>{fmtS(b.info.modes_wall_s)}</td>
133 <td></td>
134 </tr>
135 </tbody>
136 </table>
137 <div className="pl-small pl-muted">
138 The resonances agree to a percent or two although the theories, bases and eigensolvers
139 differ; the contact numbers differ sharply for a reason that can be named: the pulse is the
140 force on a rigid target, while the coupled model lets this very light plate move away under
141 the hammer, so the contact lasts longer, the force is far lower and less momentum is
142 transferred.
143 </div>
145 <LinePlot
146 title="Contact force"
147 series={[
148 { x: ca.t, y: ca.force, label: "Rayleigh-Ritz (prescribed)", color: RR_COLOR },
149 { x: cb.t, y: cb.force, label: "finite elements (coupled)", color: FEM_COLOR },
150 ]}
151 xScale={1e6}
152 xLabel="time [µs]"
153 yLabel="F [N]"
154 yUnit="N"
155 height={170}
156 {...forceAxis}
157 />
159 <div className="pl-plot-title">Modes, in order</div>
160 <div style={{ overflowX: "auto" }}>
161 <table className="modes-table">
162 <thead>
163 <tr>
164 <th>#</th>
165 <th>RR f [Hz]</th>
166 <th>FEM f [Hz]</th>
167 <th>Difference</th>
168 <th>RR |at the strike|</th>
169 <th>FEM |at the strike|</th>
170 <th>RR Q</th>
171 <th>FEM Q</th>
172 </tr>
173 </thead>
174 <tbody>
175 {rows.map((r) => (
176 <tr key={r.k}>
177 <td>{r.k + 1}</td>
178 <td>{r.fa === null ? "" : r.fa.toFixed(1)}</td>
179 <td>{r.fb === null ? "" : r.fb.toFixed(1)}</td>
180 <td>{r.fa !== null && r.fb !== null ? pct(r.fa, r.fb) : ""}</td>
181 <td>{r.ha === null ? "" : r.ha.toPrecision(3)}</td>
182 <td>{r.hb === null ? "" : r.hb.toPrecision(3)}</td>
183 <td>{r.qa === null ? "" : r.qa.toFixed(0)}</td>
184 <td>{r.qb === null ? "" : r.qb.toFixed(0)}</td>
185 </tr>
186 ))}
187 </tbody>
188 </table>
189 </div>
190 <div className="pl-small pl-muted">
191 Audible modes matched by order; the rigid-body modes of the finite-element method are left
192 out. The finite-element frequencies converge from above as its mesh is refined (the element
193 locks in shear), so at a coarse mesh they run high. Degenerate pairs of a square plate can
194 swap places between the two lists.
195 </div>
197 <LinePlot
198 title="Amplitude spectrum of the sound"
199 series={[
200 { x: a.spectrum.f, y: a.spectrum.p, label: "Rayleigh-Ritz", color: RR_COLOR },
201 { x: b.spectrum.f, y: b.spectrum.p, label: "finite elements", color: FEM_COLOR },
202 ]}
203 defaultXRange={[0, Math.max(a.info.f_max, b.info.f_max)]}
204 logY
205 xLabel="frequency [Hz]"
206 yLabel="amplitude [Pa]"
207 height={200}
208 {...spectrumAxis}
209 />
210 <LinePlot
211 title="Pressure at the microphone, unnormalized"
212 series={[
213 { x: audioT(a), y: a.audio.p, label: "Rayleigh-Ritz", color: RR_COLOR },
214 { x: audioT(b), y: b.audio.p, label: "finite elements", color: FEM_COLOR },
215 ]}
216 defaultXRange={[0, 0.003]}
217 xLabel="time [s]"
218 yLabel="p [Pa]"
219 yUnit="Pa"
220 height={150}
221 {...axis}
222 />
223 <div className="pl-small pl-muted">
224 The absolute levels differ with the impulse; the exports below are each peak-normalized, so
225 compare pitch, spectral balance and decay, not loudness.
226 </div>
227 <div className="two-up">
228 <div>
229 <div className="pl-plot-title">Rayleigh-Ritz</div>
230 <AudioPlayer
231 p={a.audio.p}
232 fs={a.audio.fs}
233 normalizeDbfs={a.audio.normalize_dbfs}
234 filename="plate-rr"
235 />
236 </div>
237 <div>
238 <div className="pl-plot-title">Finite elements</div>
239 <AudioPlayer
240 p={b.audio.p}
241 fs={b.audio.fs}
242 normalizeDbfs={b.audio.normalize_dbfs}
243 filename="plate-fem"
244 />
245 </div>
246 </div>
247 </div>
248 );
251interface ModeRow {
252 k: number;
253 fa: number | null;
254 fb: number | null;
255 ha: number | null;
256 hb: number | null;
257 qa: number | null;
258 qb: number | null;
261function modeRows(a: PlateResult, b: PlateResult): ModeRow[] {
262 const pick = (r: PlateResult) => {
263 const idx: number[] = [];
264 r.modes.audible.forEach((ok, k) => ok && idx.push(k));
265 return idx;
266 };
267 const ia = pick(a);
268 const ib = pick(b);
269 const n = Math.max(ia.length, ib.length);
270 const rows: ModeRow[] = [];
271 for (let k = 0; k < n; k++) {
272 const ka = ia[k];
273 const kb = ib[k];
274 rows.push({
275 k,
276 fa: ka === undefined ? null : a.modes.freq[ka],
277 fb: kb === undefined ? null : b.modes.freq[kb],
278 ha: ka === undefined ? null : Math.abs(a.modes.hit[ka]),
279 hb: kb === undefined ? null : Math.abs(b.modes.hit[kb]),
280 qa: ka === undefined ? null : a.modes.q[ka],
281 qb: kb === undefined ? null : b.modes.q[kb],
282 });
283 }
284 return rows;
287/** Whether two specs describe the same physical problem (the numerics may differ). */
288function sameProblem(x: ProblemSpec, y: ProblemSpec): boolean {
289 const strip = (s: ProblemSpec) => JSON.stringify({ ...s, rr: undefined, fem: undefined });
290 return strip(x) === strip(y);
293function fmtS(s: number): string {
294 return s < 1 ? `${(s * 1000).toFixed(0)} ms` : `${s.toFixed(2)} s`;
↑↓ move↵openescclose