/ tutorials / wave-acoustics
Sign in
tutorials / wave-acoustics
wave-acoustics / src / figures / drums.tsx
281 lines · 10.7 KBBlameHistoryRaw
1// Figures for Part II: the square's modes, the anisotropy of the grid, the
2// three stencils of Chapter 5, and the two rims of Chapter 6.
3import type { ReactElement } from 'react'
4import { Figure, Label, Node } from './svg.tsx'
5import { faded, familyColor, ink, method } from '../plot/palette.ts'
7const pts = (xy: [number, number][]) => xy.map(([x, y], i) => `${i ? 'L' : 'M'}${x.toFixed(1)} ${y.toFixed(1)}`).join('')
9/** Six modes of the square membrane, with their frequencies relative to the lowest. */
10export function Modes() {
11 const modes: [number, number][] = [
12 [1, 1],
13 [2, 1],
14 [2, 2],
15 [3, 1],
16 [3, 2],
17 [3, 3],
18 ]
19 const s = 92
20 const gap = 22
21 const x0 = (716 - (6 * s + 5 * gap)) / 2
22 const y0 = 18
23 const n = 18
24 return (
25 <Figure
26 width={716}
27 height={176}
28 caption="Six modes of a square membrane, shaded by the sign of the displacement, with their frequencies as multiples of the lowest. Modes (m, n) and (n, m) have the same frequency, so the square's spectrum is full of coincident pairs, and none of the ratios but (2, 2) is a whole number."
29 >
30 {modes.map(([m, k], q) => {
31 const ox = x0 + q * (s + gap)
32 const cells: ReactElement[] = []
33 for (let i = 0; i < n; i++) {
34 for (let j = 0; j < n; j++) {
35 const v = Math.sin((m * Math.PI * (j + 0.5)) / n) * Math.sin((k * Math.PI * (i + 0.5)) / n)
36 const c = v > 0 ? method.boundary : method.fd2
37 cells.push(
38 <rect
39 key={`${i}-${j}`}
40 x={ox + (j * s) / n}
41 y={y0 + (i * s) / n}
42 width={s / n + 0.4}
43 height={s / n + 0.4}
44 fill={faded(c, 1 - Math.abs(v) * 0.8)}
45 />,
46 )
47 }
48 }
49 return (
50 <g key={q}>
51 {cells}
52 <rect x={ox} y={y0} width={s} height={s} fill="none" stroke={ink.secondary} strokeWidth={1.6} />
53 <Label x={ox + s / 2} y={y0 + s + 18} size={12} fill={ink.primary}>
54 {`(${m}, ${k})`}
55 </Label>
56 <Label x={ox + s / 2} y={y0 + s + 35} size={11.5} fill={ink.muted}>
57 {`× ${(Math.sqrt(m * m + k * k) / Math.SQRT2).toFixed(3)}`}
58 </Label>
59 </g>
60 )
61 })}
62 </Figure>
63 )
66/** Where a circular wavefront ends up on the grid, for a short wavelength. */
67export function Anisotropy() {
68 const cx = 250
69 const cy = 200
70 const R = 170
71 const P = 3
72 const kh = (2 * Math.PI) / P
73 const speed = (th: number, lam: number) => {
74 const s2 = Math.sin((kh * Math.cos(th)) / 2) ** 2 + Math.sin((kh * Math.sin(th)) / 2) ** 2
75 return (2 * Math.asin(lam * Math.sqrt(s2))) / (lam * kh)
76 }
77 const lams = [0.3, 0.5, Math.SQRT1_2]
78 const front = (lam: number) => {
79 const out: [number, number][] = []
80 for (let i = 0; i <= 90; i++) {
81 const th = (Math.PI / 2) * (i / 90)
82 const r = R * speed(th, lam)
83 out.push([cx + r * Math.cos(th), cy - r * Math.sin(th)])
84 }
85 return pts(out)
86 }
87 return (
88 <Figure
89 width={716}
90 height={228}
91 caption="A quarter of a circular wavefront at three points per wavelength: where the exact wave has reached (grey), and where the grid's waves of the same length have reached in the same time, for three Courant numbers. Waves along the grid's axes are slowest; along the diagonal, at λ = 1/√2, the grid is exact. At twenty points per wavelength the same picture would be indistinguishable from a circle."
92 >
93 <line x1={cx} y1={cy} x2={cx + R + 20} y2={cy} stroke={ink.axis} />
94 <line x1={cx} y1={cy} x2={cx} y2={cy - R - 14} stroke={ink.axis} />
95 <line x1={cx} y1={cy} x2={cx + (R + 10) * Math.SQRT1_2} y2={cy - (R + 10) * Math.SQRT1_2} stroke={ink.axis} strokeDasharray="3 3" />
96 <path d={`M${cx + R} ${cy}A${R} ${R} 0 0 0 ${cx} ${cy - R}`} fill="none" stroke={ink.reference} strokeWidth={2.4} />
97 {lams.map((l, k) => (
98 <path key={l} d={front(l)} fill="none" stroke={familyColor(k, lams.length)} strokeWidth={2} />
99 ))}
100 <Label x={cx + R + 24} y={cy} size={11} fill={ink.muted} anchor="start">
101 along an axis
102 </Label>
103 <Label x={cx + (R + 16) * Math.SQRT1_2} y={cy - (R + 16) * Math.SQRT1_2} size={11} fill={ink.muted} anchor="start">
104 along the diagonal
105 </Label>
106 <Label x={cx - 6} y={cy + 14} size={11} fill={ink.muted} anchor="end">
107 start
108 </Label>
109 {lams.map((l, k) => (
110 <g key={l}>
111 <line x1={500} x2={526} y1={70 + 22 * k} y2={70 + 22 * k} stroke={familyColor(k, lams.length)} strokeWidth={2} />
112 <Label x={534} y={70 + 22 * k} size={11.5} fill={ink.secondary} anchor="start">
113 {k === 2 ? 'λ = 1/√2' : `λ = ${l}`}
114 </Label>
115 </g>
116 ))}
117 <line x1={500} x2={526} y1={70 + 22 * 3} y2={70 + 22 * 3} stroke={ink.reference} strokeWidth={2.4} />
118 <Label x={534} y={70 + 22 * 3} size={11.5} fill={ink.secondary} anchor="start">
119 exact
120 </Label>
121 </Figure>
122 )
125/** The nodes each of Chapter 5's schemes reads to update one node. */
126export function Stencils() {
127 const g = 22
128 const panel = (ox: number, title: string, sub: string, set: [number, number, string][]) => {
129 const cy = 100
130 const grid: ReactElement[] = []
131 for (let i = -2; i <= 2; i++) {
132 for (let j = -2; j <= 2; j++) {
133 grid.push(<Node key={`g${i}${j}`} x={ox + j * g} y={cy + i * g} r={2.2} fill={ink.axis} />)
134 }
135 }
136 return (
137 <g>
138 {grid}
139 {set.map(([i, j, c]) => (
140 <Node key={`${i}${j}`} x={ox + j * g} y={cy + i * g} r={i === 0 && j === 0 ? 7 : 5.5} fill={c} />
141 ))}
142 <Label x={ox} y={172} size={12} fill={ink.primary}>
143 {title}
144 </Label>
145 <Label x={ox} y={190} size={11} fill={ink.muted}>
146 {sub}
147 </Label>
148 </g>
149 )
150 }
151 const five: [number, number, string][] = [
152 [0, 0, method.fd2],
153 [0, 1, method.fd2],
154 [0, -1, method.fd2],
155 [1, 0, method.fd2],
156 [-1, 0, method.fd2],
157 ]
158 const cross = (c: string): [number, number, string][] => [
159 [0, 0, c],
160 [0, 1, c],
161 [0, -1, c],
162 [1, 0, c],
163 [-1, 0, c],
164 [0, 2, c],
165 [0, -2, c],
166 [2, 0, c],
167 [-2, 0, c],
168 ]
169 const both: [number, number, string][] = [
170 ...cross(method.fd4),
171 [1, 1, faded(method.fd4, 0.35)],
172 [1, -1, faded(method.fd4, 0.35)],
173 [-1, 1, faded(method.fd4, 0.35)],
174 [-1, -1, faded(method.fd4, 0.35)],
175 ]
176 return (
177 <Figure
178 width={716}
179 height={206}
180 caption="The nodes each scheme reads at the current step to update the centre node. The fourth-order Laplacian widens the five-point cross to nine nodes; the fully fourth-order scheme also applies the five-point Laplacian twice, which reaches the four diagonal neighbours (lighter) as well."
181 >
182 {panel(130, 'second order', 'five-point cross', five)}
183 {panel(358, 'fourth order in space', 'nine-point cross', cross(method.waveguide))}
184 {panel(586, 'fourth order in both', 'thirteen nodes', both)}
185 </Figure>
186 )
189/** A curved rim on a square grid: staircase against Shortley-Weller. */
190export function Rim() {
191 const g = 34
192 const panel = (ox: number, sw: boolean) => {
193 // a circle of radius R centred below-left of the panel, so that its arc
194 // runs diagonally across it
195 const R = 7.3
196 const cxg = -2.2
197 const cyg = -2.6
198 const inside = (i: number, j: number) => (j - cxg) ** 2 + (i - cyg) ** 2 < R * R
199 const X = (j: number) => ox + j * g
200 const Y = (i: number) => 206 - i * g
201 const arc: [number, number][] = []
202 for (let k = 0; k <= 60; k++) {
203 const t = (Math.PI / 2) * (k / 60)
204 const j = cxg + R * Math.cos(t)
205 const i = cyg + R * Math.sin(t)
206 if (j >= -0.4 && j <= 6.4 && i >= -0.4 && i <= 5.4) arc.push([X(j), Y(i)])
207 }
208 const els: ReactElement[] = []
209 for (let i = 0; i <= 5; i++) {
210 for (let j = 0; j <= 6; j++) {
211 const inn = inside(i, j)
212 els.push(
213 <circle
214 key={`n${i}${j}`}
215 cx={X(j)}
216 cy={Y(i)}
217 r={4}
218 fill={inn ? ink.node : 'none'}
219 stroke={inn ? 'none' : ink.muted}
220 strokeWidth={1.2}
221 />,
222 )
223 if (inn && sw) {
224 // where the rim crosses the grid line to an outside neighbour
225 if (!inside(i, j + 1) && j + 1 <= 6.4) {
226 const jc = cxg + Math.sqrt(R * R - (i - cyg) ** 2)
227 els.push(<line key={`e${i}${j}`} x1={X(j)} y1={Y(i)} x2={X(jc)} y2={Y(i)} stroke={method.boundary} strokeWidth={2} />)
228 els.push(<circle key={`c${i}${j}`} cx={X(jc)} cy={Y(i)} r={3} fill={method.boundary} />)
229 }
230 if (!inside(i + 1, j) && i + 1 <= 5.4) {
231 const ic = cyg + Math.sqrt(R * R - (j - cxg) ** 2)
232 els.push(<line key={`u${i}${j}`} x1={X(j)} y1={Y(i)} x2={X(j)} y2={Y(ic)} stroke={method.boundary} strokeWidth={2} />)
233 els.push(<circle key={`d${i}${j}`} cx={X(j)} cy={Y(ic)} r={3} fill={method.boundary} />)
234 }
235 }
236 }
237 }
238 // the staircase: the stencil reaches outside neighbours, which are held
239 // at zero as if the rim ran through them
240 const stair: ReactElement[] = []
241 if (!sw) {
242 for (let i = 0; i <= 5; i++) {
243 for (let j = 0; j <= 6; j++) {
244 if (!inside(i, j)) continue
245 const nb: [number, number][] = [
246 [i, j + 1],
247 [i + 1, j],
248 ]
249 for (const [a, b] of nb) {
250 if (a > 5 || b > 6 || inside(a, b)) continue
251 stair.push(<line key={`s${i}${j}${a}${b}`} x1={X(j)} y1={Y(i)} x2={X(b)} y2={Y(a)} stroke={method.fd2} strokeWidth={2} />)
252 stair.push(<circle key={`z${a}${b}${i}${j}`} cx={X(b)} cy={Y(a)} r={4} fill={method.fd2} />)
253 }
254 }
255 }
256 }
257 return (
258 <g>
259 <path d={pts(arc)} fill="none" stroke={ink.reference} strokeWidth={2} />
260 {stair}
261 {els}
262 </g>
263 )
264 }
265 return (
266 <Figure
267 width={716}
268 height={262}
269 caption="The rim of a drum crossing a square grid. Filled nodes are inside and are unknowns; open ones are outside and held at zero. Left, the staircase: the stencil reaches outside neighbours (blue), which are held at zero as if the rim passed through them, so the drum the grid sees extends up to one spacing beyond the true rim (grey), by an amount that varies irregularly along it. Right, Shortley–Weller: the true crossing points (pink) are kept, at a fraction θ of a spacing from the node, and the stencil uses the unequal spacing."
270 >
271 {panel(70, false)}
272 {panel(430, true)}
273 <Label x={172} y={248} size={12} fill={ink.primary}>
274 staircase
275 </Label>
276 <Label x={532} y={248} size={12} fill={ink.primary}>
277 Shortley–Weller
278 </Label>
279 </Figure>
280 )
↑↓ move↵openescclose