1import { useEffect, useMemo, useState } from "react";
2import {
3 DEFAULT_INSTANCE,
4 INSTANCES,
5 getInstance,
6 PROBLEM_ID,
7} from "../../problems/laplace2d/spec";
8import { SOLVERS, solverSourceDir } from "../../solvers";
9import type { ResultFile, ResultPoint } from "../../harness/resultSchema";
10import {
11 environmentLabel,
12 fetchCommittedResults,
13 isResultFile,
14} from "../results";
15import { solverColorVar } from "../colors";
16import { sweepInBrowser } from "../workerClient";
17import { DEFAULT_TIMING } from "../../harness/timing";
18import { solverSourceFiles } from "../matlabSources";
19import {
20 WorkPrecisionChart,
21 type ChartCurve,
22} from "../components/WorkPrecisionChart";
23import { PointsTable } from "../components/PointsTable";
24import { DomainView } from "../components/DomainView";
25import { SolutionSection } from "../components/SolutionSection";
27const REPO_URL = "https://github.com/concept-collection/fastandaccurate";
28const SPEC_URL = `${REPO_URL}/blob/main/docs/problems/laplace-dirichlet-2d.md`;
30interface LocalRun {
31 key: string;
32 solverId: string;
33 instanceId: string;
34 minTimedRuns: number;
35 points: ResultPoint[];
36 done: boolean;
37}
39export function ProblemPage({ problemId }: { problemId: string }) {
40 const [instanceId, setInstanceId] = useState(DEFAULT_INSTANCE);
41 const [committed, setCommitted] = useState<ResultFile[] | null>(null);
42 const [committedError, setCommittedError] = useState<string | null>(null);
43 const [loaded, setLoaded] = useState<ResultFile[]>([]);
44 const [localRuns, setLocalRuns] = useState<LocalRun[]>([]);
45 const [hidden, setHidden] = useState<Set<string>>(new Set());
46 const [running, setRunning] = useState<string | null>(null);
47 const [runStatus, setRunStatus] = useState<string | null>(null);
48 const [minRuns, setMinRuns] = useState(DEFAULT_TIMING.minTimedRuns);
49 const [copied, setCopied] = useState(false);
51 const inst = getInstance(instanceId);
52 const visibleSolverList = SOLVERS.filter((s) => !hidden.has(s.id));
53 const cliCommand =
54 `npx https://concept-collection.github.io/fastandaccurate/cli.tgz?v=${__BUILD_ID__} ` +
55 `run --instance ${instanceId}` +
56 (visibleSolverList.length === 1 ? ` --solver ${visibleSolverList[0].id}` : "") +
57 ` --label "my machine"`;
59 useEffect(() => {
60 fetchCommittedResults()
61 .then(setCommitted)
62 .catch((err) =>
63 setCommittedError(err instanceof Error ? err.message : String(err))
64 );
65 }, []);
67 const allSolverIds = useMemo(() => {
68 const ids = new Set<string>(SOLVERS.map((s) => s.id));
69 committed?.forEach((r) => ids.add(r.solver.id));
70 loaded.forEach((r) => ids.add(r.solver.id));
71 return [...ids];
72 }, [committed, loaded]);
74 const curves: ChartCurve[] = useMemo(() => {
75 const out: ChartCurve[] = [];
76 const color = (id: string) => solverColorVar(id, allSolverIds);
77 committed
78 ?.filter((r) => r.problem === PROBLEM_ID && r.instance === instanceId)
79 .forEach((r, i) => {
80 out.push({
81 key: `committed:${i}`,
82 solverId: r.solver.id,
83 label: `${r.solver.id} — ${environmentLabel(r)}`,
84 color: color(r.solver.id),
85 points: r.points,
86 });
87 });
88 loaded
89 .filter((r) => r.problem === PROBLEM_ID && r.instance === instanceId)
90 .forEach((r, i) => {
91 out.push({
92 key: `loaded:${i}`,
93 solverId: r.solver.id,
94 label: `${r.solver.id} — ${environmentLabel(r)} (loaded)`,
95 color: color(r.solver.id),
96 dash: "8 4",
97 points: r.points,
98 });
99 });
100 localRuns
101 .filter((r) => r.instanceId === instanceId)
102 .forEach((r) => {
103 out.push({
104 key: r.key,
105 solverId: r.solverId,
106 label: `${r.solverId} — this browser`,
107 color: color(r.solverId),
108 dash: "4 4",
109 open: true,
110 points: r.points,
111 });
112 });
113 return out.filter((c) => !hidden.has(c.solverId));
114 }, [committed, loaded, localRuns, instanceId, hidden, allSolverIds]);
116 async function runSolver(solverId: string) {
117 const key = `local:${solverId}:${instanceId}:${Date.now()}`;
118 setLocalRuns((rs) => [
119 ...rs.filter(
120 (r) => !(r.solverId === solverId && r.instanceId === instanceId)
121 ),
122 { key, solverId, instanceId, minTimedRuns: minRuns, points: [], done: false },
123 ]);
124 setRunning(solverId);
125 try {
126 const points = await sweepInBrowser(
127 instanceId,
128 solverId,
129 { ...DEFAULT_TIMING, minTimedRuns: minRuns },
130 (point, index, total) => {
131 setRunStatus(
132 `${solverId} on ${instanceId}: point ${index + 1}/${total} (n = ${point.n}) — rel max error ${point.relMax.toExponential(2)}`
133 );
134 setLocalRuns((rs) =>
135 rs.map((r) =>
136 r.key === key ? { ...r, points: [...r.points, point] } : r
137 )
138 );
139 }
140 );
141 setLocalRuns((rs) =>
142 rs.map((r) => (r.key === key ? { ...r, points, done: true } : r))
143 );
144 setRunStatus(null);
145 } catch (err) {
146 setRunStatus(
147 `${solverId} failed: ${err instanceof Error ? err.message : String(err)}`
148 );
149 } finally {
150 setRunning(null);
151 }
152 }
154 function loadFiles(files: FileList | null) {
155 if (!files) return;
156 for (const file of Array.from(files)) {
157 file.text().then((text) => {
158 try {
159 const data: unknown = JSON.parse(text);
160 if (isResultFile(data)) {
161 setLoaded((ls) => [...ls, data]);
162 } else {
163 alert(`${file.name} is not a fastandaccurate result file`);
164 }
165 } catch {
166 alert(`${file.name}: not valid JSON`);
167 }
168 });
169 }
170 }
172 if (problemId !== PROBLEM_ID) {
173 return (
174 <>
175 <p className="small">
176 <a href="#/">← problems</a>
177 </p>
178 <h1>Unknown problem</h1>
179 <p>
180 No problem named <code>{problemId}</code>.{" "}
181 <a href="#/">Back to the problem list.</a>
182 </p>
183 </>
184 );
185 }
187 return (
188 <>
189 <p className="small">
190 <a href="#/">← problems</a>
191 </p>
192 <h1>
193 <code>{PROBLEM_ID}</code>
194 </h1>
195 <p className="subtitle">
196 Interior Dirichlet Laplace problem on a star-shaped 2D domain
197 </p>
198 <p>
199 Solve Δu = 0 on the domain, with Dirichlet data u = g on the
200 boundary. The boundary is r(θ) = 1 + a·cos(kθ) on most instances,
201 and on <code>square-corners</code> the superellipse
202 |x|^p + |y|^p = 1, a square whose corners are rounded to a radius
203 of about 1.4/p. The data comes from an exact
204 harmonic function, a sum of three logarithmic point sources placed a
205 distance d outside the boundary, so errors are measured against the
206 true solution rather than a reference computation. The distance d
207 sets the difficulty: the closer the sources, the shorter the distance
208 the data continues harmonically past the boundary, and methods whose
209 representations assume that continuation lose it. A solver receives
210 the curve (with derivatives), the boundary data as a function of the
211 boundary parameter, and the evaluation points, and returns solution
212 values at those points. Those points are the same 289 on most
213 instances; <code>square-corners</code> adds sixteen inside its
214 corners and <code>star-nearfield</code> thirty-two along the inward
215 normal, as close as 0.005 to the boundary, where a quadrature rule
216 with no near-field correction stops converging. The precise statement, solver interface, and
217 timing protocol are in the <a href={SPEC_URL}>specification</a>.
218 </p>
219 <div className="row" style={{ marginTop: 14 }}>
220 <div>
221 <div style={{ marginBottom: 10 }}>
222 <label>
223 instance{" "}
224 <select
225 value={instanceId}
226 onChange={(e) => setInstanceId(e.target.value)}
227 >
228 {INSTANCES.map((i) => (
229 <option key={i.id} value={i.id}>
230 {i.id} — {i.label}
231 </option>
232 ))}
233 </select>
234 </label>
235 </div>
236 <p className="small muted" style={{ maxWidth: 380 }}>
237 {inst.description}
238 </p>
239 <table className="data">
240 <tbody>
241 <tr>
242 {inst.shape === "rounded-square" ? (
243 <>
244 <th className="left">p</th>
245 <td>{inst.p}</td>
246 <th className="left">corners</th>
247 <td>4</td>
248 </>
249 ) : (
250 <>
251 <th className="left">a</th>
252 <td>{inst.a}</td>
253 <th className="left">k</th>
254 <td>{inst.k}</td>
255 </>
256 )}
257 <th className="left">d</th>
258 <td>{inst.d}</td>
259 </tr>
260 </tbody>
261 </table>
262 </div>
263 <DomainView inst={inst} />
264 </div>
266 <h2>Solvers</h2>
267 <p className="small muted" style={{ maxWidth: 640 }}>
268 Most solvers are MATLAB function files implementing the interface in
269 the <a href={SPEC_URL}>specification</a>, and most of those run
270 through numbl, in the browser and from the command line alike; the
271 rest run only in real MATLAB, through the command line. The{" "}
272 <code>-mat</code> entries are their numbl twin's file run that way,
273 so each such pair measures the runtime rather than the method.{" "}
274 <code>mfs-gpu</code> is the same specification's second form:
275 TypeScript and WGSL against the problem as a plain object, running on
276 a WebGPU device.
277 </p>
278 {SOLVERS.map((s) => (
279 <div
280 key={s.id}
281 className="panel"
282 style={{ maxWidth: 860, marginBottom: 12 }}
283 >
284 <div>
285 <span
286 className="legend-swatch"
287 style={{ background: solverColorVar(s.id, allSolverIds) }}
288 />
289 <strong>{s.name}</strong>{" "}
290 <span className="small muted">
291 {s.id} v{s.version} · {s.backend} ·{" "}
292 {s.runtime === "matlab"
293 ? "runs in MATLAB via the command line"
294 : s.runtime === "webgpu"
295 ? "runs on WebGPU in the browser and command line"
296 : "runs via numbl in the browser and command line"}
297 {s.sourceDir ? ` · same solver.m as ${s.sourceDir}` : ""}
298 </span>
299 </div>
300 <p className="small" style={{ color: "var(--text-2)" }}>
301 {s.description}
302 </p>
303 {solverSourceFiles(s.id).map((f) => (
304 <details key={f.name}>
305 <summary className="small" style={{ cursor: "pointer" }}>
306 {f.name}
307 </summary>
308 <pre style={{ maxHeight: 420, overflow: "auto", marginTop: 8 }}>
309 {f.code}
310 </pre>
311 </details>
312 ))}
313 <a
314 className="small"
315 href={`${REPO_URL}/tree/main/src/solvers/${solverSourceDir(s)}`}
316 >
317 view on GitHub
318 </a>
319 </div>
320 ))}
322 <h2>Work-precision results</h2>
323 {committedError && (
324 <p className="small muted">
325 Committed results could not be loaded ({committedError}); showing
326 local runs only.
327 </p>
328 )}
329 <WorkPrecisionChart curves={curves} />
330 <div className="row" style={{ marginTop: 14, alignItems: "center" }}>
331 {SOLVERS.map((s) => (
332 <span key={s.id} style={{ whiteSpace: "nowrap" }}>
333 <label>
334 <input
335 type="checkbox"
336 checked={!hidden.has(s.id)}
337 onChange={(e) => {
338 setHidden((h) => {
339 const next = new Set(h);
340 if (e.target.checked) next.delete(s.id);
341 else next.add(s.id);
342 return next;
343 });
344 }}
345 />{" "}
346 <span
347 className="legend-swatch"
348 style={{ background: solverColorVar(s.id, allSolverIds) }}
349 />
350 {s.name}
351 </label>{" "}
352 {s.runtime === "numbl" || s.runtime === "webgpu" ? (
353 <button
354 onClick={() => runSolver(s.id)}
355 disabled={running !== null}
356 title={`Run the full ${s.id} sweep on ${instanceId} in this browser`}
357 >
358 {running === s.id ? "running…" : "Run in this browser"}
359 </button>
360 ) : (
361 <span className="small muted">MATLAB only (via the CLI)</span>
362 )}
363 </span>
364 ))}
365 <label
366 title={
367 `Each point is timed at least this many times, then repeated ` +
368 `until it has used ${DEFAULT_TIMING.timeBudgetSeconds} s or ` +
369 `${DEFAULT_TIMING.maxTimedRuns} runs, and the fastest run is ` +
370 `reported.`
371 }
372 >
373 min timed runs{" "}
374 <select
375 value={minRuns}
376 onChange={(e) => setMinRuns(parseInt(e.target.value, 10))}
377 >
378 {[1, 3, 5, 10].map((r) => (
379 <option key={r} value={r}>
380 {r}
381 </option>
382 ))}
383 </select>
384 </label>
385 </div>
386 {runStatus && <p className="small muted">{runStatus}</p>}
387 <h3>Run this on your machine</h3>
388 <p className="small muted" style={{ maxWidth: 640 }}>
389 This command runs the{" "}
390 {visibleSolverList.length === 1
391 ? `${visibleSolverList[0].id} sweep`
392 : "same sweeps"}{" "}
393 on the <code>{instanceId}</code> instance (node 20 or newer) and
394 writes result JSON files. Load them below to see them on this chart,
395 or submit them by pull request (see <a href="#/about">About</a>).
396 </p>
397 <div className="row" style={{ alignItems: "flex-start", gap: 10 }}>
398 <pre style={{ margin: 0, flex: "1 1 420px", overflowX: "auto" }}>
399 {cliCommand}
400 </pre>
401 <button
402 onClick={() => {
403 navigator.clipboard.writeText(cliCommand).then(() => {
404 setCopied(true);
405 setTimeout(() => setCopied(false), 1500);
406 });
407 }}
408 >
409 {copied ? "copied" : "Copy"}
410 </button>
411 </div>
412 <div className="row" style={{ marginTop: 10, alignItems: "center" }}>
413 <label className="small">
414 load result file{" "}
415 <input
416 type="file"
417 accept=".json,application/json"
418 multiple
419 onChange={(e) => loadFiles(e.target.files)}
420 />
421 </label>
422 </div>
423 <PointsTable curves={curves} />
425 <h2>Solution and error</h2>
426 <p className="small muted" style={{ maxWidth: 640 }}>
427 Compute one solve at a chosen resolution and compare the field with
428 the exact solution, on a shared color scale. The error map shows the
429 absolute pointwise difference on a log scale; the errors reported in
430 the results above are measured at the evaluation points, which can
431 be shown on the error map.
432 </p>
433 <SolutionSection inst={inst} />
434 </>
435 );
436}