1import { useState } from 'react'
2import Plot from '../plot/Plot.tsx'
3import Legend from '../plot/Legend.tsx'
4import { ink, series, status } from '../plot/palette.ts'
5import { extent, linePath, padDomain, type Frame } from '../plot/scales.ts'
6import type { ExploreOut } from '../engine/types.ts'
8interface Props {
9 out: ExploreOut
10 showClassical: boolean
11 onToggleClassical: (on: boolean) => void
12}
14export default function PolesPanel({ out, showClassical, onToggleClassical }: Props) {
15 const [hoverRoot, setHoverRoot] = useState<number | null>(null)
16 const p = out.poles
17 if (!p) return <div className="panel">Loading.</div>
19 const xd: [number, number] = [p.t[0], p.t[p.t.length - 1]]
20 const clean = p.realPoles.length === 0
21 // a root counts as sitting on the real axis if its imaginary part is a
22 // rounding error next to the interval it lives on
23 const axisTol = (out.x[out.x.length - 1] - out.x[0]) * 1e-7
24 const nOnAxis = p.rootsIm.filter((v) => Math.abs(v) <= axisTol).length
26 const rootRange = Math.max(
27 ...p.rootsRe.map((v) => Math.abs(v - (out.x[0] + out.x[out.x.length - 1]) / 2)),
28 ...p.rootsIm.map(Math.abs),
29 (out.x[out.x.length - 1] - out.x[0]) / 2,
30 )
31 const cx0 = (out.x[0] + out.x[out.x.length - 1]) / 2
32 const cd: [number, number] = [cx0 - rootRange * 1.12, cx0 + rootRange * 1.12]
33 const cyd: [number, number] = [-rootRange * 1.12, rootRange * 1.12]
35 return (
36 <div className="panel">
37 <div className={`verdict ${clean ? 'good' : 'bad'}`}>
38 <span className="verdict-mark" aria-hidden="true">
39 {clean ? '✓' : '✕'}
40 </span>
41 <span>
42 {clean ? (
43 <>
44 <b>No real poles.</b> The denominator keeps one sign across the whole real line, which is
45 Theorem 1, and every one of its {p.rootsShown ? p.rootsRe.length : 'roots'} roots sits off the
46 real axis.
47 </>
48 ) : (
49 <>
50 <b>
51 {p.realPoles.length} real pole{p.realPoles.length === 1 ? '' : 's'}.
52 </b>{' '}
53 The denominator changes sign, so r blows up inside the interval. These weights are not the ones
54 of equation (18).
55 </>
56 )}
57 </span>
58 </div>
60 <p className="panel-lede">
61 Writing r as a quotient of polynomials (equation 7) puts everything on the denominator s of equation
62 (10). Its zeros are exactly the poles of r, so Theorem 1 amounts to the claim that s never crosses
63 zero.
64 </p>
66 <h4 className="sub">The denominator on the real line</h4>
67 <p className="panel-note">
68 s(x) spans many orders of magnitude, so what is drawn is the signed n-th root of it. That leaves every
69 sign and every zero exactly where it was, and brings the rest into a range that fits on a page. The
70 range shown runs well past both ends of the interpolation interval, since Theorem 1 is a statement
71 about all of <b>R</b>, not just [a, b].
72 </p>
73 <Plot
74 height={200}
75 xDomain={xd}
76 yDomain={[-1.15, 1.15]}
77 xLabel="x"
78 yLabel="signed n-th root of s"
79 margin={{ l: 74 }}
80 under={(f) => (
81 <>
82 <rect
83 x={f.sx(out.x[0])}
84 width={f.sx(out.x[out.x.length - 1]) - f.sx(out.x[0])}
85 y={0}
86 height={f.ih}
87 fill={ink.node}
88 opacity={0.05}
89 />
90 <line x1={0} x2={f.iw} y1={f.sy(0)} y2={f.sy(0)} stroke={ink.axis} strokeWidth={1.5} />
91 </>
92 )}
93 >
94 {(f) => (
95 <>
96 <path d={linePath(p.t, p.u, f)} fill="none" stroke={series.r} strokeWidth={2.5} />
97 {p.realPoles.map((xv, i) => (
98 <g key={i}>
99 <line
100 x1={f.sx(xv)}
101 x2={f.sx(xv)}
102 y1={0}
103 y2={f.ih}
104 stroke={status.critical}
105 strokeWidth={1.5}
106 strokeDasharray="4 3"
107 />
108 <path
109 d={`M${f.sx(xv) - 4} ${f.sy(0) - 4} L${f.sx(xv) + 4} ${f.sy(0) + 4} M${f.sx(xv) + 4} ${
110 f.sy(0) - 4
111 } L${f.sx(xv) - 4} ${f.sy(0) + 4}`}
112 stroke={status.critical}
113 strokeWidth={2}
114 />
115 </g>
116 ))}
117 {out.x.map((xv, i) => (
118 <line
119 key={i}
120 x1={f.sx(xv)}
121 x2={f.sx(xv)}
122 y1={f.ih}
123 y2={f.ih - 5}
124 stroke={ink.node}
125 strokeWidth={1.5}
126 />
127 ))}
128 </>
129 )}
130 </Plot>
132 <h4 className="sub">Where the roots actually are</h4>
133 {p.rootsShown ? (
134 <>
135 <p className="panel-note">
136 The {p.rootsRe.length} roots of s in the complex plane. Theorem 1 says none of them are real, so
137 none of them touch the horizontal axis. The grey band is the interpolation interval; ticks on the
138 axis are the nodes.
139 {nOnAxis > 0 && ' Roots that have landed on the axis are marked with a cross.'}
140 </p>
141 <Legend
142 items={[
143 { label: 'root of s', color: series.r, shape: 'dot' },
144 ...(nOnAxis > 0 ? [{ label: 'root on the real axis: a pole', color: status.critical, shape: 'cross' as const }] : []),
145 ]}
146 />
147 <Plot
148 height={300}
149 xDomain={cd}
150 yDomain={cyd}
151 xLabel="Re"
152 yLabel="Im"
153 under={(f: Frame) => (
154 <>
155 <rect
156 x={f.sx(out.x[0])}
157 width={f.sx(out.x[out.x.length - 1]) - f.sx(out.x[0])}
158 y={0}
159 height={f.ih}
160 fill={ink.node}
161 opacity={0.05}
162 />
163 <line x1={0} x2={f.iw} y1={f.sy(0)} y2={f.sy(0)} stroke={ink.axis} strokeWidth={1.5} />
164 <line x1={f.sx(0)} x2={f.sx(0)} y1={0} y2={f.ih} stroke={ink.grid} strokeWidth={1} />
165 </>
166 )}
167 overlay={
168 hoverRoot != null && (
169 <div className="tooltip">
170 {p.rootsRe[hoverRoot].toPrecision(4)}
171 {p.rootsIm[hoverRoot] >= 0 ? ' + ' : ' − '}
172 {Math.abs(p.rootsIm[hoverRoot]).toPrecision(4)} i
173 </div>
174 )
175 }
176 >
177 {(f) => (
178 <>
179 {out.x.map((xv, i) => (
180 <line
181 key={i}
182 x1={f.sx(xv)}
183 x2={f.sx(xv)}
184 y1={f.sy(0) - 4}
185 y2={f.sy(0) + 4}
186 stroke={ink.node}
187 strokeWidth={1.5}
188 />
189 ))}
190 {p.rootsRe.map((re, i) => {
191 const onAxis = Math.abs(p.rootsIm[i]) <= axisTol
192 return onAxis ? (
193 <path
194 key={i}
195 d={`M${f.sx(re) - 5} ${f.sy(p.rootsIm[i]) - 5} L${f.sx(re) + 5} ${
196 f.sy(p.rootsIm[i]) + 5
197 } M${f.sx(re) + 5} ${f.sy(p.rootsIm[i]) - 5} L${f.sx(re) - 5} ${f.sy(p.rootsIm[i]) + 5}`}
198 stroke={status.critical}
199 strokeWidth={2.5}
200 strokeLinecap="round"
201 onPointerEnter={() => setHoverRoot(i)}
202 onPointerLeave={() => setHoverRoot(null)}
203 />
204 ) : (
205 <circle
206 key={i}
207 cx={f.sx(re)}
208 cy={f.sy(p.rootsIm[i])}
209 r={hoverRoot === i ? 7 : 5}
210 fill={series.r}
211 stroke="#151a21"
212 strokeWidth={2}
213 onPointerEnter={() => setHoverRoot(i)}
214 onPointerLeave={() => setHoverRoot(null)}
215 />
216 )
217 })}
218 </>
219 )}
220 </Plot>
221 </>
222 ) : (
223 <p className="panel-note">
224 {out.n > 40
225 ? 'Not drawn above n = 40: recovering roots from the coefficients of a polynomial of that degree is not reliable enough to be worth showing. The sign test above still holds at any n.'
226 : 'The denominator reduces to a constant, so it has no roots at all — which is one way for an interpolant to have no poles. That is what the Lagrange weights of equation (2) do.'}
227 </p>
228 )}
230 <h4 className="sub">The classical alternative</h4>
231 <div className="row-controls">
232 <label className="check">
233 <input
234 type="checkbox"
235 checked={showClassical}
236 onChange={(e) => onToggleClassical(e.target.checked)}
237 />
238 fit p<sub>M</sub> / q<sub>N</sub> with M + N = n
239 </label>
240 </div>
241 <p className="panel-note">
242 The construction the paper's introduction rejects: fit the same data with a quotient of polynomials of
243 degrees M and N summing to n. It is a good approximation where it is finite, and there is no way to
244 stop it putting poles wherever it likes.
245 </p>
246 {showClassical && p.classical ? (
247 <>
248 <Legend
249 items={[
250 { label: 'f(x)', color: ink.reference, dash: '5 4' },
251 { label: 'rational r(x), this page', color: series.r },
252 { label: 'classical p_M / q_N', color: series.classical },
253 ...(p.classicalPoles && p.classicalPoles.length
254 ? [{ label: 'its poles', color: status.critical, shape: 'cross' as const }]
255 : []),
256 ]}
257 />
258 <ClassicalPlot out={out} />
259 </>
260 ) : (
261 <p className="panel-note muted">Turn it on to draw it.</p>
262 )}
263 </div>
264 )
265}
267function ClassicalPlot({ out }: { out: ExploreOut }) {
268 const p = out.poles!
269 const xd: [number, number] = [p.t[0], p.t[p.t.length - 1]]
270 const yd = padDomain(extent(out.ft, out.r), 0.45)
271 const inView = (p.classicalPoles ?? []).filter((v) => v >= xd[0] && v <= xd[1])
272 return (
273 <>
274 <Plot
275 height={260}
276 xDomain={xd}
277 yDomain={yd}
278 xLabel="x"
279 yLabel="f, r, classical"
280 under={(f) => (
281 <rect
282 x={f.sx(out.x[0])}
283 width={f.sx(out.x[out.x.length - 1]) - f.sx(out.x[0])}
284 y={0}
285 height={f.ih}
286 fill={ink.node}
287 opacity={0.05}
288 />
289 )}
290 >
291 {(f) => (
292 <>
293 {inView.map((xv, i) => (
294 <line
295 key={i}
296 x1={f.sx(xv)}
297 x2={f.sx(xv)}
298 y1={0}
299 y2={f.ih}
300 stroke={status.critical}
301 strokeWidth={1.5}
302 strokeDasharray="4 3"
303 />
304 ))}
305 <path d={linePath(out.t, out.ft, f)} fill="none" stroke={ink.reference} strokeWidth={2} strokeDasharray="5 4" />
306 <path d={linePath(p.t, p.classical!, f)} fill="none" stroke={series.classical} strokeWidth={2} />
307 <path d={linePath(out.t, out.r, f)} fill="none" stroke={series.r} strokeWidth={2.5} />
308 {out.x.map((xv, i) => (
309 <circle
310 key={i}
311 cx={f.sx(xv)}
312 cy={f.sy(out.y[i])}
313 r={3}
314 fill={ink.node}
315 stroke="#151a21"
316 strokeWidth={1.5}
317 />
318 ))}
319 </>
320 )}
321 </Plot>
322 <p className="panel-note">
323 {inView.length === 0
324 ? 'On this data it happens to have no poles in the window shown. Change n, or the function, or the nodes, and that is not something you can rely on.'
325 : `${inView.length} real pole${inView.length === 1 ? '' : 's'} in the window, at ${inView
326 .map((v) => v.toFixed(3))
327 .join(', ')}.`}
328 </p>
329 </>
330 )
331}