// Figures for the benchmark chapter: the usual convergence and // work-precision panels for the in-browser sweep, plus a slot for results // recorded outside the browser and loaded from a JSON file. The point of the // upload is that the reader runs the offline scripts on the same machine as // the page, so the two sets of timings share a clock and may honestly share // an axis. import { useRef, useState } from 'react' import { Convergence, WorkPrecision, type SweepOut, type SweepStyle } from '../onedim/panels.tsx' import { method as methodColor, type MethodKey } from '../../plot/palette.ts' import type { Num } from '../../engine/types.ts' export interface BenchOut extends SweepOut { DOF: Num[][] } interface OfflineMethod { name: string slot?: string ns: number[] E: Num[] T: Num[] } interface OfflineFile { label: string kind?: string methods: OfflineMethod[] } export const BENCH_SLOTS: MethodKey[] = ['fd2', 'mps', 'bie', 'spectral'] /** Offline series keep the colour of their method and take a dash per file. */ const FILE_DASHES = ['6 4', '2 3', '9 3 2 3'] function isSlot(s: string | undefined): s is MethodKey { return s != null && s in methodColor } /** Accepts the documented schema and rejects everything else with a reason. */ function parseOffline(name: string, text: string): OfflineFile | string { let raw: unknown try { raw = JSON.parse(text) } catch { return `${name}: not JSON` } const o = raw as Partial & { schema?: string } if (o.schema !== 'laplace-dirichlet-bench-1') return `${name}: missing schema "laplace-dirichlet-bench-1"` if (!Array.isArray(o.methods) || o.methods.length === 0) return `${name}: no methods` for (const m of o.methods) { if (typeof m.name !== 'string' || !Array.isArray(m.ns) || !Array.isArray(m.E) || !Array.isArray(m.T)) return `${name}: method entries need name, ns, E, T` } return { label: typeof o.label === 'string' ? o.label : name, kind: o.kind, methods: o.methods } } /** The browser sweep merged with whatever offline files are loaded. */ function merged(data: BenchOut, files: OfflineFile[]): { out: SweepOut; style: SweepStyle } { const names = [...data.names] const NS: Num[][] = data.NS ? data.NS.map((r) => [...r]) : data.names.map(() => [...data.ns]) const E = data.E.map((r) => [...r]) const T = data.T.map((r) => [...r]) const orders = data.orders.map((r) => [...r]) const slots: MethodKey[] = [...BENCH_SLOTS] const dashes: (string | undefined)[] = data.names.map(() => undefined) files.forEach((f, i) => { for (const m of f.methods) { names.push(`${m.name} · ${f.label}`) NS.push(m.ns) E.push(m.E) T.push(m.T) orders.push(m.ns.map(() => null)) slots.push(isSlot(m.slot) ? m.slot : 'fd2') dashes.push(FILE_DASHES[i % FILE_DASHES.length]) } }) return { out: { names, ns: data.ns, NS, E, T, orders }, style: { slots, dashes } } } export function BenchSweep({ data, kind }: { data: BenchOut; kind: string }) { const [files, setFiles] = useState([]) const [problem, setProblem] = useState(null) const input = useRef(null) const onFiles = async (list: FileList | null) => { if (!list) return const next: OfflineFile[] = [] let bad: string | null = null for (const f of Array.from(list)) { const parsed = parseOffline(f.name, await f.text()) if (typeof parsed === 'string') bad = parsed else next.push(parsed) } setProblem(bad) if (next.length > 0) setFiles((cur) => [...cur, ...next]) if (input.current) input.current.value = '' } const m = merged(data, files) return ( <>
void onFiles(e.target.files)} /> {files.map((f, i) => ( {f.label} {f.kind && f.kind !== kind ? ` (recorded on '${f.kind}')` : ''} ))} {problem && {problem}}
) }