/ concept-collection / fastandaccurate
Sign in
concept-collection / fastandaccurate
fastandaccurate / src / app / pages / ProblemPage.tsx
391 lines · 12.9 KBCodeBlameHistory
404de06Split into home / problem / about pages; minimal landing with problems listJeremy Magland 1import { useEffect, useMemo, useState } from "react";
2import {
3 INSTANCES,
4 getInstance,
5 PROBLEM_ID,
6} from "../../problems/laplace2d/spec";
80e4f62Trim About, result files only from the CLI, copyable per-instance run commandJeremy Magland 7import { SOLVERS } from "../../solvers";
8import type { ResultFile, ResultPoint } from "../../harness/resultSchema";
10 environmentLabel,
11 fetchCommittedResults,
12 isResultFile,
13} from "../results";
14import { solverColorVar } from "../colors";
15import { sweepInBrowser } from "../workerClient";
26b5b49Show solver source code on the problem pageJeremy Magland 16import { solverSource } from "../matlabSources";
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);
80e4f62Trim About, result files only from the CLI, copyable per-instance run commandJeremy Magland 47 const [copied, setCopied] = useState(false);
49 const inst = getInstance(instanceId);
80e4f62Trim About, result files only from the CLI, copyable per-instance run commandJeremy Magland 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 </tr>
240 </tbody>
241 </table>
242 </div>
243 <DomainView inst={inst} />
244 </div>
26b5b49Show solver source code on the problem pageJeremy Magland 246 <h2>Solvers</h2>
247 <p className="small muted" style={{ maxWidth: 640 }}>
248 Each solver is a MATLAB function file implementing the interface in
249 the <a href={SPEC_URL}>specification</a>; the same file runs in the
250 browser via numbl and from the command line.
251 </p>
252 {SOLVERS.map((s) => (
253 <div
254 key={s.id}
255 className="panel"
256 style={{ maxWidth: 860, marginBottom: 12 }}
257 >
258 <div>
259 <span
260 className="legend-swatch"
261 style={{ background: solverColorVar(s.id, allSolverIds) }}
262 />
263 <strong>{s.name}</strong>{" "}
264 <span className="small muted">
265 {s.id} v{s.version} · {s.backend}
266 </span>
267 </div>
268 <p className="small" style={{ color: "var(--text-2)" }}>
269 {s.description}
270 </p>
271 <details>
272 <summary className="small" style={{ cursor: "pointer" }}>
273 solver.m
274 </summary>
275 <pre style={{ maxHeight: 420, overflow: "auto", marginTop: 8 }}>
276 {solverSource(s.id)}
277 </pre>
278 </details>
279 <a
280 className="small"
281 href={`${REPO_URL}/blob/main/src/solvers/${s.id}/solver.m`}
282 >
283 view on GitHub
284 </a>
285 </div>
286 ))}
404de06Split into home / problem / about pages; minimal landing with problems listJeremy Magland 288 <h2>Work-precision results</h2>
289 {committedError && (
290 <p className="small muted">
291 Committed results could not be loaded ({committedError}); showing
292 local runs only.
293 </p>
294 )}
295 <WorkPrecisionChart curves={curves} />
296 <div className="row" style={{ marginTop: 14, alignItems: "center" }}>
297 {SOLVERS.map((s) => (
298 <span key={s.id} style={{ whiteSpace: "nowrap" }}>
299 <label>
300 <input
301 type="checkbox"
302 checked={!hidden.has(s.id)}
303 onChange={(e) => {
304 setHidden((h) => {
305 const next = new Set(h);
306 if (e.target.checked) next.delete(s.id);
307 else next.add(s.id);
308 return next;
309 });
310 }}
311 />{" "}
312 <span
313 className="legend-swatch"
314 style={{ background: solverColorVar(s.id, allSolverIds) }}
315 />
316 {s.name}
317 </label>{" "}
318 <button
319 onClick={() => runSolver(s.id)}
320 disabled={running !== null}
321 title={`Run the full ${s.id} sweep on ${instanceId} in this browser`}
322 >
323 {running === s.id ? "running…" : "Run in this browser"}
324 </button>
325 </span>
326 ))}
327 <label>
328 repeats{" "}
329 <select
330 value={repeats}
331 onChange={(e) => setRepeats(parseInt(e.target.value, 10))}
332 >
333 {[1, 3, 5].map((r) => (
334 <option key={r} value={r}>
335 {r}
336 </option>
337 ))}
338 </select>
339 </label>
340 </div>
341 {runStatus && <p className="small muted">{runStatus}</p>}
80e4f62Trim About, result files only from the CLI, copyable per-instance run commandJeremy Magland 342 <h3>Run this on your machine</h3>
343 <p className="small muted" style={{ maxWidth: 640 }}>
a6326c5Remove design-rationale phrasing from site copy and READMEsJeremy Magland 344 This command runs the{" "}
346 ? `${visibleSolverList[0].id} sweep`
347 : "same sweeps"}{" "}
348 on the <code>{instanceId}</code> instance (node 20 or newer) and
a6326c5Remove design-rationale phrasing from site copy and READMEsJeremy Magland 349 writes result JSON files. Load them below to see them on this chart,
350 or submit them by pull request (see <a href="#/about">About</a>).
352 <div className="row" style={{ alignItems: "flex-start", gap: 10 }}>
353 <pre style={{ margin: 0, flex: "1 1 420px", overflowX: "auto" }}>
354 {cliCommand}
355 </pre>
356 <button
357 onClick={() => {
358 navigator.clipboard.writeText(cliCommand).then(() => {
359 setCopied(true);
360 setTimeout(() => setCopied(false), 1500);
361 });
362 }}
363 >
364 {copied ? "copied" : "Copy"}
365 </button>
366 </div>
404de06Split into home / problem / about pages; minimal landing with problems listJeremy Magland 367 <div className="row" style={{ marginTop: 10, alignItems: "center" }}>
368 <label className="small">
369 load result file{" "}
370 <input
371 type="file"
372 accept=".json,application/json"
373 multiple
374 onChange={(e) => loadFiles(e.target.files)}
375 />
376 </label>
377 </div>
378 <PointsTable curves={curves} />
380 <h2>Solution and error</h2>
381 <p className="small muted" style={{ maxWidth: 640 }}>
382 Compute one solve at a chosen resolution and compare the field with
66784cbShared exact-solution color scale in solution view; mark evaluation points on the error mapJeremy Magland 383 the exact solution, on a shared color scale. The error map shows the
384 absolute pointwise difference on a log scale; the errors reported in
7fcfeb0Densify evaluation set to 289 points; evaluation-point overlay now a toggle, default offJeremy Magland 385 the results above are measured at the evaluation points, which can
386 be shown on the error map.
388 <SolutionSection inst={inst} />
389 </>
390 );
moveopenescclose