// The solution-and-error view: compute one solve in the browser at a // chosen resolution and show the field next to its pointwise error // against the exact solution. import { useMemo, useState } from "react"; import type { Laplace2dInstance } from "../../problems/laplace2d/spec"; import { evalPoints, exactU, vizGrid, VIZ_NGRID, } from "../../problems/laplace2d/exact"; import { SOLVERS, getSolver, sweepNFor } from "../../solvers"; import { solutionInBrowser } from "../workerClient"; import type { ResultPoint } from "../../harness/resultSchema"; import { FieldView, fieldAbsMax } from "./FieldView"; function exactGridValues(inst: Laplace2dInstance): Float64Array { const { xs } = vizGrid(inst); const out = new Float64Array(VIZ_NGRID * VIZ_NGRID); for (let ix = 0; ix < VIZ_NGRID; ix++) { for (let iy = 0; iy < VIZ_NGRID; iy++) { out[ix * VIZ_NGRID + iy] = exactU(inst, xs[ix], xs[iy]); } } return out; } interface Computed { solverId: string; n: number; uGrid: Float64Array; point: ResultPoint; } // Only solvers that run in the browser can compute a field here. // Everything a browser can run: numbl in the worker, and the WebGPU // solvers on the worker's own device. const BROWSER_SOLVERS = SOLVERS.filter( (s) => s.runtime === "numbl" || s.runtime === "webgpu" ); export function SolutionSection({ inst }: { inst: Laplace2dInstance }) { const [solverId, setSolverId] = useState(BROWSER_SOLVERS[0].id); const [n, setN] = useState( BROWSER_SOLVERS[0].sweepN[Math.floor(BROWSER_SOLVERS[0].sweepN.length * 0.7)] ); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [computed, setComputed] = useState(null); const [showMarks, setShowMarks] = useState(false); const exact = useMemo(() => exactGridValues(inst), [inst]); const errField = useMemo(() => { if (!computed) return null; const out = new Float64Array(exact.length); for (let i = 0; i < exact.length; i++) { out[i] = Math.abs(computed.uGrid[i] - exact[i]); } return out; }, [computed, exact]); const solver = getSolver(solverId); const showsComputed = computed !== null && computed.solverId === solverId; // One color scale shared by the exact and computed solution fields, set // by the dynamic range of the exact solution; a computed field that // exceeds it clips. const sharedRange = useMemo(() => { const vabs = fieldAbsMax(inst, exact); return { lo: -vabs, hi: vabs }; }, [inst, exact]); const marks = useMemo(() => evalPoints(inst), [inst]); // The resolutions offered depend on the instance as well as the solver, // since a harder geometry sweeps a different range. If the instance // changes under a selected n that its list does not contain, fall back // to a resolution partway up the new list. const ns = useMemo(() => sweepNFor(solver, inst.id), [solver, inst.id]); const nSel = ns.includes(n) ? n : ns[Math.floor(ns.length * 0.7)]; async function compute() { setBusy(true); setError(null); try { const { point, uGrid } = await solutionInBrowser(inst.id, solverId, nSel); setComputed({ solverId, n: nSel, uGrid, point }); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } return (
{error &&

{error}

}
{showsComputed && computed && ( )} {showsComputed && errField && computed && ( )}
); }