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 { solverSource } 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 Each solver is a MATLAB function file implementing the interface in
269 the <a href={SPEC_URL}>specification</a>. Most run through numbl, in
270 the browser and from the command line alike; the rest run only in
271 real MATLAB, through the command line. The <code>-mat</code> entries
272 are their numbl twin's file run that way, so each such pair measures
273 the runtime rather than the method.
274 </p>
275 {SOLVERS.map((s) => (
276 <div
277 key={s.id}
278 className="panel"
279 style={{ maxWidth: 860, marginBottom: 12 }}
280 >
281 <div>
282 <span
283 className="legend-swatch"
284 style={{ background: solverColorVar(s.id, allSolverIds) }}
285 />
286 <strong>{s.name}</strong>{" "}
287 <span className="small muted">
288 {s.id} v{s.version} · {s.backend} ·{" "}
289 {s.runtime === "matlab"
290 ? "runs in MATLAB via the command line"
291 : "runs via numbl in the browser and command line"}
292 {s.sourceDir ? ` · same solver.m as ${s.sourceDir}` : ""}
293 </span>
294 </div>
295 <p className="small" style={{ color: "var(--text-2)" }}>
296 {s.description}
297 </p>
298 <details>
299 <summary className="small" style={{ cursor: "pointer" }}>
300 solver.m
301 </summary>
302 <pre style={{ maxHeight: 420, overflow: "auto", marginTop: 8 }}>
303 {solverSource(s.id)}
304 </pre>
305 </details>
306 <a
307 className="small"
308 href={`${REPO_URL}/blob/main/src/solvers/${solverSourceDir(s)}/solver.m`}
309 >
310 view on GitHub
311 </a>
312 </div>
313 ))}
315 <h2>Work-precision results</h2>
316 {committedError && (
317 <p className="small muted">
318 Committed results could not be loaded ({committedError}); showing
319 local runs only.
320 </p>
321 )}
322 <WorkPrecisionChart curves={curves} />
323 <div className="row" style={{ marginTop: 14, alignItems: "center" }}>
324 {SOLVERS.map((s) => (
325 <span key={s.id} style={{ whiteSpace: "nowrap" }}>
326 <label>
327 <input
328 type="checkbox"
329 checked={!hidden.has(s.id)}
330 onChange={(e) => {
331 setHidden((h) => {
332 const next = new Set(h);
333 if (e.target.checked) next.delete(s.id);
334 else next.add(s.id);
335 return next;
336 });
337 }}
338 />{" "}
339 <span
340 className="legend-swatch"
341 style={{ background: solverColorVar(s.id, allSolverIds) }}
342 />
343 {s.name}
344 </label>{" "}
345 {s.runtime === "numbl" ? (
346 <button
347 onClick={() => runSolver(s.id)}
348 disabled={running !== null}
349 title={`Run the full ${s.id} sweep on ${instanceId} in this browser`}
350 >
351 {running === s.id ? "running…" : "Run in this browser"}
352 </button>
353 ) : (
354 <span className="small muted">MATLAB only (via the CLI)</span>
355 )}
356 </span>
357 ))}
358 <label
359 title={
360 `Each point is timed at least this many times, then repeated ` +
361 `until it has used ${DEFAULT_TIMING.timeBudgetSeconds} s or ` +
362 `${DEFAULT_TIMING.maxTimedRuns} runs, and the fastest run is ` +
363 `reported.`
364 }
365 >
366 min timed runs{" "}
367 <select
368 value={minRuns}
369 onChange={(e) => setMinRuns(parseInt(e.target.value, 10))}
370 >
371 {[1, 3, 5, 10].map((r) => (
372 <option key={r} value={r}>
373 {r}
374 </option>
375 ))}
376 </select>
377 </label>
378 </div>
379 {runStatus && <p className="small muted">{runStatus}</p>}
380 <h3>Run this on your machine</h3>
381 <p className="small muted" style={{ maxWidth: 640 }}>
382 This command runs the{" "}
383 {visibleSolverList.length === 1
384 ? `${visibleSolverList[0].id} sweep`
385 : "same sweeps"}{" "}
386 on the <code>{instanceId}</code> instance (node 20 or newer) and
387 writes result JSON files. Load them below to see them on this chart,
388 or submit them by pull request (see <a href="#/about">About</a>).
389 </p>
390 <div className="row" style={{ alignItems: "flex-start", gap: 10 }}>
391 <pre style={{ margin: 0, flex: "1 1 420px", overflowX: "auto" }}>
392 {cliCommand}
393 </pre>
394 <button
395 onClick={() => {
396 navigator.clipboard.writeText(cliCommand).then(() => {
397 setCopied(true);
398 setTimeout(() => setCopied(false), 1500);
399 });
400 }}
401 >
402 {copied ? "copied" : "Copy"}
403 </button>
404 </div>
405 <div className="row" style={{ marginTop: 10, alignItems: "center" }}>
406 <label className="small">
407 load result file{" "}
408 <input
409 type="file"
410 accept=".json,application/json"
411 multiple
412 onChange={(e) => loadFiles(e.target.files)}
413 />
414 </label>
415 </div>
416 <PointsTable curves={curves} />
418 <h2>Solution and error</h2>
419 <p className="small muted" style={{ maxWidth: 640 }}>
420 Compute one solve at a chosen resolution and compare the field with
421 the exact solution, on a shared color scale. The error map shows the
422 absolute pointwise difference on a log scale; the errors reported in
423 the results above are measured at the evaluation points, which can
424 be shown on the error map.
425 </p>
426 <SolutionSection inst={inst} />
427 </>
428 );
429}