/ concept-collection / fastandaccurate
Sign in
concept-collection / fastandaccurate
Shared exact-solution color scale in solution view; mark evaluation points on the error map
Jeremy Magland <jmagland@flatironinstitute.org> committed commit 66784cbaec97 parent a6326c5 Browse files
3 changed files+116−16
src/app/components/FieldView.tsxmodified+89−9View file
@@ -14,10 +14,42 @@ export interface FieldViewProps {
1414 mode: "diverging" | "logmag";
1515 title: string;
1616 caption?: string;
17+ /** Fixed color-scale range; when absent, the range comes from the data.
18+ * For diverging mode this should be symmetric about zero. */
19+ range?: { lo: number; hi: number };
20+ /** Points drawn on top of the field (e.g. the evaluation points). */
21+ overlayPoints?: { x: number; y: number }[];
1722 }
1823
19-export function FieldView({ inst, values, mode, title, caption }: FieldViewProps) {
24+/** Max |v| over the grid points inside the domain, for building a shared
25+ * diverging range across several fields. */
26+export function fieldAbsMax(
27+ inst: Laplace2dInstance,
28+ values: Float64Array
29+): number {
30+ const { xs } = vizGrid(inst);
31+ let m = 0;
32+ for (let ix = 0; ix < VIZ_NGRID; ix++) {
33+ for (let iy = 0; iy < VIZ_NGRID; iy++) {
34+ if (!insideDomain(inst, xs[ix], xs[iy])) continue;
35+ const v = values[ix * VIZ_NGRID + iy];
36+ if (isFinite(v)) m = Math.max(m, Math.abs(v));
37+ }
38+ }
39+ return m;
40+}
41+
42+export function FieldView({
43+ inst,
44+ values,
45+ mode,
46+ title,
47+ caption,
48+ range,
49+ overlayPoints,
50+}: FieldViewProps) {
2051 const canvasRef = useRef<HTMLCanvasElement>(null);
52+ const overlayRef = useRef<HTMLCanvasElement>(null);
2153 const barRef = useRef<HTMLCanvasElement>(null);
2254 const rangeRef = useRef<HTMLDivElement>(null);
2355
@@ -46,13 +78,17 @@ export function FieldView({ inst, values, mode, title, caption }: FieldViewProps
4678 let hi: number;
4779 let scale: (v: number) => number;
4880 if (mode === "diverging") {
49- const vabs = Math.max(Math.abs(vmin), Math.abs(vmax), 1e-300);
81+ const vabs = range
82+ ? Math.max(Math.abs(range.lo), Math.abs(range.hi), 1e-300)
83+ : Math.max(Math.abs(vmin), Math.abs(vmax), 1e-300);
5084 lo = -vabs;
5185 hi = vabs;
5286 scale = (v) => v / vabs; // [-1, 1]
5387 } else {
54- hi = Math.max(vmax, 1e-300);
55- lo = Math.max(vmin, hi * 1e-8, 1e-300);
88+ hi = range ? range.hi : Math.max(vmax, 1e-300);
89+ lo = range
90+ ? Math.max(range.lo, 1e-300)
91+ : Math.max(vmin, hi * 1e-8, 1e-300);
5692 const llo = Math.log10(lo);
5793 const lhi = Math.log10(hi);
5894 scale = (v) =>
@@ -107,23 +143,67 @@ export function FieldView({ inst, values, mode, title, caption }: FieldViewProps
107143 mode === "logmag" ? v.toExponential(1) : v.toPrecision(3);
108144 rangeRef.current.textContent = `${fmt(lo)} … ${fmt(hi)}`;
109145 }
146+
147+ // overlay: marked points (crisp, at display resolution)
148+ const overlay = overlayRef.current;
149+ if (overlay) {
150+ const disp = 300;
151+ const dpr = window.devicePixelRatio || 1;
152+ overlay.width = disp * dpr;
153+ overlay.height = disp * dpr;
154+ const octx = overlay.getContext("2d");
155+ if (octx) {
156+ octx.scale(dpr, dpr);
157+ octx.clearRect(0, 0, disp, disp);
158+ if (overlayPoints && overlayPoints.length > 0) {
159+ const { R } = vizGrid(inst);
160+ const tok = (name: string) =>
161+ getComputedStyle(document.documentElement)
162+ .getPropertyValue(name)
163+ .trim();
164+ octx.fillStyle = tok("--series-2");
165+ octx.strokeStyle = tok("--surface");
166+ octx.lineWidth = 1;
167+ for (const p of overlayPoints) {
168+ const px = ((p.x + R) / (2 * R)) * disp;
169+ const py = disp - ((p.y + R) / (2 * R)) * disp;
170+ octx.beginPath();
171+ octx.arc(px, py, 2.2, 0, 2 * Math.PI);
172+ octx.fill();
173+ octx.stroke();
174+ }
175+ }
176+ }
177+ }
110178 };
111179
112180 draw();
113181 const mq = window.matchMedia("(prefers-color-scheme: dark)");
114182 mq.addEventListener("change", draw);
115183 return () => mq.removeEventListener("change", draw);
116- }, [inst, values, mode]);
184+ }, [inst, values, mode, range, overlayPoints]);
117185
118186 return (
119187 <figure style={{ margin: 0 }}>
120188 <div className="small" style={{ fontWeight: 600, marginBottom: 4 }}>
121189 {title}
122190 </div>
123- <canvas
124- ref={canvasRef}
125- style={{ width: 300, height: 300, imageRendering: "auto" }}
126- />
191+ <div style={{ position: "relative", width: 300, height: 300 }}>
192+ <canvas
193+ ref={canvasRef}
194+ style={{ width: 300, height: 300, imageRendering: "auto" }}
195+ />
196+ <canvas
197+ ref={overlayRef}
198+ style={{
199+ position: "absolute",
200+ inset: 0,
201+ width: 300,
202+ height: 300,
203+ pointerEvents: "none",
204+ }}
205+ />
206+ </div>
127207 <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 4 }}>
128208 <canvas ref={barRef} style={{ width: 220, height: 10, borderRadius: 3 }} />
129209 </div>
src/app/components/SolutionSection.tsxmodified+23−5View file
@@ -4,11 +4,16 @@
44
55 import { useMemo, useState } from "react";
66 import type { Laplace2dInstance } from "../../problems/laplace2d/spec";
7-import { exactU, vizGrid, VIZ_NGRID } from "../../problems/laplace2d/exact";
7+import {
8+ evalPoints,
9+ exactU,
10+ vizGrid,
11+ VIZ_NGRID,
12+} from "../../problems/laplace2d/exact";
813 import { SOLVERS, getSolver } from "../../solvers";
914 import { solutionInBrowser } from "../workerClient";
1015 import type { ResultPoint } from "../../harness/resultSchema";
11-import { FieldView } from "./FieldView";
16+import { FieldView, fieldAbsMax } from "./FieldView";
1217
1318 function exactGridValues(inst: Laplace2dInstance): Float64Array {
1419 const { xs } = vizGrid(inst);
@@ -51,6 +56,16 @@ export function SolutionSection({ inst }: { inst: Laplace2dInstance }) {
5156 const solver = getSolver(solverId);
5257 const showsComputed = computed !== null && computed.solverId === solverId;
5358
59+ // One color scale shared by the exact and computed solution fields, set
60+ // by the dynamic range of the exact solution; a computed field that
61+ // exceeds it clips.
62+ const sharedRange = useMemo(() => {
63+ const vabs = fieldAbsMax(inst, exact);
64+ return { lo: -vabs, hi: vabs };
65+ }, [inst, exact]);
66+
67+ const marks = useMemo(() => evalPoints(inst), [inst]);
68+
5469 async function compute() {
5570 setBusy(true);
5671 setError(null);
@@ -105,16 +120,18 @@ export function SolutionSection({ inst }: { inst: Laplace2dInstance }) {
105120 inst={inst}
106121 values={exact}
107122 mode="diverging"
123+ range={sharedRange}
108124 title="Exact solution"
109- caption="The manufactured harmonic function, sampled on the visualization grid."
125+ caption="The exact solution on the visualization grid. The color scale is shared with the computed field."
110126 />
111127 {showsComputed && computed && (
112128 <FieldView
113129 inst={inst}
114130 values={computed.uGrid}
115131 mode="diverging"
132+ range={sharedRange}
116133 title={`${solver.name}, n = ${computed.n}`}
117- caption={`Computed in this browser: rel max error ${computed.point.relMax.toExponential(2)}, solve ${(computed.point.solveSeconds * 1000).toPrecision(3)} ms.`}
134+ 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.`}
118135 />
119136 )}
120137 {showsComputed && errField && computed && (
@@ -122,8 +139,9 @@ export function SolutionSection({ inst }: { inst: Laplace2dInstance }) {
122139 inst={inst}
123140 values={errField}
124141 mode="logmag"
142+ overlayPoints={marks}
125143 title="Pointwise error (log scale)"
126- caption="Absolute difference from the exact solution; the color scale spans 8 decades below the maximum."
144+ caption="Absolute difference from the exact solution; the color scale spans 8 decades below the maximum. Dots mark the 65 evaluation points where the reported errors are measured."
127145 />
128146 )}
129147 </div>
src/app/pages/ProblemPage.tsxmodified+4−2View file
@@ -337,8 +337,10 @@ export function ProblemPage({ problemId }: { problemId: string }) {
337337 <h2>Solution and error</h2>
338338 <p className="small muted" style={{ maxWidth: 640 }}>
339339 Compute one solve at a chosen resolution and compare the field with
340- the exact solution. The solution uses a diverging scale about zero;
341- the error is the absolute pointwise difference on a log scale.
340+ the exact solution, on a shared color scale. The error map shows the
341+ absolute pointwise difference on a log scale; the errors reported in
342+ the results above are measured at the 65 evaluation points, marked
343+ as dots on the error map.
342344 </p>
343345 <SolutionSection inst={inst} />
344346 </>
moveopenescclose