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