/ tutorials / wave-acoustics
Sign in
tutorials / wave-acoustics
wave-acoustics / src / chapters / membrane / panels.tsx
237 lines · 7.3 KBCodeBlameHistory
d8098edAn interactive tutorial on the wave equation in acoustics: strings, drums and rooms, measured by earJeremy Magland 1// Panels for the square-membrane labs of Chapters 4 and 5.
2import type { Num, Params } from '../../engine/types.ts'
3import Listen, { type Clip } from '../../audio/Listen.tsx'
4import { Row, SeriesPlot, NumTable, fmtCents, fmtErr, fmtMs, fmtNum } from '../common/panels.tsx'
5import { faded, familyColor, familyShapes, ink, method, type MethodKey } from '../../plot/palette.ts'
6import type { Control } from '../../ui/types.ts'
8export interface StrikeOut {
9 ms: number
10 N: number
11 lam: number
12 fs: number
13 rel: number
14 nmodes: number
15 f11: number
16 fr: number[]
17 dbe: number[]
18 dbf: number[]
19 fmodes: number[]
20 tw: number[]
21 yw: number[]
22 yew: number[]
23 clipExact: Clip
24 clipFd: Clip
25 clipDiff: Clip
28export interface CentsOut {
29 N: number
30 lams: number[]
31 m: number[]
32 n: number[]
33 ring: number[]
34 rs: number[]
35 angle: number[]
36 ppw: number[]
37 meas: Num[][]
38 pred: Num[][]
39 agree: number
42export interface OrdersOut {
43 Ns: number[]
44 schemes: string[]
45 lams: number[]
46 modes: number[][]
47 cents: Num[][]
48 worst: Num[][]
49 ms: Num[][]
50 orders: Num[][]
53/**
54 * Which colour and shape each scheme wears, in every chapter. Fourth order
55 * in space alone borrows the waveguide's amber, since no chapter shows both;
56 * the spare red is too close to fd4's orange to sit beside it.
57 */
58export const SCHEME: Record<string, { key: MethodKey; label: string; shape: 'circle' | 'square' | 'triangle' }> = {
59 fd2: { key: 'fd2', label: 'second order (leapfrog, five-point)', shape: 'circle' },
60 fd4: { key: 'waveguide', label: 'fourth order in space only', shape: 'square' },
61 fd44: { key: 'fd4', label: 'fourth order in space and time', shape: 'triangle' },
64/** The drum of Chapters 4 and 5: 150 Hz fundamental on a unit square. */
65export const DRUM = { c: 150 * Math.SQRT2, kmax: 90, width: 0.03, xr: 0.77, yr: 0.63, fmax: 3000 }
67export const STRIKE_CONTROL: Control = {
68 kind: 'select',
69 key: 'at',
70 label: 'strike at',
71 options: [
72 { value: 'off', label: 'off centre, (0.31, 0.42)' },
73 { value: 'centre', label: 'the centre' },
74 { value: 'rim', label: 'near the rim, (0.12, 0.2)' },
75 ],
78export function strikeParams(p: Params, scheme: string) {
79 const at = String(p.at ?? 'off')
80 const [xs, ys] = at === 'centre' ? [0.5, 0.5] : at === 'rim' ? [0.12, 0.2] : [0.31, 0.42]
81 return { mode: 'strike', ...DRUM, N: p.N, lam: p.lam, T: 1.0, xs, ys, scheme }
84export function StrikePanels({ d, scheme = 'fd2' }: { d: StrikeOut; scheme?: string }) {
85 const col = method[SCHEME[scheme].key]
86 return (
87 <>
88 <SeriesPlot
89 title="the recording from 0.1 s after the blow, over four periods of the fundamental"
90 height={180}
91 xLabel="ms"
92 series={[
93 { x: d.tw, y: d.yew, color: ink.reference, label: 'exact (modal sum)', width: 2.6 },
94 { x: d.tw, y: d.yw, color: col, label: `N = ${d.N}, λ = ${d.lam}`, width: 1.3 },
95 ]}
96 />
97 <SeriesPlot
98 title="spectrum of the first 0.4 s; faint lines are the exact mode frequencies the blow excites"
99 height={230}
100 xLabel="Hz"
101 yLabel="dB"
102 yDomain={[-90, 3]}
103 vlines={[{ x: d.fmodes, color: faded(ink.muted, 0.5) }]}
104 series={[
105 { x: d.fr, y: d.dbe, color: ink.reference, label: 'exact', width: 1.8 },
106 { x: d.fr, y: d.dbf, color: col, label: 'simulated', width: 1.2 },
107 ]}
108 />
109 <Listen
110 items={[
111 { label: 'exact', clip: d.clipExact },
112 { label: 'simulated', clip: d.clipFd },
113 { label: 'the difference', clip: d.clipDiff },
114 ]}
115 note={`all three at the same scale, at the simulation's own rate of ${(d.fs / 1000).toFixed(1)} kHz`}
116 />
117 </>
118 )
121export function strikeSummary(d: StrikeOut) {
122 return [
123 { label: 'sample rate 1/dt', value: `${(d.fs / 1000).toFixed(2)} kHz` },
124 { label: 'modes in the exact sum', value: String(d.nmodes) },
125 { label: 'relative rms error', value: fmtErr(d.rel) },
126 ]
129export function CentsPanels({ d }: { d: CentsOut }) {
130 const nl = d.lams.length
131 return (
132 <Row>
133 {d.rs.map((R, r) => {
134 const idx = d.ring.map((g, i) => (g === r + 1 ? i : -1)).filter((i) => i >= 0)
135 const ppw = (2 * d.N) / R
136 return (
137 <SeriesPlot
138 key={R}
139 title={`|k| ≈ ${R}π: about ${fmtNum(ppw, 2)} points per wavelength`}
140 height={220}
141 xDomain={[0, 90]}
142 xTicks={[0, 15, 30, 45, 60, 75, 90]}
143 xLabel="direction, degrees from the x axis"
144 yLabel="cents"
145 series={d.lams.flatMap((l, k) => [
146 {
147 x: idx.map((i) => d.angle[i]),
148 y: idx.map((i) => d.pred[k][i]),
149 color: familyColor(k, nl),
150 width: 1.1,
151 noLegend: true,
152 },
153 {
154 x: idx.map((i) => d.angle[i]),
155 y: idx.map((i) => d.meas[k][i]),
156 color: familyColor(k, nl),
157 shape: familyShapes[k],
158 line: false,
159 label: `λ = ${fmtNum(l, 3)}`,
160 tip: (j: number) => {
161 const i = idx[j]
162 return [`mode (${d.m[i]}, ${d.n[i]}), ${d.angle[i].toFixed(1)}°`, `measured ${fmtCents(d.meas[k][i])}`]
163 },
164 },
165 ])}
166 />
167 )
168 })}
169 </Row>
170 )
173export function OrdersPanels({ d }: { d: OrdersOut }) {
174 const sc = d.schemes.map((s) => SCHEME[s])
175 const nm = d.modes.length
176 const tip = (s: number) => (j: number) => [
177 `N = ${d.Ns[j]}, run ${fmtMs(d.ms[s][j])}`,
178 ...d.modes.map((m, q) => `mode (${m[0]},${m[1]}): ${fmtCents(d.cents[s * nm + q][j])}`),
179 ]
180 return (
181 <>
182 <Row>
183 <SeriesPlot
184 title="worst pitch error of the three test modes, against the grid"
185 height={250}
186 xLog
187 yLog
188 yDomain={[1e-3, 3000]}
189 xLabel="grid intervals N"
190 xTicks={d.Ns}
191 yLabel="|cents|"
192 hlines={[{ y: 5, label: '5 cents' }]}
193 refs={[
194 { p: -2, through: [40, 150], label: 'slope −2', span: 0.35, labelAt: 'end' },
195 { p: -4, through: [40, 12], label: 'slope −4', span: 0.35 },
196 ]}
197 series={sc.map((s, k) => ({
198 x: d.Ns,
199 y: d.worst[k],
200 color: method[s.key],
201 shape: s.shape,
202 label: s.label,
203 tip: tip(k),
204 }))}
205 />
206 <SeriesPlot
207 title="the same errors against the time each run took"
208 height={250}
209 xLog
210 yLog
211 yDomain={[1e-3, 3000]}
212 xLabel="ms"
213 yLabel="|cents|"
214 hlines={[{ y: 5, label: '5 cents' }]}
215 series={sc.map((s, k) => ({
216 x: d.ms[k],
217 y: d.worst[k],
218 color: method[s.key],
219 shape: s.shape,
220 label: s.label,
221 tip: tip(k),
222 }))}
223 legend={false}
224 />
225 </Row>
226 <NumTable
227 caption="worst pitch error in cents, and its observed order between successive grids"
228 head={['N', ...d.schemes.map((s) => `${s} error`), ...d.schemes.map((s) => `${s} order`)]}
229 rows={d.Ns.map((N, j) => [
230 N,
231 ...d.schemes.map((_, s) => fmtNum(d.worst[s][j], 3)),
232 ...d.schemes.map((_, s) => fmtNum(d.orders[s][j], 3)),
233 ])}
234 />
235 </>
236 )
↑↓ move↵openescclose