/ tutorials / laplace-dirichlet
Sign in
tutorials / laplace-dirichlet
laplace-dirichlet / src / chapters / ch11 / panels.tsx
142 lines · 4.9 KBCodeBlameHistory
6292f12A benchmark chapter, and offline benchmarks to compare it againstJeremy Magland 1// Figures for the benchmark chapter: the usual convergence and
2// work-precision panels for the in-browser sweep, plus a slot for results
3// recorded outside the browser and loaded from a JSON file. The point of the
4// upload is that the reader runs the offline scripts on the same machine as
5// the page, so the two sets of timings share a clock and may honestly share
6// an axis.
7import { useRef, useState } from 'react'
8import { Convergence, WorkPrecision, type SweepOut, type SweepStyle } from '../onedim/panels.tsx'
9import { method as methodColor, type MethodKey } from '../../plot/palette.ts'
10import type { Num } from '../../engine/types.ts'
12export interface BenchOut extends SweepOut {
13 DOF: Num[][]
16interface OfflineMethod {
17 name: string
18 slot?: string
19 ns: number[]
20 E: Num[]
21 T: Num[]
24interface OfflineFile {
25 label: string
26 kind?: string
27 methods: OfflineMethod[]
30export const BENCH_SLOTS: MethodKey[] = ['fd2', 'mps', 'bie', 'spectral']
32/** Offline series keep the colour of their method and take a dash per file. */
33const FILE_DASHES = ['6 4', '2 3', '9 3 2 3']
35function isSlot(s: string | undefined): s is MethodKey {
36 return s != null && s in methodColor
39/** Accepts the documented schema and rejects everything else with a reason. */
40function parseOffline(name: string, text: string): OfflineFile | string {
41 let raw: unknown
42 try {
43 raw = JSON.parse(text)
44 } catch {
45 return `${name}: not JSON`
46 }
47 const o = raw as Partial<OfflineFile> & { schema?: string }
48 if (o.schema !== 'laplace-dirichlet-bench-1') return `${name}: missing schema "laplace-dirichlet-bench-1"`
49 if (!Array.isArray(o.methods) || o.methods.length === 0) return `${name}: no methods`
50 for (const m of o.methods) {
51 if (typeof m.name !== 'string' || !Array.isArray(m.ns) || !Array.isArray(m.E) || !Array.isArray(m.T))
52 return `${name}: method entries need name, ns, E, T`
53 }
54 return { label: typeof o.label === 'string' ? o.label : name, kind: o.kind, methods: o.methods }
57/** The browser sweep merged with whatever offline files are loaded. */
58function merged(data: BenchOut, files: OfflineFile[]): { out: SweepOut; style: SweepStyle } {
59 const names = [...data.names]
60 const NS: Num[][] = data.NS ? data.NS.map((r) => [...r]) : data.names.map(() => [...data.ns])
61 const E = data.E.map((r) => [...r])
62 const T = data.T.map((r) => [...r])
63 const orders = data.orders.map((r) => [...r])
64 const slots: MethodKey[] = [...BENCH_SLOTS]
65 const dashes: (string | undefined)[] = data.names.map(() => undefined)
66 files.forEach((f, i) => {
67 for (const m of f.methods) {
68 names.push(`${m.name} · ${f.label}`)
69 NS.push(m.ns)
70 E.push(m.E)
71 T.push(m.T)
72 orders.push(m.ns.map(() => null))
73 slots.push(isSlot(m.slot) ? m.slot : 'fd2')
74 dashes.push(FILE_DASHES[i % FILE_DASHES.length])
75 }
76 })
77 return { out: { names, ns: data.ns, NS, E, T, orders }, style: { slots, dashes } }
80export function BenchSweep({ data, kind }: { data: BenchOut; kind: string }) {
81 const [files, setFiles] = useState<OfflineFile[]>([])
82 const [problem, setProblem] = useState<string | null>(null)
83 const input = useRef<HTMLInputElement>(null)
85 const onFiles = async (list: FileList | null) => {
86 if (!list) return
87 const next: OfflineFile[] = []
88 let bad: string | null = null
89 for (const f of Array.from(list)) {
90 const parsed = parseOffline(f.name, await f.text())
91 if (typeof parsed === 'string') bad = parsed
92 else next.push(parsed)
93 }
94 setProblem(bad)
95 if (next.length > 0) setFiles((cur) => [...cur, ...next])
96 if (input.current) input.current.value = ''
97 }
99 const m = merged(data, files)
100 return (
101 <>
102 <Convergence
103 data={data}
104 style={{ slots: BENCH_SLOTS, refs: [{ p: -4 / 3, label: 'slope −4/3' }] }}
105 height={320}
106 xLabel="the method's own resolution parameter n"
107 />
108 <WorkPrecision
109 data={m.out}
110 style={m.style}
111 height={340}
112 labelCount={data.names.length}
113 resLabel="resolution"
114 />
115 <div className="lab-bar">
116 <button className="ghost" onClick={() => input.current?.click()}>
117 load offline results...
118 </button>
119 <input
120 ref={input}
121 type="file"
122 accept=".json,application/json"
123 multiple
124 style={{ display: 'none' }}
125 onChange={(e) => void onFiles(e.target.files)}
126 />
127 {files.map((f, i) => (
128 <span className="stat" key={`${f.label}-${i}`}>
129 <span className="stat-label">
130 {f.label}
131 {f.kind && f.kind !== kind ? ` (recorded on '${f.kind}')` : ''}
132 </span>
133 <button className="ghost" onClick={() => setFiles((cur) => cur.filter((_, j) => j !== i))}>
134 remove
135 </button>
136 </span>
137 ))}
138 {problem && <span className="warn">{problem}</span>}
139 </div>
140 </>
141 )
↑↓ move↵openescclose