1// Figures for the one-dimensional labs. Every lab in Part I sends back one of
2// these two shapes, so the same two panels serve all of them.
3import { Fragment } from 'react'
4import Plot from '../../plot/Plot.tsx'
5import Legend from '../../plot/Legend.tsx'
6import { EndLabel, Line, Markers, RefSlope } from '../../plot/Curves.tsx'
7import { ink, marker, method, type MethodKey } from '../../plot/palette.ts'
8import { extent, padDomain } from '../../plot/scales.ts'
9import type { Num } from '../../engine/types.ts'
11export interface SingleOut {
12 names: string[]
13 ms: number
14 n: number
15 x: number[]
16 u: Num[]
17 uex: number[]
18 err: Num[]
19 maxerr: Num
20 xf: number[]
21 uf: number[]
22}
24export interface SweepOut {
25 names: string[]
26 ns: number[]
27 /** per-method resolutions, when the methods do not share a ladder */
28 NS?: Num[][]
29 E: Num[][]
30 T: Num[][]
31 orders: Num[][]
32}
34/** The x values for series k: its own ladder if it has one. */
35function xsOf(data: SweepOut, k: number): number[] {
36 const row = data.NS?.[k]
37 if (!row) return data.ns
38 return row.map((v) => (v == null ? NaN : v))
39}
41export function fmtErr(v: Num): string {
42 if (v == null || !isFinite(v)) return '--'
43 if (v === 0) return '0'
44 return v.toExponential(2)
45}
47/** The computed solution over the exact one, and the error underneath. */
48export function Solution1D({ data, slot = 'fd2' }: { data: SingleOut; slot?: MethodKey }) {
49 const color = method[slot]
50 const yd = padDomain(extent(data.uf, data.u))
51 const ed = padDomain(extent(data.err))
52 const showNodes = data.x.length <= 130
53 return (
54 <>
55 <div className="plot-title">the solution: exact, and computed at the grid points</div>
56 <Plot height={220} xDomain={[0, 1]} yDomain={yd} xLabel="x" yLabel="u">
57 {(f) => (
58 <>
59 <Line x={data.xf} y={data.uf} f={f} color={ink.reference} width={2} />
60 <Line x={data.x} y={data.u} f={f} color={color} width={1.5} dash="4 3" />
61 {showNodes && <Markers x={data.x} y={data.u} f={f} color={color} shape={marker[slot]} r={3.2} />}
62 </>
63 )}
64 </Plot>
65 <Legend
66 items={[
67 { label: 'exact solution', color: ink.reference },
68 { label: `computed (${data.n} intervals)`, color, dash: '4 3' },
69 ]}
70 />
71 <div className="plot-title">the error, computed minus exact, at the grid points</div>
72 <Plot height={170} xDomain={[0, 1]} yDomain={ed} xLabel="x" yLabel="error">
73 {(f) => (
74 <>
75 <Line x={[0, 1]} y={[0, 0]} f={f} color={ink.axis} width={1} />
76 <Line x={data.x} y={data.err} f={f} color={color} width={2} />
77 {showNodes && <Markers x={data.x} y={data.err} f={f} color={color} shape={marker[slot]} r={3.2} />}
78 </>
79 )}
80 </Plot>
81 </>
82 )
83}
85export interface SweepStyle {
86 /** palette slot per method, in the order the script lists them */
87 slots: MethodKey[]
88 /** dashed reference lines: exponent, label, and an optional anchor point */
89 refs?: { p: number; label: string; at?: [number, number] }[]
90 /** per-series dash pattern, when two series share a colour on purpose */
91 dashes?: (string | undefined)[]
92}
94function seriesExtent(rows: Num[][]): [number, number] {
95 let lo = Infinity
96 let hi = -Infinity
97 for (const r of rows) {
98 for (const v of r) {
99 if (v == null || !isFinite(v) || v <= 0) continue
100 if (v < lo) lo = v
101 if (v > hi) hi = v
102 }
103 }
104 return isFinite(lo) ? [lo / 2, hi * 2] : [1e-16, 1]
105}
107/** Max error against resolution, on log-log axes. */
108export function Convergence({
109 data,
110 style,
111 height = 300,
112 xLabel = 'N',
113}: {
114 data: SweepOut
115 style: SweepStyle
116 height?: number
117 xLabel?: string
118}) {
119 const yd = seriesExtent(data.E)
120 const allx = data.NS ? data.NS.flat().filter((v): v is number => v != null && v > 0) : data.ns
121 const xd: [number, number] = [Math.min(...allx) / 1.3, Math.max(...allx) * 1.3]
122 // reference lines are anchored a factor of 5 below the first data point, so
123 // that the reader compares slopes rather than trying to see through them
124 const e0 = data.E[0]?.find((v) => v != null && isFinite(v) && v > 0) ?? 1
125 return (
126 <>
127 <div className="plot-title">max error against resolution</div>
128 <Plot height={height} xDomain={xd} yDomain={yd} xLog yLog xLabel={xLabel} yLabel="max |error|">
129 {(f) => (
130 <>
131 {style.refs?.map((r) => (
132 <RefSlope
133 key={r.label}
134 f={f}
135 p={r.p}
136 through={r.at ?? [data.ns[0], (e0 as number) / 5]}
137 span={1}
138 label={r.label}
139 />
140 ))}
141 {data.E.map((row, k) => {
142 const slot = style.slots[k] ?? 'fd2'
143 return (
144 <g key={k}>
145 <Line x={xsOf(data, k)} y={row} f={f} color={method[slot]} dash={style.dashes?.[k]} />
146 <Markers x={xsOf(data, k)} y={row} f={f} color={method[slot]} shape={marker[slot]} />
147 </g>
148 )
149 })}
150 </>
151 )}
152 </Plot>
153 <Legend
154 items={data.names.map((n, k) => ({
155 label: n,
156 color: method[style.slots[k] ?? 'fd2'],
157 dash: style.dashes?.[k],
158 }))}
159 />
160 <OrderTable data={data} />
161 </>
162 )
163}
165export function OrderTable({ data }: { data: SweepOut }) {
166 return (
167 <table className="numtable">
168 <thead>
169 <tr>
170 <th>{data.NS ? 'resolution' : 'N'}</th>
171 {data.names.map((n) => (
172 <th key={n} colSpan={2}>
173 {n}
174 </th>
175 ))}
176 </tr>
177 <tr>
178 <th />
179 {data.names.map((n) => (
180 <Fragment key={n}>
181 <th>error</th>
182 <th>order</th>
183 </Fragment>
184 ))}
185 </tr>
186 </thead>
187 <tbody>
188 {data.ns.map((n, j) => (
189 <tr key={n}>
190 <td>{data.NS ? data.NS.map((r) => r[j] ?? '--').join(' / ') : n}</td>
191 {data.names.map((nm, k) => (
192 <Fragment key={nm}>
193 <td>{fmtErr(data.E[k][j])}</td>
194 <td style={{ color: ink.muted }}>
195 {data.orders[k][j] == null ? '' : (data.orders[k][j] as number).toFixed(2)}
196 </td>
197 </Fragment>
198 ))}
199 </tr>
200 ))}
201 </tbody>
202 </table>
203 )
204}
206/** Error against wall clock: the only comparison that decides anything. */
207export function WorkPrecision({
208 data,
209 style,
210 height = 320,
211 label = 'milliseconds for one solve',
212}: {
213 data: SweepOut
214 style: SweepStyle
215 height?: number
216 label?: string
217}) {
218 const yd = seriesExtent(data.E)
219 const xd = seriesExtent(data.T)
220 return (
221 <>
222 <div className="plot-title">accuracy against cost; down and to the left is better</div>
223 <Plot
224 height={height}
225 xDomain={xd}
226 yDomain={yd}
227 xLog
228 yLog
229 xLabel={label}
230 yLabel="max |error|"
231 margin={{ r: 30 }}
232 over={(f) => (
233 <>
234 {data.E.map((row, k) => {
235 // label the cheapest end of each curve, which is where the
236 // curves are furthest apart
237 const t = data.T[k]
238 let first = -1
239 for (let i = 0; i < row.length; i++) {
240 if (row[i] != null && t[i] != null) {
241 first = i
242 break
243 }
244 }
245 if (first < 0) return null
246 const px = f.sx(t[first] as number)
247 const right = px > f.iw * 0.65
248 return (
249 <EndLabel
250 key={k}
251 f={f}
252 x={t[first] as number}
253 y={row[first] as number}
254 text={data.names[k]}
255 color={method[style.slots[k] ?? 'fd2']}
256 anchor={right ? 'end' : 'start'}
257 dx={right ? -8 : 8}
258 dy={12}
259 />
260 )
261 })}
262 </>
263 )}
264 >
265 {(f) => (
266 <>
267 {data.E.map((row, k) => {
268 const slot = style.slots[k] ?? 'fd2'
269 const t = data.T[k]
270 return (
271 <g key={k}>
272 <Line x={t.map((v) => v ?? NaN)} y={row} f={f} color={method[slot]} dash={style.dashes?.[k]} />
273 <Markers x={t.map((v) => v ?? NaN)} y={row} f={f} color={method[slot]} shape={marker[slot]} />
274 </g>
275 )
276 })}
277 </>
278 )}
279 </Plot>
280 <Legend
281 items={data.names.map((n, k) => ({
282 label: n,
283 color: method[style.slots[k] ?? 'fd2'],
284 dash: style.dashes?.[k],
285 }))}
286 />
287 </>
288 )
289}