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