1import type { Chapter } from '../../ui/types.ts'
2import type { Num, Params } from '../../engine/types.ts'
3import driver from './driver.m?raw'
4import script from './openbox.m?raw'
5import text from './text.md?raw'
6import Listen, { type Clip } from '../../audio/Listen.tsx'
7import Field from '../../plot/Field.tsx'
8import { Row, SeriesPlot, fmtDb, fmtMs } from '../common/panels.tsx'
9import { method, type MethodKey } from '../../plot/palette.ts'
11interface SnapOut {
12 ms: number
13 refl: number
14 tsnap: number
15 ref: Num[][]
16 num: Num[][]
17 err: Num[][]
18 emax: number
19 umax: number
20 urefmax: number
21 t: number[]
22 eng: number[]
23 L: number
24}
26interface SweepOut {
27 methods: string[]
28 Ws: number[]
29 refl: Num[][]
30 ms: Num[][]
31}
33interface HearOut {
34 methods: string[]
35 fs: number
36 tt: number[]
37 traces: number[][]
38 clips: Clip[]
39}
41const EDGE: Record<string, { label: string; key: MethodKey; shape: 'circle' | 'square' | 'diamond' | 'cross' }> = {
42 rigid: { label: 'rigid wall', key: 'fd2', shape: 'circle' },
43 mur: { label: 'Mur’s condition', key: 'waveguide', shape: 'diamond' },
44 sponge: { label: 'damping layer', key: 'fd4', shape: 'square' },
45 pml: { label: 'perfectly matched layer', key: 'boundary', shape: 'cross' },
46}
48const METHOD_CONTROL = {
49 kind: 'select' as const,
50 key: 'method',
51 label: 'edge',
52 options: Object.entries(EDGE).map(([k, v]) => ({ value: k, label: v.label })),
53}
55const REGION = { N: 100, L: 2, lam: 0.7, sigma: 0.06, xs: 0.7, ys: 1.1, T: 0.0085 }
57function SnapPanels({ d }: { d: SnapOut }) {
58 const dom: [number, number] = [0, d.L]
59 return (
60 <>
61 <Row>
62 <Field z={d.ref} xDomain={dom} yDomain={dom} kind="div" zmax={d.umax} height={250} title={`reference, t = ${d.tsnap.toFixed(1)} ms`} />
63 <Field z={d.num} xDomain={dom} yDomain={dom} kind="div" zmax={d.umax} height={250} title="with this edge, same colour scale" />
64 <Field z={d.err} xDomain={dom} yDomain={dom} kind="div" height={250} title="the difference, on its own scale" />
65 </Row>
66 <SeriesPlot
67 title="energy in the region that should not be there, relative to the most the region ever held"
68 height={190}
69 xLabel="ms"
70 yLabel="dB"
71 yDomain={[-160, 10]}
72 series={[{ x: d.t, y: d.eng.map((v) => Math.max(v, -160)), color: method.boundary, width: 1.6 }]}
73 legend={false}
74 />
75 </>
76 )
77}
79function SweepPanels({ d }: { d: SweepOut }) {
80 const tip = (k: number) => (j: number) => [`layer ${d.Ws[j]} nodes`, `reflected ${fmtDb(d.refl[k][j])}`, `run ${fmtMs(d.ms[k][j])}`]
81 const layered = d.methods.map((m, k) => ({ m, k })).filter(({ m }) => m === 'sponge' || m === 'pml')
82 const flat = d.methods.map((m, k) => ({ m, k })).filter(({ m }) => m === 'rigid' || m === 'mur')
83 return (
84 <Row>
85 <SeriesPlot
86 title="reflected energy against the width of the layer; the two edges without a layer are drawn as lines"
87 height={250}
88 xLog
89 xTicks={d.Ws}
90 xLabel="layer width, nodes"
91 yLabel="dB"
92 yDomain={[-120, 10]}
93 hlines={flat.map(({ m, k }) => ({ y: d.refl[k][0] as number, color: method[EDGE[m].key], label: EDGE[m].label }))}
94 series={layered.map(({ m, k }) => ({
95 x: d.Ws,
96 y: d.refl[k],
97 color: method[EDGE[m].key],
98 shape: EDGE[m].shape,
99 label: EDGE[m].label,
100 tip: tip(k),
101 }))}
102 />
103 <SeriesPlot
104 title="the same against the time each run took"
105 height={250}
106 xLog
107 xLabel="ms"
108 yLabel="dB"
109 yDomain={[-120, 10]}
110 series={d.methods.map((m, k) => ({
111 x: d.ms[k],
112 y: d.refl[k],
113 color: method[EDGE[m].key],
114 shape: EDGE[m].shape,
115 label: EDGE[m].label,
116 tip: tip(k),
117 }))}
118 />
119 </Row>
120 )
121}
123function HearPanels({ d }: { d: HearOut }) {
124 return (
125 <>
126 <SeriesPlot
127 title="the first 80 ms at the microphone"
128 height={230}
129 xLabel="ms"
130 series={d.methods.map((m, k) => ({
131 x: d.tt,
132 y: d.traces[k].map((v) => v + 1.2 * (d.methods.length - 1 - k)),
133 color: method[EDGE[m].key],
134 label: EDGE[m].label,
135 width: 1.2,
136 }))}
137 yTicks={[]}
138 />
139 <Listen
140 items={d.methods.map((m, k) => ({ label: EDGE[m].label, clip: d.clips[k] }))}
141 note={`the same scale, at the simulation's own rate of ${(d.fs / 1000).toFixed(1)} kHz`}
142 />
143 </>
144 )
145}
147export const ch07: Chapter = {
148 id: 'open',
149 nav: 'Letting sound out',
150 part: 'Rooms',
151 title: 'Letting sound out',
152 blurb:
153 'A grid has to end somewhere. Four ways of ending it, measured against a reference that is exact by causality, and heard.',
154 text,
155 labs: [
156 {
157 id: 'snap',
158 title: 'One pulse, four edges',
159 caption:
160 'A pulse released 0.7 m from the left of a 2 m square. The snapshot is taken once it has reached every edge; whatever is in the difference panel came back from one.',
161 driver,
162 script,
163 controls: [
164 METHOD_CONTROL,
165 { kind: 'slider', key: 'W', label: 'layer width, nodes', min: 0, max: 0, values: [5, 10, 20, 40] },
166 ],
167 defaults: { method: 'mur', W: 10 },
168 derive: (p: Params) => ({ mode: 'snap', ...REGION, tsnap: 0.0042, method: p.method, W: p.W }),
169 editorOpen: true,
170 render: (d: SnapOut) => <SnapPanels d={d} />,
171 summary: (d: SnapOut) => [
172 { label: 'reflected energy', value: fmtDb(d.refl) },
173 { label: 'largest reflection / largest wave', value: (d.emax / d.urefmax).toExponential(1) },
174 ],
175 },
176 {
177 id: 'sweep',
178 title: 'Reflection against cost',
179 caption: 'Every edge, and the two layers at four widths. This lab takes several seconds, so it waits for the run button.',
180 driver,
181 script,
182 controls: [],
183 defaults: {},
184 derive: () => ({ mode: 'sweep', ...REGION, methods: ['rigid', 'mur', 'sponge', 'pml'], Ws: [5, 10, 20, 40] }),
185 manual: true,
186 render: (d: SweepOut) => <SweepPanels d={d} />,
187 summary: (d: SweepOut) => {
188 const k = d.methods.indexOf('pml')
189 return [
190 { label: 'Mur', value: fmtDb(d.refl[d.methods.indexOf('mur')][0]) },
191 { label: 'PML, 5 nodes', value: fmtDb(d.refl[k][0]) },
192 { label: 'PML, 40 nodes', value: fmtDb(d.refl[k][d.Ws.length - 1]) },
193 ]
194 },
195 },
196 {
197 id: 'hear',
198 title: 'A box, and a box that is not there',
199 caption:
200 'A 3 m square, a thump at one point, a microphone at another, for 0.3 s. This lab waits for the run button.',
201 driver,
202 script,
203 controls: [],
204 defaults: {},
205 derive: () => ({
206 mode: 'hear',
207 N: 60,
208 L: 3,
209 lam: 0.7,
210 sigma: 0.1,
211 xs: 1.0,
212 ys: 1.3,
213 xr: 2.1,
214 yr: 1.7,
215 T: 0.3,
216 W: 10,
217 methods: ['rigid', 'mur', 'pml'],
218 }),
219 manual: true,
220 render: (d: HearOut) => <HearPanels d={d} />,
221 },
222 ],
223}