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