1import Plot from '../plot/Plot.tsx'
2import Legend, { type LegendItem } from '../plot/Legend.tsx'
3import { dColor, ink, series } from '../plot/palette.ts'
4import { linePath, type Frame } from '../plot/scales.ts'
5import type { ConvergeOut, FuncName, NodeKind, Num } from '../engine/types.ts'
7interface Props {
8 out: ConvergeOut | null
9 running: boolean
10 stale: boolean
11 f: FuncName
12 nodes: NodeKind
13 ds: number[]
14 maxN: number
15 showSpline: boolean
16 showPoly: boolean
17 onChange: (patch: { ds?: number[]; maxN?: number; showSpline?: boolean; showPoly?: boolean }) => void
18 onRun: () => void
19}
21const D_CHOICES = [0, 1, 2, 3, 4, 5, 6, 8]
22const N_CHOICES = [80, 160, 320, 640]
24const fmt = (v: Num) => (v == null || !isFinite(v) ? '-' : v.toExponential(1))
25const fmtOrd = (v: Num) => (v == null || !isFinite(v) ? '' : v.toFixed(1))
27const F_LABEL: Record<FuncName, string> = {
28 runge: '1 / (1 + x²)',
29 sine: 'sin x',
30 abs: '|x|',
31 custom: 'the custom function',
32}
34export default function ConvergencePanel(props: Props) {
35 const { out, running, stale, f, nodes, ds, maxN, showSpline, showPoly, onChange, onRun } = props
37 const toggleD = (d: number) => {
38 const next = ds.includes(d) ? ds.filter((v) => v !== d) : [...ds, d].sort((a, b) => a - b)
39 if (next.length > 0 && next.length <= 6) onChange({ ds: next })
40 }
42 return (
43 <div className="panel">
44 <p className="panel-lede">
45 Theorem 2: for d ≥ 1 the error is O(h<sup>d+1</sup>) as h → 0, whatever the nodes look like,
46 provided f is smooth enough. On log-log axes that is a straight line of slope −(d+1), and the
47 slopes measured between consecutive n are printed in the table. With uniform nodes and Runge's
48 function this reproduces Table 1; turn the spline on for Tables 3 and 4. Currently fitting{' '}
49 <b>{F_LABEL[f]}</b> on <b>{nodes}</b> nodes.
50 </p>
52 <div className="conv-controls">
53 <div className="field">
54 <span className="field-label">blend degrees d</span>
55 <div className="chips">
56 {D_CHOICES.map((d) => (
57 <button
58 key={d}
59 className={`chip ${ds.includes(d) ? 'on' : ''}`}
60 onClick={() => toggleD(d)}
61 disabled={running}
62 >
63 {d}
64 </button>
65 ))}
66 </div>
67 </div>
68 <div className="field">
69 <span className="field-label">largest n</span>
70 <div className="chips">
71 {N_CHOICES.map((n) => (
72 <button
73 key={n}
74 className={`chip ${maxN === n ? 'on' : ''}`}
75 onClick={() => onChange({ maxN: n })}
76 disabled={running}
77 >
78 {n}
79 </button>
80 ))}
81 </div>
82 </div>
83 <div className="field">
84 <span className="field-label">compare with</span>
85 <div className="chips">
86 <button
87 className={`chip ${showSpline ? 'on' : ''}`}
88 onClick={() => onChange({ showSpline: !showSpline })}
89 disabled={running}
90 >
91 cubic spline
92 </button>
93 <button
94 className={`chip ${showPoly ? 'on' : ''}`}
95 onClick={() => onChange({ showPoly: !showPoly })}
96 disabled={running}
97 >
98 polynomial
99 </button>
100 </div>
101 </div>
102 <button className="primary" onClick={onRun} disabled={running}>
103 {running ? 'Running…' : out == null ? 'Run study ▶' : 'Re-run ▶'}
104 </button>
105 </div>
107 {!out ? (
108 <p className="panel-note muted">
109 The study refits the interpolant at every n and every d, so it is the one thing on this page that
110 does not run on its own. Press Run.
111 </p>
112 ) : (
113 <ConvergenceChart out={out} showSpline={showSpline} showPoly={showPoly} stale={stale} />
114 )}
115 </div>
116 )
117}
119function ConvergenceChart({
120 out,
121 showSpline,
122 showPoly,
123 stale,
124}: {
125 out: ConvergeOut
126 showSpline: boolean
127 showPoly: boolean
128 stale: boolean
129}) {
130 const all: Num[] = [
131 ...out.E.flat(),
132 ...(showSpline ? (out.splineErr ?? []) : []),
133 ...(showPoly ? (out.polyErr ?? []) : []),
134 ]
135 const finite = all.filter((v): v is number => v != null && isFinite(v) && v > 0)
136 const lo = Math.min(...finite)
137 const hi = Math.max(...finite)
138 const yd: [number, number] = [Math.pow(10, Math.floor(Math.log10(lo)) - 0.3), Math.pow(10, Math.ceil(Math.log10(hi)) + 0.3)]
139 const xd: [number, number] = [out.ns[0] * 0.85, out.ns[out.ns.length - 1] * 1.35]
141 const legend: LegendItem[] = out.ds.map((d, i) => ({
142 label: `d = ${d}`,
143 color: dColor(i, out.ds.length),
144 }))
145 if (showPoly && out.polyErr) legend.push({ label: 'polynomial (d = n)', color: series.poly })
146 if (showSpline && out.splineErr) legend.push({ label: 'cubic spline', color: series.spline })
148 const dots = (f: Frame, vals: Num[], color: string) =>
149 vals.map((v, j) =>
150 v == null || !isFinite(v) || v <= 0 ? null : (
151 <circle key={j} cx={f.sx(out.ns[j])} cy={f.sy(v)} r={3.5} fill={color} stroke="#151a21" strokeWidth={1.5} />
152 ),
153 )
155 /** the label goes at the right end of the curve, on its last finite point */
156 const endLabel = (f: Frame, vals: Num[], color: string, text: string) => {
157 for (let j = vals.length - 1; j >= 0; j--) {
158 const v = vals[j]
159 if (v != null && isFinite(v) && v > 0) {
160 return (
161 <text x={f.sx(out.ns[j]) + 8} y={f.sy(v) + 4} fill={color} fontSize={11} fontWeight={600}>
162 {text}
163 </text>
164 )
165 }
166 }
167 return null
168 }
170 return (
171 <>
172 {stale && <div className="stale-note">Showing the previous study — the settings have changed since.</div>}
173 <Legend items={legend} />
174 <Plot
175 height={360}
176 xDomain={xd}
177 yDomain={yd}
178 xLog
179 yLog
180 xLabel="n"
181 yLabel="max |r - f|"
182 margin={{ l: 62, r: 58 }}
183 xTicks={out.ns}
184 formatX={(v) => String(Math.round(v))}
185 >
186 {(f) => (
187 <>
188 {showPoly && out.polyErr && (
189 <>
190 <path d={linePath(out.ns, out.polyErr, f)} fill="none" stroke={series.poly} strokeWidth={2} />
191 {dots(f, out.polyErr, series.poly)}
192 {endLabel(f, out.polyErr, series.poly, 'poly')}
193 </>
194 )}
195 {showSpline && out.splineErr && (
196 <>
197 <path d={linePath(out.ns, out.splineErr, f)} fill="none" stroke={series.spline} strokeWidth={2} />
198 {dots(f, out.splineErr, series.spline)}
199 {endLabel(f, out.splineErr, series.spline, 'spline')}
200 </>
201 )}
202 {out.E.map((row, i) => {
203 const c = dColor(i, out.ds.length)
204 return (
205 <g key={i}>
206 <path d={linePath(out.ns, row, f)} fill="none" stroke={c} strokeWidth={2.5} />
207 {dots(f, row, c)}
208 {endLabel(f, row, c, `d = ${out.ds[i]}`)}
209 </g>
210 )
211 })}
212 </>
213 )}
214 </Plot>
216 <h4 className="sub">The same numbers</h4>
217 <div className="table-wrap">
218 <table className="conv-table">
219 <thead>
220 <tr>
221 <th>n</th>
222 {out.ds.map((d) => (
223 <th key={d} colSpan={2}>
224 d = {d}
225 </th>
226 ))}
227 {showSpline && out.splineErr && <th colSpan={2}>cubic spline</th>}
228 {showPoly && out.polyErr && <th>polynomial</th>}
229 </tr>
230 <tr className="sub-head">
231 <th />
232 {out.ds.map((d) => [
233 <th key={`e${d}`}>error</th>,
234 <th key={`o${d}`}>order</th>,
235 ])}
236 {showSpline && out.splineErr && [<th key="se">error</th>, <th key="so">order</th>]}
237 {showPoly && out.polyErr && <th>error</th>}
238 </tr>
239 </thead>
240 <tbody>
241 {out.ns.map((n, j) => (
242 <tr key={n}>
243 <td className="n-cell">{n}</td>
244 {out.ds.map((d, i) => [
245 <td key={`e${d}`}>{fmt(out.E[i][j])}</td>,
246 <td key={`o${d}`} className="ord">
247 {fmtOrd(out.orders[i][j])}
248 </td>,
249 ])}
250 {showSpline && out.splineErr && [
251 <td key="se">{fmt(out.splineErr[j])}</td>,
252 <td key="so" className="ord">
253 {fmtOrd(out.splineOrders?.[j] ?? null)}
254 </td>,
255 ]}
256 {showPoly && out.polyErr && <td>{fmt(out.polyErr[j])}</td>}
257 </tr>
258 ))}
259 </tbody>
260 </table>
261 </div>
262 <p className="panel-note" style={{ color: ink.muted }}>
263 The order column is log(e<sub>prev</sub> / e) / log(n / n<sub>prev</sub>), so d + 1 is what Theorem 2
264 predicts for d ≥ 1. Where a row of errors stops falling, it has reached the point at which the
265 weights themselves, which grow like h<sup>−d</sup>, cost more accuracy than the higher order
266 buys.
267 </p>
268 </>
269 )
270}