import { createElement } from 'react' import type { Chapter, Control } from '../../ui/types.ts' import driver from './driver.m?raw' import script from './pacman.m?raw' import text from './text.md?raw' import { Fields2D, type Single2DOut } from '../twodim/panels.tsx' import { fmtErr } from '../onedim/panels.tsx' import { BenchSweep, type BenchOut } from './panels.tsx' import type { Num } from '../../engine/types.ts' interface FieldOut extends Single2DOut { cx: number[] cy: number[] } const KIND: Control = { kind: 'select', key: 'kind', label: 'ground truth', options: [ { value: 'both', label: 'singular + smooth (generic)' }, { value: 'sing', label: 'singular only: r^(2/3) sin(2θ/3)' }, { value: 'smooth', label: 'smooth only: log|z − z₀|' }, ], } /** Each method's resolution ladder behind the shared coarse-to-finest slider. */ const LEVELS: number[][] = [ [32, 64, 96, 128], // grid intervals per unit [8, 16, 32, 48], // basis degree [4, 6, 8, 10], // refinement depth [10, 20, 30, 40], // Chebyshev points per direction ] const NSM = [ [8, 16, 32, 64, 128], [4, 8, 16, 32, 48], [2, 4, 6, 8, 10], [6, 10, 16, 26, 40], ] function bestOf(d: BenchOut): { v: number; name: string } { let v = Infinity let name = '' d.E.forEach((row, k) => { for (const e of row) { if (e != null && isFinite(e) && e > 0 && e < v) { v = e name = d.names[k] } } }) return { v, name } } export const ch11: Chapter = { id: 'benchmarks', nav: 'One problem, every method', part: 'Beyond the square', title: 'Benchmarks: one problem, every method', blurb: 'A curved boundary with three corners, the four families of methods side by side, and timings you can extend beyond the browser.', text, labs: [ { id: 'field', title: 'The solution, and where each method hurts', caption: 'The error picture is the method’s signature: the difference scheme concentrates its error at the corner, the double layer at the boundary (that ring is the close-evaluation problem of Chapter 10), and the collocation methods spread theirs.', driver, script, manual: true, controls: [ KIND, { kind: 'select', key: 'mid', label: 'method', options: [ { value: 1, label: 'Shortley-Weller + SOR' }, { value: 2, label: 'harmonic basis' }, { value: 3, label: 'double layer, graded panels' }, { value: 4, label: 'Chebyshev, polar rectangle' }, ], }, { kind: 'slider', key: 'level', label: 'resolution', values: [1, 2, 3, 4], min: 1, max: 4, format: (v) => ['coarse', 'medium', 'fine', 'finest'][v - 1], }, ], defaults: { kind: 'both', mode: 'field', mid: 1, level: 2 }, derive: (p) => ({ ...p, n: LEVELS[Number(p.mid) - 1][Number(p.level) - 1] }), render: (d: FieldOut) => createElement(Fields2D, { data: d, xDomain: [-1.0625, 1.0625], yDomain: [-1.0625, 1.0625], curves: [{ x: d.cx, y: d.cy }], }), summary: (d: FieldOut) => [ { label: 'unknowns', value: String(d.unknowns) }, { label: 'max error', value: fmtErr(d.maxerr) }, ], }, { id: 'bench', title: 'The benchmark', caption: 'The first plot is only meaningful curve by curve, because each method refines its own parameter; the second, against wall clock, is the comparison. Results from the offline scripts land in the second plot as dashed curves.', driver, script, manual: true, editorOpen: true, controls: [KIND], defaults: { kind: 'both', mode: 'sweep', nsm: NSM }, render: (d: BenchOut, p) => createElement(BenchSweep, { data: d, kind: String(p.kind) }), summary: (d: BenchOut) => { const b = bestOf(d) const t = d.T.flat().reduce((s: number, v: Num) => s + (v ?? 0), 0) return [ { label: 'best error', value: `${fmtErr(b.v)} (${b.name})` }, { label: 'time in solves', value: t < 1000 ? `${t.toFixed(0)} ms` : `${(t / 1000).toFixed(1)} s` }, ] }, }, ], }