/ proof-of-concept / plate-dynamics-lab-2
Sign in
proof-of-concept / plate-dynamics-lab-2
plate-dynamics-lab-2 / packages / core / test / rr.test.ts
121 lines · 5.1 KBBlameHistoryRaw
1import { describe, expect, it } from "vitest";
2import {
3 audibleFreqs,
4 isolatedAudible,
5 maxAbs,
6 reference,
7 referenceWav,
8 solve,
9 solveSingle,
10 testSpec,
11} from "./helpers";
13/**
14 * The Rayleigh-Ritz method against the diagnostics of aluminum_plate_sim.py
15 * (sim-docs): the same basis, quadrature, hole correction, thread spring,
16 * damping, pulse and radiation model, so the numbers should agree to
17 * rounding, and the first 50 ms of the peak-normalized sound to the 24-bit
18 * quantization of the reference WAV.
19 */
20describe("Rayleigh-Ritz against the reference simulator", () => {
21 it("reproduces the centre hit: modes, contact, sound", async () => {
22 const ref = reference("rr_center");
23 const { result } = await solve(testSpec("center-hit"), "rr");
24 const f = audibleFreqs(result);
25 expect(f.length).toBe(ref.modes.length);
26 ref.modes.forEach((m, k) => {
27 expect(Math.abs(f[k] - m.frequency_hz) / m.frequency_hz).toBeLessThan(1e-7);
28 });
29 // damping and participations, on modes that are not part of a degenerate pair
30 for (const k of isolatedAudible(result)) {
31 const m = ref.modes[k];
32 expect(result.modes.q[k]).toBeCloseTo(m.q_total, 3);
33 expect(Math.abs(result.modes.hit[k])).toBeCloseTo(Math.abs(m.hit_participation), 5);
34 expect(Math.abs(result.modes.radiation[k])).toBeCloseTo(
35 Math.abs(m.radiation_participation),
36 8,
37 );
38 }
39 const c = result.contact;
40 expect(c.tc).toBeCloseTo(ref.derived.contact_duration_s, 12);
41 expect(c.impulse).toBeCloseTo(ref.derived.impact_impulse_Ns, 10);
42 expect(c.peak_force).toBeCloseTo(ref.derived.estimated_peak_contact_force_N, 6);
43 expect(result.info.critical_frequency).toBeCloseTo(ref.derived.critical_frequency_hz, 6);
44 expect(result.info.plate_mass).toBeCloseTo(ref.derived.plate_mass_kg, 12);
45 // the sound: the peak of the reference lies in the first 50 ms, so a 100 ms render normalizes alike
46 const wav = referenceWav("rr_center");
47 expect(result.audio.fs).toBe(wav.fs);
48 const a = result.audio.p;
49 const g = result.audio.gain;
50 let worst = 0;
51 for (let i = 0; i < wav.samples.length; i++)
52 worst = Math.max(worst, Math.abs(a[i] * g - wav.samples[i]));
53 expect(worst).toBeLessThan(1e-4);
54 expect(result.audio.peak_pa).toBeCloseTo(ref.derived.unnormalized_peak_pressure_pa_approx, 6);
55 });
57 it("reproduces the off-centre hit, which wakes the asymmetric modes", async () => {
58 const ref = reference("rr_off");
59 const { result } = await solve(testSpec("off-center-hit"), "rr");
60 // the modes are the same (the strike does not change them) ...
61 const f = audibleFreqs(result);
62 ref.modes.forEach((m, k) => {
63 expect(Math.abs(f[k] - m.frequency_hz) / m.frequency_hz).toBeLessThan(1e-7);
64 });
65 // ... but the lowest mode, silent at the centre, now takes part
66 expect(Math.abs(result.modes.hit[0])).toBeGreaterThan(1);
67 for (const k of isolatedAudible(result))
68 expect(Math.abs(result.modes.hit[k])).toBeCloseTo(
69 Math.abs(ref.modes[k].hit_participation),
70 5,
71 );
72 const wav = referenceWav("rr_off");
73 const a = result.audio.p;
74 const g = result.audio.gain;
75 let worst = 0;
76 for (let i = 0; i < wav.samples.length; i++)
77 worst = Math.max(worst, Math.abs(a[i] * g - wav.samples[i]));
78 expect(worst).toBeLessThan(1e-4);
79 });
81 it("is stable in the basis order: order 8 moves the first ten modes by less than 0.05 Hz", async () => {
82 const ref = reference("rr_center_o8");
83 const spec = testSpec();
84 spec.rr.basis_order = 8;
85 spec.rr.quadrature_order = 20;
86 const { result } = await solve(spec, "rr");
87 const f = audibleFreqs(result);
88 ref.modes.slice(0, 10).forEach((m, k) => {
89 expect(Math.abs(f[k] - m.frequency_hz)).toBeLessThan(0.05);
90 });
91 });
93 it("gives the same result as two stages and as one script", async () => {
94 const spec = testSpec();
95 spec.render.duration = 0.02;
96 const [staged, single] = await Promise.all([solve(spec, "rr"), solveSingle(spec, "rr")]);
97 expect(maxAbs(staged.result.audio.p)).toBeGreaterThan(0);
98 let worst = 0;
99 for (let i = 0; i < single.audio.p.length; i++)
100 worst = Math.max(worst, Math.abs(single.audio.p[i] - staged.result.audio.p[i]));
101 expect(worst).toBeLessThan(1e-12 * maxAbs(single.audio.p));
102 expect(single.frames.w.data.length).toBe(staged.result.frames.w.data.length);
103 });
105 it("carries the mode shapes, curvatures and frames the views draw", async () => {
106 const spec = testSpec();
107 spec.render.duration = 0.02;
108 const { result } = await solve(spec, "rr");
109 const g = spec.rr.grid;
110 expect(result.grid.x.length).toBe(g);
111 expect(result.modes.shapes.nx).toBe(g);
112 expect(result.modes.shapes.nf).toBe(result.modes.n);
113 expect(result.frames.w.nf).toBe(spec.render.nframes);
114 expect(result.frames.vm.nf).toBe(spec.render.nframes);
115 expect(maxAbs(result.frames.w.data)).toBeGreaterThan(0);
116 expect(maxAbs(result.envelope.vm.data)).toBeGreaterThan(0);
117 expect(result.probe.w.length).toBe(result.t.length);
118 expect(result.info.n_rigid).toBe(0);
119 expect(result.contact.hammer_v.length).toBe(0);
120 });
121});
↑↓ move↵openescclose