1// The solution-and-error view: compute one solve in the browser at a
2// chosen resolution and show the field next to its pointwise error
3// against the exact solution.
5import { useMemo, useState } from "react";
6import type { Laplace2dInstance } from "../../problems/laplace2d/spec";
7import {
8 evalPoints,
9 exactU,
10 vizGrid,
11 VIZ_NGRID,
12} from "../../problems/laplace2d/exact";
13import { SOLVERS, getSolver, sweepNFor } from "../../solvers";
14import { solutionInBrowser } from "../workerClient";
15import type { ResultPoint } from "../../harness/resultSchema";
16import { FieldView, fieldAbsMax } from "./FieldView";
18function exactGridValues(inst: Laplace2dInstance): Float64Array {
19 const { xs } = vizGrid(inst);
20 const out = new Float64Array(VIZ_NGRID * VIZ_NGRID);
21 for (let ix = 0; ix < VIZ_NGRID; ix++) {
22 for (let iy = 0; iy < VIZ_NGRID; iy++) {
23 out[ix * VIZ_NGRID + iy] = exactU(inst, xs[ix], xs[iy]);
24 }
25 }
26 return out;
27}
29interface Computed {
30 solverId: string;
31 n: number;
32 uGrid: Float64Array;
33 point: ResultPoint;
34}
36// Only solvers that run in the browser can compute a field here.
37// Everything a browser can run: numbl in the worker, and the WebGPU
38// solvers on the worker's own device.
39const BROWSER_SOLVERS = SOLVERS.filter(
40 (s) => s.runtime === "numbl" || s.runtime === "webgpu"
41);
43export function SolutionSection({ inst }: { inst: Laplace2dInstance }) {
44 const [solverId, setSolverId] = useState(BROWSER_SOLVERS[0].id);
45 const [n, setN] = useState<number>(
46 BROWSER_SOLVERS[0].sweepN[Math.floor(BROWSER_SOLVERS[0].sweepN.length * 0.7)]
47 );
48 const [busy, setBusy] = useState(false);
49 const [error, setError] = useState<string | null>(null);
50 const [computed, setComputed] = useState<Computed | null>(null);
51 const [showMarks, setShowMarks] = useState(false);
53 const exact = useMemo(() => exactGridValues(inst), [inst]);
55 const errField = useMemo(() => {
56 if (!computed) return null;
57 const out = new Float64Array(exact.length);
58 for (let i = 0; i < exact.length; i++) {
59 out[i] = Math.abs(computed.uGrid[i] - exact[i]);
60 }
61 return out;
62 }, [computed, exact]);
64 const solver = getSolver(solverId);
65 const showsComputed = computed !== null && computed.solverId === solverId;
67 // One color scale shared by the exact and computed solution fields, set
68 // by the dynamic range of the exact solution; a computed field that
69 // exceeds it clips.
70 const sharedRange = useMemo(() => {
71 const vabs = fieldAbsMax(inst, exact);
72 return { lo: -vabs, hi: vabs };
73 }, [inst, exact]);
75 const marks = useMemo(() => evalPoints(inst), [inst]);
77 // The resolutions offered depend on the instance as well as the solver,
78 // since a harder geometry sweeps a different range. If the instance
79 // changes under a selected n that its list does not contain, fall back
80 // to a resolution partway up the new list.
81 const ns = useMemo(() => sweepNFor(solver, inst.id), [solver, inst.id]);
82 const nSel = ns.includes(n) ? n : ns[Math.floor(ns.length * 0.7)];
84 async function compute() {
85 setBusy(true);
86 setError(null);
87 try {
88 const { point, uGrid } = await solutionInBrowser(inst.id, solverId, nSel);
89 setComputed({ solverId, n: nSel, uGrid, point });
90 } catch (err) {
91 setError(err instanceof Error ? err.message : String(err));
92 } finally {
93 setBusy(false);
94 }
95 }
97 return (
98 <div>
99 <div className="row" style={{ alignItems: "center", gap: 12, marginBottom: 12 }}>
100 <label>
101 solver{" "}
102 <select
103 value={solverId}
104 onChange={(e) => {
105 const id = e.target.value;
106 setSolverId(id);
107 const sw = sweepNFor(getSolver(id), inst.id);
108 setN(sw[Math.floor(sw.length * 0.7)]);
109 }}
110 >
111 {BROWSER_SOLVERS.map((s) => (
112 <option key={s.id} value={s.id}>
113 {s.name}
114 </option>
115 ))}
116 </select>
117 </label>
118 <label>
119 n{" "}
120 <select value={nSel} onChange={(e) => setN(parseInt(e.target.value, 10))}>
121 {ns.map((v) => (
122 <option key={v} value={v}>
123 {v}
124 </option>
125 ))}
126 </select>
127 </label>
128 <button className="primary" onClick={compute} disabled={busy}>
129 {busy ? "computing…" : "Compute in this browser"}
130 </button>
131 <label className="small">
132 <input
133 type="checkbox"
134 checked={showMarks}
135 onChange={(e) => setShowMarks(e.target.checked)}
136 />{" "}
137 show evaluation points on the error map
138 </label>
139 </div>
140 {error && <p className="small" style={{ color: "var(--series-2)" }}>{error}</p>}
141 <div className="row">
142 <FieldView
143 inst={inst}
144 values={exact}
145 mode="diverging"
146 range={sharedRange}
147 title="Exact solution"
148 caption="The exact solution on the visualization grid. The color scale is shared with the computed field."
149 />
150 {showsComputed && computed && (
151 <FieldView
152 inst={inst}
153 values={computed.uGrid}
154 mode="diverging"
155 range={sharedRange}
156 title={`${solver.name}, n = ${computed.n}`}
157 caption={`Computed in this browser: rel max error ${computed.point.relMax.toExponential(2)} at the evaluation points, solve ${(computed.point.solveSeconds * 1000).toPrecision(3)} ms. Same color scale as the exact solution.`}
158 />
159 )}
160 {showsComputed && errField && computed && (
161 <FieldView
162 inst={inst}
163 values={errField}
164 mode="logmag"
165 overlayPoints={showMarks ? marks : undefined}
166 title="Pointwise error (log scale)"
167 caption={
168 "Absolute difference from the exact solution; the color scale " +
169 "spans 8 decades below the maximum." +
170 (showMarks
171 ? ` Dots mark the ${marks.length} evaluation points where the reported errors are measured.`
172 : "")
173 }
174 />
175 )}
176 </div>
177 </div>
178 );
179}