// Panels for the square-membrane labs of Chapters 4 and 5. import type { Num, Params } from '../../engine/types.ts' import Listen, { type Clip } from '../../audio/Listen.tsx' import { Row, SeriesPlot, NumTable, fmtCents, fmtErr, fmtMs, fmtNum } from '../common/panels.tsx' import { faded, familyColor, familyShapes, ink, method, type MethodKey } from '../../plot/palette.ts' import type { Control } from '../../ui/types.ts' export interface StrikeOut { ms: number N: number lam: number fs: number rel: number nmodes: number f11: number fr: number[] dbe: number[] dbf: number[] fmodes: number[] tw: number[] yw: number[] yew: number[] clipExact: Clip clipFd: Clip clipDiff: Clip } export interface CentsOut { N: number lams: number[] m: number[] n: number[] ring: number[] rs: number[] angle: number[] ppw: number[] meas: Num[][] pred: Num[][] agree: number } export interface OrdersOut { Ns: number[] schemes: string[] lams: number[] modes: number[][] cents: Num[][] worst: Num[][] ms: Num[][] orders: Num[][] } /** * Which colour and shape each scheme wears, in every chapter. Fourth order * in space alone borrows the waveguide's amber, since no chapter shows both; * the spare red is too close to fd4's orange to sit beside it. */ export const SCHEME: Record = { fd2: { key: 'fd2', label: 'second order (leapfrog, five-point)', shape: 'circle' }, fd4: { key: 'waveguide', label: 'fourth order in space only', shape: 'square' }, fd44: { key: 'fd4', label: 'fourth order in space and time', shape: 'triangle' }, } /** The drum of Chapters 4 and 5: 150 Hz fundamental on a unit square. */ export const DRUM = { c: 150 * Math.SQRT2, kmax: 90, width: 0.03, xr: 0.77, yr: 0.63, fmax: 3000 } export const STRIKE_CONTROL: Control = { kind: 'select', key: 'at', label: 'strike at', options: [ { value: 'off', label: 'off centre, (0.31, 0.42)' }, { value: 'centre', label: 'the centre' }, { value: 'rim', label: 'near the rim, (0.12, 0.2)' }, ], } export function strikeParams(p: Params, scheme: string) { const at = String(p.at ?? 'off') const [xs, ys] = at === 'centre' ? [0.5, 0.5] : at === 'rim' ? [0.12, 0.2] : [0.31, 0.42] return { mode: 'strike', ...DRUM, N: p.N, lam: p.lam, T: 1.0, xs, ys, scheme } } export function StrikePanels({ d, scheme = 'fd2' }: { d: StrikeOut; scheme?: string }) { const col = method[SCHEME[scheme].key] return ( <> ) } export function strikeSummary(d: StrikeOut) { return [ { label: 'sample rate 1/dt', value: `${(d.fs / 1000).toFixed(2)} kHz` }, { label: 'modes in the exact sum', value: String(d.nmodes) }, { label: 'relative rms error', value: fmtErr(d.rel) }, ] } export function CentsPanels({ d }: { d: CentsOut }) { const nl = d.lams.length return ( {d.rs.map((R, r) => { const idx = d.ring.map((g, i) => (g === r + 1 ? i : -1)).filter((i) => i >= 0) const ppw = (2 * d.N) / R return ( [ { x: idx.map((i) => d.angle[i]), y: idx.map((i) => d.pred[k][i]), color: familyColor(k, nl), width: 1.1, noLegend: true, }, { x: idx.map((i) => d.angle[i]), y: idx.map((i) => d.meas[k][i]), color: familyColor(k, nl), shape: familyShapes[k], line: false, label: `λ = ${fmtNum(l, 3)}`, tip: (j: number) => { const i = idx[j] return [`mode (${d.m[i]}, ${d.n[i]}), ${d.angle[i].toFixed(1)}°`, `measured ${fmtCents(d.meas[k][i])}`] }, }, ])} /> ) })} ) } export function OrdersPanels({ d }: { d: OrdersOut }) { const sc = d.schemes.map((s) => SCHEME[s]) const nm = d.modes.length const tip = (s: number) => (j: number) => [ `N = ${d.Ns[j]}, run ${fmtMs(d.ms[s][j])}`, ...d.modes.map((m, q) => `mode (${m[0]},${m[1]}): ${fmtCents(d.cents[s * nm + q][j])}`), ] return ( <> ({ x: d.Ns, y: d.worst[k], color: method[s.key], shape: s.shape, label: s.label, tip: tip(k), }))} /> ({ x: d.ms[k], y: d.worst[k], color: method[s.key], shape: s.shape, label: s.label, tip: tip(k), }))} legend={false} /> `${s} error`), ...d.schemes.map((s) => `${s} order`)]} rows={d.Ns.map((N, j) => [ N, ...d.schemes.map((_, s) => fmtNum(d.worst[s][j], 3)), ...d.schemes.map((_, s) => fmtNum(d.orders[s][j], 3)), ])} /> ) }