// Figures for the one-dimensional labs. Every lab in Part I sends back one of // these two shapes, so the same two panels serve all of them. import { Fragment } from 'react' import Plot from '../../plot/Plot.tsx' import Legend from '../../plot/Legend.tsx' import { EndLabel, Line, Markers, RefSlope } from '../../plot/Curves.tsx' import { ink, marker, method, type MethodKey } from '../../plot/palette.ts' import { extent, padDomain } from '../../plot/scales.ts' import type { Num } from '../../engine/types.ts' export interface SingleOut { names: string[] ms: number n: number x: number[] u: Num[] uex: number[] err: Num[] maxerr: Num xf: number[] uf: number[] } export interface SweepOut { names: string[] ns: number[] /** per-method resolutions, when the methods do not share a ladder */ NS?: Num[][] E: Num[][] T: Num[][] orders: Num[][] } /** The x values for series k: its own ladder if it has one. */ function xsOf(data: SweepOut, k: number): number[] { const row = data.NS?.[k] if (!row) return data.ns return row.map((v) => (v == null ? NaN : v)) } export function fmtErr(v: Num): string { if (v == null || !isFinite(v)) return '--' if (v === 0) return '0' return v.toExponential(2) } /** The computed solution over the exact one, and the error underneath. */ export function Solution1D({ data, slot = 'fd2' }: { data: SingleOut; slot?: MethodKey }) { const color = method[slot] const yd = padDomain(extent(data.uf, data.u)) const ed = padDomain(extent(data.err)) const showNodes = data.x.length <= 130 return ( <>
the solution: exact, and computed at the grid points
{(f) => ( <> {showNodes && } )}
the error, computed minus exact, at the grid points
{(f) => ( <> {showNodes && } )} ) } export interface SweepStyle { /** palette slot per method, in the order the script lists them */ slots: MethodKey[] /** dashed reference lines: exponent, label, and an optional anchor point */ refs?: { p: number; label: string; at?: [number, number] }[] /** per-series dash pattern, when two series share a colour on purpose */ dashes?: (string | undefined)[] } function seriesExtent(rows: Num[][]): [number, number] { let lo = Infinity let hi = -Infinity for (const r of rows) { for (const v of r) { if (v == null || !isFinite(v) || v <= 0) continue if (v < lo) lo = v if (v > hi) hi = v } } return isFinite(lo) ? [lo / 2, hi * 2] : [1e-16, 1] } /** Max error against resolution, on log-log axes. */ export function Convergence({ data, style, height = 300, xLabel = 'N', }: { data: SweepOut style: SweepStyle height?: number xLabel?: string }) { const yd = seriesExtent(data.E) const allx = data.NS ? data.NS.flat().filter((v): v is number => v != null && v > 0) : data.ns const xd: [number, number] = [Math.min(...allx) / 1.3, Math.max(...allx) * 1.3] // reference lines are anchored a factor of 5 below the first data point, so // that the reader compares slopes rather than trying to see through them const e0 = data.E[0]?.find((v) => v != null && isFinite(v) && v > 0) ?? 1 return ( <>
max error against resolution
{(f) => ( <> {style.refs?.map((r) => ( ))} {data.E.map((row, k) => { const slot = style.slots[k] ?? 'fd2' return ( ) })} )} ({ label: n, color: method[style.slots[k] ?? 'fd2'], dash: style.dashes?.[k], }))} /> ) } export function OrderTable({ data }: { data: SweepOut }) { return ( {data.names.map((n) => ( ))} ))} {data.ns.map((n, j) => ( {data.names.map((nm, k) => ( ))} ))}
{data.NS ? 'resolution' : 'N'} {n}
{data.names.map((n) => ( error order
{data.NS ? data.NS.map((r) => r[j] ?? '--').join(' / ') : n}{fmtErr(data.E[k][j])} {data.orders[k][j] == null ? '' : (data.orders[k][j] as number).toFixed(2)}
) } /** Error against wall clock: the only comparison that decides anything. */ export function WorkPrecision({ data, style, height = 320, label = 'milliseconds for one solve', }: { data: SweepOut style: SweepStyle height?: number label?: string }) { const yd = seriesExtent(data.E) const xd = seriesExtent(data.T) return ( <>
accuracy against cost; down and to the left is better
( <> {data.E.map((row, k) => { // label the cheapest end of each curve, which is where the // curves are furthest apart const t = data.T[k] let first = -1 for (let i = 0; i < row.length; i++) { if (row[i] != null && t[i] != null) { first = i break } } if (first < 0) return null const px = f.sx(t[first] as number) const right = px > f.iw * 0.65 return ( ) })} )} > {(f) => ( <> {data.E.map((row, k) => { const slot = style.slots[k] ?? 'fd2' const t = data.T[k] return ( v ?? NaN)} y={row} f={f} color={method[slot]} dash={style.dashes?.[k]} /> v ?? NaN)} y={row} f={f} color={method[slot]} shape={marker[slot]} /> ) })} )} ({ label: n, color: method[style.slots[k] ?? 'fd2'], dash: style.dashes?.[k], }))} /> ) }