/ concept-collection / fastandaccurate
Sign in
concept-collection / fastandaccurate
fastandaccurate / src / app / App.tsx
390 lines · 13.8 KBBlameHistoryRaw
1import { useEffect, useMemo, useState } from "react";
2import {
3 INSTANCES,
4 getInstance,
5 PROBLEM_ID,
6} from "../problems/laplace2d/spec";
7import { SOLVERS, getSolver } from "../solvers";
8import {
9 buildResultFile,
10 type ResultFile,
11 type ResultPoint,
12} from "../harness/resultSchema";
13import {
14 environmentLabel,
15 fetchCommittedResults,
16 isResultFile,
17 RESULTS_REPO_URL,
18} from "./results";
19import { solverColorVar } from "./colors";
20import { sweepInBrowser } from "./workerClient";
21import { WorkPrecisionChart, type ChartCurve } from "./components/WorkPrecisionChart";
22import { PointsTable } from "./components/PointsTable";
23import { DomainView } from "./components/DomainView";
24import { SolutionSection } from "./components/SolutionSection";
26const REPO_URL = "https://github.com/concept-collection/fastandaccurate";
27const SPEC_URL = `${REPO_URL}/blob/main/docs/problems/laplace-dirichlet-2d.md`;
29interface LocalRun {
30 key: string;
31 solverId: string;
32 instanceId: string;
33 repeats: number;
34 points: ResultPoint[];
35 done: boolean;
38export function App() {
39 const [instanceId, setInstanceId] = useState(INSTANCES[1].id);
40 const [committed, setCommitted] = useState<ResultFile[] | null>(null);
41 const [committedError, setCommittedError] = useState<string | null>(null);
42 const [loaded, setLoaded] = useState<ResultFile[]>([]);
43 const [localRuns, setLocalRuns] = useState<LocalRun[]>([]);
44 const [hidden, setHidden] = useState<Set<string>>(new Set());
45 const [running, setRunning] = useState<string | null>(null);
46 const [runStatus, setRunStatus] = useState<string | null>(null);
47 const [repeats, setRepeats] = useState(3);
48 const [machineLabel, setMachineLabel] = useState("");
50 const inst = getInstance(instanceId);
52 useEffect(() => {
53 fetchCommittedResults()
54 .then(setCommitted)
55 .catch((err) =>
56 setCommittedError(err instanceof Error ? err.message : String(err))
57 );
58 }, []);
60 const allSolverIds = useMemo(() => {
61 const ids = new Set<string>(SOLVERS.map((s) => s.id));
62 committed?.forEach((r) => ids.add(r.solver.id));
63 loaded.forEach((r) => ids.add(r.solver.id));
64 return [...ids];
65 }, [committed, loaded]);
67 const curves: ChartCurve[] = useMemo(() => {
68 const out: ChartCurve[] = [];
69 const color = (id: string) => solverColorVar(id, allSolverIds);
70 committed
71 ?.filter((r) => r.problem === PROBLEM_ID && r.instance === instanceId)
72 .forEach((r, i) => {
73 out.push({
74 key: `committed:${i}`,
75 solverId: r.solver.id,
76 label: `${r.solver.id}${environmentLabel(r)}`,
77 color: color(r.solver.id),
78 points: r.points,
79 });
80 });
81 loaded
82 .filter((r) => r.problem === PROBLEM_ID && r.instance === instanceId)
83 .forEach((r, i) => {
84 out.push({
85 key: `loaded:${i}`,
86 solverId: r.solver.id,
87 label: `${r.solver.id}${environmentLabel(r)} (loaded)`,
88 color: color(r.solver.id),
89 dash: "8 4",
90 points: r.points,
91 });
92 });
93 localRuns
94 .filter((r) => r.instanceId === instanceId)
95 .forEach((r) => {
96 out.push({
97 key: r.key,
98 solverId: r.solverId,
99 label: `${r.solverId} — this browser`,
100 color: color(r.solverId),
101 dash: "4 4",
102 open: true,
103 points: r.points,
104 });
105 });
106 return out.filter((c) => !hidden.has(c.solverId));
107 }, [committed, loaded, localRuns, instanceId, hidden, allSolverIds]);
109 async function runSolver(solverId: string) {
110 const key = `local:${solverId}:${instanceId}:${Date.now()}`;
111 setLocalRuns((rs) => [
112 ...rs.filter((r) => !(r.solverId === solverId && r.instanceId === instanceId)),
113 { key, solverId, instanceId, repeats, points: [], done: false },
114 ]);
115 setRunning(solverId);
116 try {
117 const points = await sweepInBrowser(
118 instanceId,
119 solverId,
120 repeats,
121 (point, index, total) => {
122 setRunStatus(
123 `${solverId} on ${instanceId}: point ${index + 1}/${total} (n = ${point.n}) — rel max error ${point.relMax.toExponential(2)}`
124 );
125 setLocalRuns((rs) =>
126 rs.map((r) => (r.key === key ? { ...r, points: [...r.points, point] } : r))
127 );
128 }
129 );
130 setLocalRuns((rs) =>
131 rs.map((r) => (r.key === key ? { ...r, points, done: true } : r))
132 );
133 setRunStatus(null);
134 } catch (err) {
135 setRunStatus(
136 `${solverId} failed: ${err instanceof Error ? err.message : String(err)}`
137 );
138 } finally {
139 setRunning(null);
140 }
141 }
143 async function downloadRun(run: LocalRun) {
144 const manifest = getSolver(run.solverId);
145 const result = await buildResultFile({
146 instance: getInstance(run.instanceId),
147 solver: {
148 id: manifest.id,
149 version: manifest.version,
150 backend: manifest.backend,
151 source: "builtin",
152 },
153 environment: {
154 kind: "browser",
155 runtime: navigator.userAgent,
156 numblVersion: __NUMBL_VERSION__,
157 machineLabel: machineLabel || undefined,
158 browserReproducible: true,
159 },
160 repeats: run.repeats,
161 points: run.points,
162 });
163 const blob = new Blob([JSON.stringify(result, null, 2) + "\n"], {
164 type: "application/json",
165 });
166 const a = document.createElement("a");
167 a.href = URL.createObjectURL(blob);
168 a.download = `${PROBLEM_ID}.${run.instanceId}.${run.solverId}.browser.json`;
169 a.click();
170 URL.revokeObjectURL(a.href);
171 }
173 function loadFiles(files: FileList | null) {
174 if (!files) return;
175 for (const file of Array.from(files)) {
176 file.text().then((text) => {
177 try {
178 const data: unknown = JSON.parse(text);
179 if (isResultFile(data)) {
180 setLoaded((ls) => [...ls, data]);
181 } else {
182 alert(`${file.name} is not a fastandaccurate result file`);
183 }
184 } catch {
185 alert(`${file.name}: not valid JSON`);
186 }
187 });
188 }
189 }
191 const cliUrl = `https://concept-collection.github.io/fastandaccurate/cli.tgz?v=${__BUILD_ID__}`;
193 return (
194 <main>
195 <h1>fastandaccurate</h1>
196 <p className="subtitle">Speed and accuracy benchmarks for PDE solvers</p>
197 <p>
198 Each <strong>problem</strong> here is posed in the continuum, with an
199 exact reference solution; a solver chooses its own discretization and
200 is scored at problem-specified evaluation points. The central object
201 is the <strong>work-precision curve</strong>: error against compute
202 time as the solver's resolution varies. There is deliberately no
203 single ranking; which curve is best can differ by accuracy regime,
204 instance, and machine. Results shown here are committed to a public{" "}
205 <a href={RESULTS_REPO_URL}>results repository</a> by pull request, and
206 any in-browser solver can be rerun on your own machine, right on this
207 page, to check them.
208 </p>
209 <p className="small muted">
210 <a href={REPO_URL}>Source</a> · <a href={SPEC_URL}>Problem specification</a> ·{" "}
211 <a href={RESULTS_REPO_URL}>Results repository</a> · Solvers run in
212 MATLAB syntax via <a href="https://numbl.org">numbl</a>, client side.
213 </p>
215 <h2>The problem: laplace-dirichlet-2d</h2>
216 <p>
217 Solve Δu = 0 on the star-shaped domain with boundary
218 r(θ) = 1 + a·cos(kθ), with Dirichlet data u = g on the boundary. The
219 data comes from an exact harmonic function, a sum of three logarithmic
220 point sources placed a distance d outside the boundary, so errors are
221 measured against the true solution, not a reference computation. The
222 distance d sets the difficulty: the closer the sources, the shorter
223 the distance the data continues harmonically past the boundary, and
224 methods whose representations assume that continuation lose it. A
225 solver receives the curve (with derivatives), the boundary data as a
226 function of the boundary parameter, and the evaluation points, and
227 returns solution values at those points; reported time is the whole
228 solve including the solver's own discretization (median of repeats
229 after one untimed warmup). The precise statement, interface, and
230 protocol are in the <a href={SPEC_URL}>specification</a>.
231 </p>
232 <div className="row" style={{ marginTop: 14 }}>
233 <div>
234 <div style={{ marginBottom: 10 }}>
235 <label>
236 instance{" "}
237 <select
238 value={instanceId}
239 onChange={(e) => setInstanceId(e.target.value)}
240 >
241 {INSTANCES.map((i) => (
242 <option key={i.id} value={i.id}>
243 {i.id} — {i.label}
244 </option>
245 ))}
246 </select>
247 </label>
248 </div>
249 <p className="small muted" style={{ maxWidth: 380 }}>
250 {inst.description}
251 </p>
252 <table className="data">
253 <tbody>
254 <tr>
255 <th className="left">a</th>
256 <td>{inst.a}</td>
257 <th className="left">k</th>
258 <td>{inst.k}</td>
259 <th className="left">d</th>
260 <td>{inst.d}</td>
261 </tr>
262 </tbody>
263 </table>
264 </div>
265 <DomainView inst={inst} />
266 </div>
268 <h2>Work-precision results</h2>
269 {committedError && (
270 <p className="small muted">
271 Committed results could not be loaded ({committedError}); showing
272 local runs only.
273 </p>
274 )}
275 <WorkPrecisionChart curves={curves} />
276 <div className="row" style={{ marginTop: 14, alignItems: "center" }}>
277 {SOLVERS.map((s) => (
278 <span key={s.id} style={{ whiteSpace: "nowrap" }}>
279 <label>
280 <input
281 type="checkbox"
282 checked={!hidden.has(s.id)}
283 onChange={(e) => {
284 setHidden((h) => {
285 const next = new Set(h);
286 if (e.target.checked) next.delete(s.id);
287 else next.add(s.id);
288 return next;
289 });
290 }}
291 />{" "}
292 <span
293 className="legend-swatch"
294 style={{ background: solverColorVar(s.id, allSolverIds) }}
295 />
296 {s.name}
297 </label>{" "}
298 <button
299 onClick={() => runSolver(s.id)}
300 disabled={running !== null}
301 title={`Run the full ${s.id} sweep on ${instanceId} in this browser`}
302 >
303 {running === s.id ? "running…" : "Run in this browser"}
304 </button>
305 </span>
306 ))}
307 <label>
308 repeats{" "}
309 <select
310 value={repeats}
311 onChange={(e) => setRepeats(parseInt(e.target.value, 10))}
312 >
313 {[1, 3, 5].map((r) => (
314 <option key={r} value={r}>
315 {r}
316 </option>
317 ))}
318 </select>
319 </label>
320 </div>
321 {runStatus && <p className="small muted">{runStatus}</p>}
322 <div className="row" style={{ marginTop: 10, alignItems: "center" }}>
323 <label className="small">
324 machine label{" "}
325 <input
326 type="text"
327 placeholder="e.g. office workstation"
328 value={machineLabel}
329 onChange={(e) => setMachineLabel(e.target.value)}
330 />
331 </label>
332 {localRuns
333 .filter((r) => r.done && r.instanceId === instanceId)
334 .map((r) => (
335 <button key={r.key} onClick={() => downloadRun(r)}>
336 Download {r.solverId} result JSON
337 </button>
338 ))}
339 <label className="small">
340 load result file{" "}
341 <input
342 type="file"
343 accept=".json,application/json"
344 multiple
345 onChange={(e) => loadFiles(e.target.files)}
346 />
347 </label>
348 </div>
349 <PointsTable curves={curves} />
351 <h2>Solution and error</h2>
352 <p className="small muted" style={{ maxWidth: 640 }}>
353 Compute one solve at a chosen resolution and compare the field with
354 the exact solution. The solution uses a diverging scale about zero;
355 the error is the absolute pointwise difference on a log scale.
356 </p>
357 <SolutionSection inst={inst} />
359 <h2>Run it outside the browser</h2>
360 <p style={{ maxWidth: 720 }}>
361 The same harness runs in node, with the same solvers, the same
362 protocol, and the same result format (node 20 or newer; no install
363 step):
364 </p>
365 <pre>{`npx ${cliUrl} run --label "my workstation"`}</pre>
366 <p style={{ maxWidth: 720 }}>
367 This writes one result JSON per instance and solver. To benchmark
368 your own solver, point the harness at a MATLAB function file that
369 implements the problem's solver interface:
370 </p>
371 <pre>{`npx ${cliUrl} run --solver-file my_method.m --solver-id my-method`}</pre>
372 <p style={{ maxWidth: 720 }}>
373 Result files can be loaded above (load result file) to view them
374 against the committed curves before submitting anything. To publish,
375 open a pull request adding the files under <code>results/</code> in
376 the <a href={RESULTS_REPO_URL}>results repository</a>; provenance
377 (machine, runtime, numbl version, solver version) travels inside each
378 file. Solvers in other languages are planned to enter the same way:
379 run offline, produce the same result format, submit by PR, with the
380 file marked as not reproducible in the browser.
381 </p>
383 <footer>
384 fastandaccurate · Apache-2.0 ·{" "}
385 <a href={REPO_URL}>concept-collection/fastandaccurate</a> · numbl{" "}
386 {__NUMBL_VERSION__}
387 </footer>
388 </main>
389 );
moveopenescclose