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