d8098edAn interactive tutorial on the wave equation in acoustics: strings, drums and rooms, measured by earJeremy Magland 1import type { Chapter } from '../../ui/types.ts'
2import driver from './driver.m?raw'
3import script from './room3d.m?raw'
4import text from './text.md?raw'
5import Listen, { type Clip } from '../../audio/Listen.tsx'
6import { Row, SeriesPlot, fmtErr, fmtMs, fmtNum } from '../common/panels.tsx'
7import { faded, ink, method } from '../../plot/palette.ts'
9interface ConvergeOut {
10 Ns: number[]
11 err: number[]
12 ms: number[]
13 cells: number[]
14 ns: number[]
15 orders: (number | null)[]
16 fhi: number
17 ppw: number[]
18 fullband: number
19 rate: number
20 fullcells: number
21 lowband: number
22 t: number[]
23 y: number[]
24 ye: number[]
25 t1: number[]
26 y1: number[]
27}
29interface HearOut {
30 Ns: number[]
31 fsk: number[]
32 clips: Clip[]
33 clipExact: Clip
34}
36const BOX = { L: [3.2, 2.4, 2.0], lam: 0.55, sigma: 0.0005, xs: [0.8, 0.6, 0.8], xr: [2.4, 1.6, 1.2] }
38function duration(sec: number): string {
39 if (sec < 120) return `${sec.toFixed(0)} s`
40 if (sec < 7200) return `${(sec / 60).toFixed(0)} minutes`
41 if (sec < 2 * 86400) return `${(sec / 3600).toFixed(1)} hours`
42 if (sec < 2 * 365 * 86400) return `${(sec / 86400).toFixed(0)} days`
43 return `${(sec / (365.25 * 86400)).toFixed(1)} years`
44}
46function ConvergePanels({ d }: { d: ConvergeOut }) {
47 const last = d.Ns.length - 1
48 const tip = (i: number) => [
49 `N = ${d.Ns[i]}: ${d.cells[i].toLocaleString()} nodes`,
50 `${fmtNum(d.ppw[i], 3)} points per wavelength at ${d.fhi} Hz`,
51 `error ${fmtErr(d.err[i])}, run ${fmtMs(d.ms[i])}`,
52 ]
53 return (
54 <>
55 <SeriesPlot
56 title="pressure at the receiver: the exact image sum, and the grid at its coarsest and its finest"
57 height={220}
58 xLabel="ms"
59 series={[
60 { x: d.t, y: d.ye, color: ink.reference, width: 2.6, label: 'exact' },
61 { x: d.t1, y: d.y1, color: faded(method.fd2, 0.35), width: 1.1, label: `N = ${d.Ns[0]}` },
62 { x: d.t, y: d.y, color: method.fd2, width: 1.3, label: `N = ${d.Ns[last]}` },
63 ]}
64 yTicks={[]}
65 />
66 <Row>
67 <SeriesPlot
68 title="relative rms error at the receiver over the first 30 ms"
69 height={230}
70 xLog
71 yLog
72 xTicks={d.Ns}
73 yDomain={[0.03, 3]}
74 xLabel="nodes along the 3.2 m side"
75 yLabel="error"
76 refs={[{ p: -2, through: [36, 0.7], label: 'slope −2', span: 0.4, labelAt: 'end' }]}
77 series={[{ x: d.Ns, y: d.err, color: method.fd2, shape: 'circle', label: 'leapfrog', tip }]}
78 legend={false}
79 />
80 <SeriesPlot
81 title="time per node per step"
82 height={230}
83 xLog
84 xTicks={d.Ns}
85 yDomain={[0, Math.max(...d.ns) * 1.15]}
86 xLabel="nodes along the 3.2 m side"
87 yLabel="ns"
88 series={[{ x: d.Ns, y: d.ns, color: method.fd2, shape: 'circle', tip }]}
89 legend={false}
90 />
91 </Row>
92 </>
93 )
94}
96export const ch09: Chapter = {
97 id: 'fdtd',
98 nav: 'A room, by grid',
99 part: 'Rooms',
100 title: 'A room, by finite differences',
101 blurb:
102 'The leapfrog scheme in three dimensions, checked against the exact image sum for rigid walls, and the arithmetic that confines it to low frequencies.',
103 text,
104 labs: [
105 {
106 id: 'converge',
107 title: 'A pulse in a rigid 3.2 × 2.4 × 2 m room',
108 caption:
109 'A pulse with most of its energy below about 1.1 kHz, heard 1.8 m away for 30 ms, which is long enough for several dozen reflections. This lab waits for the run button.',
110 driver,
111 script,
112 controls: [],
113 defaults: {},
114 derive: () => ({ mode: 'converge', ...BOX, T: 0.03, Ns: [16, 24, 32, 48, 64] }),
115 manual: true,
116 editorOpen: true,
117 render: (d: ConvergeOut) => <ConvergePanels d={d} />,
118 summary: (d: ConvergeOut) => [
119 { label: `error at N = ${d.Ns[d.Ns.length - 1]}`, value: fmtErr(d.err[d.err.length - 1]) },
120 { label: 'observed order', value: fmtNum(d.orders[d.orders.length - 1], 3) },
121 { label: 'ns per node per step', value: fmtNum(d.rate, 3) },
122 { label: 'Chapter 8’s room, 1 s, to 20 kHz', value: duration(d.fullband) },
123 { label: 'the same, to 2 kHz', value: duration(d.lowband) },
124 ],
125 },
126 {
127 id: 'hear',
128 title: 'Three tenths of a second in the room',
129 caption:
130 'The same pulse and the same rigid room, for long enough to hear it ring, exactly and on two grids. This lab waits for the run button.',
131 driver,
132 script,
133 controls: [],
134 defaults: {},
135 derive: () => ({ mode: 'hear', ...BOX, T: 0.3, fs: 22050, Ns: [16, 32] }),
136 manual: true,
137 render: (d: HearOut) => (
138 <Listen
139 items={[
140 { label: 'exact', clip: d.clipExact },
141 ...d.clips.map((c, k) => ({ label: `N = ${d.Ns[k]}`, clip: c })),
142 ]}
143 note={`the same scale; the grids run at ${d.fsk.map((f) => `${(f / 1000).toFixed(1)}`).join(' and ')} kHz`}
144 />
145 ),
146 },
147 ],
148}