/ simulated_instruments / cymbal2
Sign in
simulated_instruments / cymbal2
cymbal2 / webapp / src / tutorial / figures / WavenumberFig.tsx
164 lines · 5.6 KBCodeBlameHistory
17959acInteractive tutorial on the theory and the solverJeremy Magland 1// One azimuthal wavenumber at a time. Left: the shape cos(m theta) on the
2// disk, the pattern every quantity is decomposed into. Right: the eigenvalue
3// of the discrete azimuthal second difference against the continuum value
4// m^2, showing where the grid stops being faithful.
6import { useState } from 'react';
7import { useCanvas, drawFrame, PLOT_COLORS } from '../../components/plots/canvasUtils';
8import { Figure, FigControls, FigSlider } from '../figparts';
10const HEIGHT = 270;
11const NTH = 60;
13export default function WavenumberFig() {
14 const [m, setM] = useState(4);
16 const ref = useCanvas(
17 (ctx, w, h) => {
18 drawFrame(ctx, w, h);
19 const half = Math.min(w * 0.42, h);
20 const cx = half / 2 + 14;
21 const cy = h / 2;
22 const R = Math.min(half, h) / 2 - 18;
23 const Rin = R * 0.14;
25 // disk shaded by cos(m theta) with a radial envelope, via offscreen bitmap
26 const px = 160;
27 const off = document.createElement('canvas');
28 off.width = px;
29 off.height = px;
30 const octx = off.getContext('2d')!;
31 const img = octx.createImageData(px, px);
32 for (let yy = 0; yy < px; yy++) {
33 for (let xx = 0; xx < px; xx++) {
34 const dx = (xx - px / 2) / (px / 2);
35 const dy = (yy - px / 2) / (px / 2);
36 const r = Math.hypot(dx, dy);
37 const o = 4 * (yy * px + xx);
38 if (r > 1 || r < Rin / R) {
39 img.data[o + 3] = 0;
40 continue;
41 }
42 const th = Math.atan2(dy, dx);
43 const env = (r - Rin / R) / (1 - Rin / R);
44 const v = Math.cos(m * th) * env; // -1..1
45 // cyan for +, amber for -
46 const t = Math.abs(v);
47 const rr = v >= 0 ? 77 : 232;
48 const gg = v >= 0 ? 208 : 163;
49 const bb = v >= 0 ? 225 : 61;
50 img.data[o] = Math.round(24 + (rr - 24) * t);
51 img.data[o + 1] = Math.round(27 + (gg - 27) * t);
52 img.data[o + 2] = Math.round(32 + (bb - 32) * t);
53 img.data[o + 3] = 255;
54 }
55 }
56 octx.putImageData(img, 0, 0);
57 ctx.drawImage(off, cx - R, cy - R, 2 * R, 2 * R);
58 ctx.strokeStyle = PLOT_COLORS.axis;
59 ctx.beginPath();
60 ctx.arc(cx, cy, R, 0, 2 * Math.PI);
61 ctx.stroke();
62 ctx.fillStyle = PLOT_COLORS.axis;
63 ctx.font = '11px system-ui';
64 ctx.textAlign = 'center';
65 ctx.fillText(`cos(${m}θ): ${m} nodal diameter${m === 1 ? '' : 's'}`, cx, h - 8);
67 // right panel: -lam2 vs m^2
68 const padL = half + 60;
69 const padR = 16;
70 const padT = 16;
71 const padB = 34;
72 const pw = w - padL - padR;
73 const ph = h - padT - padB;
74 const mMax = NTH / 2;
75 const hth = (2 * Math.PI) / NTH;
76 const yMax = mMax * mMax;
77 const X = (mm: number) => padL + (mm / mMax) * pw;
78 const Y = (v: number) => padT + ph - (v / yMax) * ph;
80 ctx.strokeStyle = PLOT_COLORS.grid;
81 ctx.strokeRect(padL, padT, pw, ph);
83 // continuum m^2, dashed
84 ctx.strokeStyle = 'rgba(139, 147, 161, 0.8)';
85 ctx.setLineDash([4, 4]);
86 ctx.beginPath();
87 for (let mm = 0; mm <= mMax; mm += 0.5) {
88 const x = X(mm);
89 const y = Y(mm * mm);
90 if (mm === 0) ctx.moveTo(x, y);
91 else ctx.lineTo(x, y);
92 }
93 ctx.stroke();
94 ctx.setLineDash([]);
96 // discrete eigenvalue
97 ctx.strokeStyle = PLOT_COLORS.trace;
98 ctx.lineWidth = 2;
99 ctx.beginPath();
100 for (let mm = 0; mm <= mMax; mm += 0.5) {
101 const lam = (2 - 2 * Math.cos(mm * hth)) / (hth * hth);
102 const x = X(mm);
103 const y = Y(lam);
104 if (mm === 0) ctx.moveTo(x, y);
105 else ctx.lineTo(x, y);
106 }
107 ctx.stroke();
108 ctx.lineWidth = 1;
110 // current m marker
111 const lamM = (2 - 2 * Math.cos(m * hth)) / (hth * hth);
112 ctx.fillStyle = PLOT_COLORS.accent;
113 ctx.beginPath();
114 ctx.arc(X(m), Y(lamM), 4, 0, 2 * Math.PI);
115 ctx.fill();
117 ctx.fillStyle = PLOT_COLORS.axis;
118 ctx.textAlign = 'center';
119 ctx.fillText('wavenumber m', padL + pw / 2, h - 8);
120 for (const mm of [0, 10, 20, 30]) {
121 ctx.fillText(`${mm}`, X(mm), padT + ph + 13);
122 }
123 ctx.textAlign = 'left';
124 ctx.fillStyle = PLOT_COLORS.trace;
125 ctx.fillText('discrete −λ₂(m)', padL + 8, padT + 14);
126 ctx.fillStyle = PLOT_COLORS.axis;
127 ctx.fillText('continuum m²', padL + 8, padT + 28);
128 ctx.fillText(`grid cutoff m = ${mMax}`, X(mMax) - 100, padT + ph - 8);
129 },
130 [m],
131 HEIGHT,
132 );
134 return (
135 <Figure
136 caption={
137 <>
138 Left: everything on the grid is decomposed azimuthally into patterns cos(m&theta;)
139 and sin(m&theta;); m counts nodal diameters. Right: on the discrete ring the second
140 derivative in &theta; acts on such a pattern as multiplication by a single number,
141 &minus;&lambda;<sub>2</sub>(m) (cyan), which follows the continuum m&sup2; (dashed)
142 at low order and falls away toward the grid cutoff at m = N<sub>&theta;</sub>/2 = 30.
143 The two consequences: every linear solve splits into independent small radial
144 problems, one per wavenumber; and the highest orders the grid carries are
145 progressively detuned, which is one reason the model's top octaves are duller than
146 a real crash.
147 </>
148 }
149 >
150 <canvas ref={ref} />
151 <FigControls>
152 <FigSlider
153 label="wavenumber m"
154 value={m}
155 min={0}
156 max={30}
157 step={1}
158 format={(v) => `${Math.round(v)}`}
159 onChange={(v) => setM(Math.round(v))}
160 />
161 </FigControls>
162 </Figure>
163 );
↑↓ move↵openescclose