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 {
22 WorkPrecisionChart,
23 type ChartCurve,
24} from "../components/WorkPrecisionChart";
25import { PointsTable } from "../components/PointsTable";
26import { DomainView } from "../components/DomainView";
27import { SolutionSection } from "../components/SolutionSection";
29const REPO_URL = "https://github.com/concept-collection/fastandaccurate";
30const SPEC_URL = `${REPO_URL}/blob/main/docs/problems/laplace-dirichlet-2d.md`;
32interface LocalRun {
33 key: string;
34 solverId: string;
35 instanceId: string;
36 repeats: number;
37 points: ResultPoint[];
38 done: boolean;
39}
41export function ProblemPage({ problemId }: { problemId: string }) {
42 const [instanceId, setInstanceId] = useState(INSTANCES[1].id);
43 const [committed, setCommitted] = useState<ResultFile[] | null>(null);
44 const [committedError, setCommittedError] = useState<string | null>(null);
45 const [loaded, setLoaded] = useState<ResultFile[]>([]);
46 const [localRuns, setLocalRuns] = useState<LocalRun[]>([]);
47 const [hidden, setHidden] = useState<Set<string>>(new Set());
48 const [running, setRunning] = useState<string | null>(null);
49 const [runStatus, setRunStatus] = useState<string | null>(null);
50 const [repeats, setRepeats] = useState(3);
51 const [machineLabel, setMachineLabel] = useState("");
53 const inst = getInstance(instanceId);
55 useEffect(() => {
56 fetchCommittedResults()
57 .then(setCommitted)
58 .catch((err) =>
59 setCommittedError(err instanceof Error ? err.message : String(err))
60 );
61 }, []);
63 const allSolverIds = useMemo(() => {
64 const ids = new Set<string>(SOLVERS.map((s) => s.id));
65 committed?.forEach((r) => ids.add(r.solver.id));
66 loaded.forEach((r) => ids.add(r.solver.id));
67 return [...ids];
68 }, [committed, loaded]);
70 const curves: ChartCurve[] = useMemo(() => {
71 const out: ChartCurve[] = [];
72 const color = (id: string) => solverColorVar(id, allSolverIds);
73 committed
74 ?.filter((r) => r.problem === PROBLEM_ID && r.instance === instanceId)
75 .forEach((r, i) => {
76 out.push({
77 key: `committed:${i}`,
78 solverId: r.solver.id,
79 label: `${r.solver.id} — ${environmentLabel(r)}`,
80 color: color(r.solver.id),
81 points: r.points,
82 });
83 });
84 loaded
85 .filter((r) => r.problem === PROBLEM_ID && r.instance === instanceId)
86 .forEach((r, i) => {
87 out.push({
88 key: `loaded:${i}`,
89 solverId: r.solver.id,
90 label: `${r.solver.id} — ${environmentLabel(r)} (loaded)`,
91 color: color(r.solver.id),
92 dash: "8 4",
93 points: r.points,
94 });
95 });
96 localRuns
97 .filter((r) => r.instanceId === instanceId)
98 .forEach((r) => {
99 out.push({
100 key: r.key,
101 solverId: r.solverId,
102 label: `${r.solverId} — this browser`,
103 color: color(r.solverId),
104 dash: "4 4",
105 open: true,
106 points: r.points,
107 });
108 });
109 return out.filter((c) => !hidden.has(c.solverId));
110 }, [committed, loaded, localRuns, instanceId, hidden, allSolverIds]);
112 async function runSolver(solverId: string) {
113 const key = `local:${solverId}:${instanceId}:${Date.now()}`;
114 setLocalRuns((rs) => [
115 ...rs.filter(
116 (r) => !(r.solverId === solverId && r.instanceId === instanceId)
117 ),
118 { key, solverId, instanceId, repeats, points: [], done: false },
119 ]);
120 setRunning(solverId);
121 try {
122 const points = await sweepInBrowser(
123 instanceId,
124 solverId,
125 repeats,
126 (point, index, total) => {
127 setRunStatus(
128 `${solverId} on ${instanceId}: point ${index + 1}/${total} (n = ${point.n}) — rel max error ${point.relMax.toExponential(2)}`
129 );
130 setLocalRuns((rs) =>
131 rs.map((r) =>
132 r.key === key ? { ...r, points: [...r.points, point] } : r
133 )
134 );
135 }
136 );
137 setLocalRuns((rs) =>
138 rs.map((r) => (r.key === key ? { ...r, points, done: true } : r))
139 );
140 setRunStatus(null);
141 } catch (err) {
142 setRunStatus(
143 `${solverId} failed: ${err instanceof Error ? err.message : String(err)}`
144 );
145 } finally {
146 setRunning(null);
147 }
148 }
150 async function downloadRun(run: LocalRun) {
151 const manifest = getSolver(run.solverId);
152 const result = await buildResultFile({
153 instance: getInstance(run.instanceId),
154 solver: {
155 id: manifest.id,
156 version: manifest.version,
157 backend: manifest.backend,
158 source: "builtin",
159 },
160 environment: {
161 kind: "browser",
162 runtime: navigator.userAgent,
163 numblVersion: __NUMBL_VERSION__,
164 machineLabel: machineLabel || undefined,
165 browserReproducible: true,
166 },
167 repeats: run.repeats,
168 points: run.points,
169 });
170 const blob = new Blob([JSON.stringify(result, null, 2) + "\n"], {
171 type: "application/json",
172 });
173 const a = document.createElement("a");
174 a.href = URL.createObjectURL(blob);
175 a.download = `${PROBLEM_ID}.${run.instanceId}.${run.solverId}.browser.json`;
176 a.click();
177 URL.revokeObjectURL(a.href);
178 }
180 function loadFiles(files: FileList | null) {
181 if (!files) return;
182 for (const file of Array.from(files)) {
183 file.text().then((text) => {
184 try {
185 const data: unknown = JSON.parse(text);
186 if (isResultFile(data)) {
187 setLoaded((ls) => [...ls, data]);
188 } else {
189 alert(`${file.name} is not a fastandaccurate result file`);
190 }
191 } catch {
192 alert(`${file.name}: not valid JSON`);
193 }
194 });
195 }
196 }
198 if (problemId !== PROBLEM_ID) {
199 return (
200 <>
201 <p className="small">
202 <a href="#/">← problems</a>
203 </p>
204 <h1>Unknown problem</h1>
205 <p>
206 No problem named <code>{problemId}</code>.{" "}
207 <a href="#/">Back to the problem list.</a>
208 </p>
209 </>
210 );
211 }
213 return (
214 <>
215 <p className="small">
216 <a href="#/">← problems</a>
217 </p>
218 <h1>
219 <code>{PROBLEM_ID}</code>
220 </h1>
221 <p className="subtitle">
222 Interior Dirichlet Laplace problem on a star-shaped 2D domain
223 </p>
224 <p>
225 Solve Δu = 0 on the domain with boundary r(θ) = 1 + a·cos(kθ), with
226 Dirichlet data u = g on the boundary. The data comes from an exact
227 harmonic function, a sum of three logarithmic point sources placed a
228 distance d outside the boundary, so errors are measured against the
229 true solution rather than a reference computation. The distance d
230 sets the difficulty: the closer the sources, the shorter the distance
231 the data continues harmonically past the boundary, and methods whose
232 representations assume that continuation lose it. A solver receives
233 the curve (with derivatives), the boundary data as a function of the
234 boundary parameter, and the evaluation points, and returns solution
235 values at those points. The precise statement, solver interface, and
236 timing protocol are in the <a href={SPEC_URL}>specification</a>.
237 </p>
238 <div className="row" style={{ marginTop: 14 }}>
239 <div>
240 <div style={{ marginBottom: 10 }}>
241 <label>
242 instance{" "}
243 <select
244 value={instanceId}
245 onChange={(e) => setInstanceId(e.target.value)}
246 >
247 {INSTANCES.map((i) => (
248 <option key={i.id} value={i.id}>
249 {i.id} — {i.label}
250 </option>
251 ))}
252 </select>
253 </label>
254 </div>
255 <p className="small muted" style={{ maxWidth: 380 }}>
256 {inst.description}
257 </p>
258 <table className="data">
259 <tbody>
260 <tr>
261 <th className="left">a</th>
262 <td>{inst.a}</td>
263 <th className="left">k</th>
264 <td>{inst.k}</td>
265 <th className="left">d</th>
266 <td>{inst.d}</td>
267 </tr>
268 </tbody>
269 </table>
270 </div>
271 <DomainView inst={inst} />
272 </div>
274 <h2>Work-precision results</h2>
275 {committedError && (
276 <p className="small muted">
277 Committed results could not be loaded ({committedError}); showing
278 local runs only.
279 </p>
280 )}
281 <WorkPrecisionChart curves={curves} />
282 <div className="row" style={{ marginTop: 14, alignItems: "center" }}>
283 {SOLVERS.map((s) => (
284 <span key={s.id} style={{ whiteSpace: "nowrap" }}>
285 <label>
286 <input
287 type="checkbox"
288 checked={!hidden.has(s.id)}
289 onChange={(e) => {
290 setHidden((h) => {
291 const next = new Set(h);
292 if (e.target.checked) next.delete(s.id);
293 else next.add(s.id);
294 return next;
295 });
296 }}
297 />{" "}
298 <span
299 className="legend-swatch"
300 style={{ background: solverColorVar(s.id, allSolverIds) }}
301 />
302 {s.name}
303 </label>{" "}
304 <button
305 onClick={() => runSolver(s.id)}
306 disabled={running !== null}
307 title={`Run the full ${s.id} sweep on ${instanceId} in this browser`}
308 >
309 {running === s.id ? "running…" : "Run in this browser"}
310 </button>
311 </span>
312 ))}
313 <label>
314 repeats{" "}
315 <select
316 value={repeats}
317 onChange={(e) => setRepeats(parseInt(e.target.value, 10))}
318 >
319 {[1, 3, 5].map((r) => (
320 <option key={r} value={r}>
321 {r}
322 </option>
323 ))}
324 </select>
325 </label>
326 </div>
327 {runStatus && <p className="small muted">{runStatus}</p>}
328 <div className="row" style={{ marginTop: 10, alignItems: "center" }}>
329 <label className="small">
330 machine label{" "}
331 <input
332 type="text"
333 placeholder="e.g. office workstation"
334 value={machineLabel}
335 onChange={(e) => setMachineLabel(e.target.value)}
336 />
337 </label>
338 {localRuns
339 .filter((r) => r.done && r.instanceId === instanceId)
340 .map((r) => (
341 <button key={r.key} onClick={() => downloadRun(r)}>
342 Download {r.solverId} result JSON
343 </button>
344 ))}
345 <label className="small">
346 load result file{" "}
347 <input
348 type="file"
349 accept=".json,application/json"
350 multiple
351 onChange={(e) => loadFiles(e.target.files)}
352 />
353 </label>
354 </div>
355 <PointsTable curves={curves} />
357 <h2>Solution and error</h2>
358 <p className="small muted" style={{ maxWidth: 640 }}>
359 Compute one solve at a chosen resolution and compare the field with
360 the exact solution. The solution uses a diverging scale about zero;
361 the error is the absolute pointwise difference on a log scale.
362 </p>
363 <SolutionSection inst={inst} />
365 <p className="small muted" style={{ marginTop: "2.2rem" }}>
366 The same sweeps run outside the browser with the command line, and
367 both browser and command-line results are submitted by pull request
368 to the <a href={RESULTS_REPO_URL}>results repository</a>; see{" "}
369 <a href="#/about">About</a>.
370 </p>
371 </>
372 );
373}