import { useState } from 'react' import Plot from '../plot/Plot.tsx' import Legend from '../plot/Legend.tsx' import { ink, series, status } from '../plot/palette.ts' import { extent, linePath, padDomain, type Frame } from '../plot/scales.ts' import type { ExploreOut } from '../engine/types.ts' interface Props { out: ExploreOut showClassical: boolean onToggleClassical: (on: boolean) => void } export default function PolesPanel({ out, showClassical, onToggleClassical }: Props) { const [hoverRoot, setHoverRoot] = useState(null) const p = out.poles if (!p) return
Loading.
const xd: [number, number] = [p.t[0], p.t[p.t.length - 1]] const clean = p.realPoles.length === 0 // a root counts as sitting on the real axis if its imaginary part is a // rounding error next to the interval it lives on const axisTol = (out.x[out.x.length - 1] - out.x[0]) * 1e-7 const nOnAxis = p.rootsIm.filter((v) => Math.abs(v) <= axisTol).length const rootRange = Math.max( ...p.rootsRe.map((v) => Math.abs(v - (out.x[0] + out.x[out.x.length - 1]) / 2)), ...p.rootsIm.map(Math.abs), (out.x[out.x.length - 1] - out.x[0]) / 2, ) const cx0 = (out.x[0] + out.x[out.x.length - 1]) / 2 const cd: [number, number] = [cx0 - rootRange * 1.12, cx0 + rootRange * 1.12] const cyd: [number, number] = [-rootRange * 1.12, rootRange * 1.12] return (
{clean ? ( <> No real poles. The denominator keeps one sign across the whole real line, which is Theorem 1, and every one of its {p.rootsShown ? p.rootsRe.length : 'roots'} roots sits off the real axis. ) : ( <> {p.realPoles.length} real pole{p.realPoles.length === 1 ? '' : 's'}. {' '} The denominator changes sign, so r blows up inside the interval. These weights are not the ones of equation (18). )}

Writing r as a quotient of polynomials (equation 7) puts everything on the denominator s of equation (10). Its zeros are exactly the poles of r, so Theorem 1 amounts to the claim that s never crosses zero.

The denominator on the real line

s(x) spans many orders of magnitude, so what is drawn is the signed n-th root of it. That leaves every sign and every zero exactly where it was, and brings the rest into a range that fits on a page. The range shown runs well past both ends of the interpolation interval, since Theorem 1 is a statement about all of R, not just [a, b].

( <> )} > {(f) => ( <> {p.realPoles.map((xv, i) => ( ))} {out.x.map((xv, i) => ( ))} )}

Where the roots actually are

{p.rootsShown ? ( <>

The {p.rootsRe.length} roots of s in the complex plane. Theorem 1 says none of them are real, so none of them touch the horizontal axis. The grey band is the interpolation interval; ticks on the axis are the nodes. {nOnAxis > 0 && ' Roots that have landed on the axis are marked with a cross.'}

0 ? [{ label: 'root on the real axis: a pole', color: status.critical, shape: 'cross' as const }] : []), ]} /> ( <> )} overlay={ hoverRoot != null && (
{p.rootsRe[hoverRoot].toPrecision(4)} {p.rootsIm[hoverRoot] >= 0 ? ' + ' : ' − '} {Math.abs(p.rootsIm[hoverRoot]).toPrecision(4)} i
) } > {(f) => ( <> {out.x.map((xv, i) => ( ))} {p.rootsRe.map((re, i) => { const onAxis = Math.abs(p.rootsIm[i]) <= axisTol return onAxis ? ( setHoverRoot(i)} onPointerLeave={() => setHoverRoot(null)} /> ) : ( setHoverRoot(i)} onPointerLeave={() => setHoverRoot(null)} /> ) })} )}
) : (

{out.n > 40 ? '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.' : '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.'}

)}

The classical alternative

The construction the paper's introduction rejects: fit the same data with a quotient of polynomials of degrees M and N summing to n. It is a good approximation where it is finite, and there is no way to stop it putting poles wherever it likes.

{showClassical && p.classical ? ( <> ) : (

Turn it on to draw it.

)}
) } function ClassicalPlot({ out }: { out: ExploreOut }) { const p = out.poles! const xd: [number, number] = [p.t[0], p.t[p.t.length - 1]] const yd = padDomain(extent(out.ft, out.r), 0.45) const inView = (p.classicalPoles ?? []).filter((v) => v >= xd[0] && v <= xd[1]) return ( <> ( )} > {(f) => ( <> {inView.map((xv, i) => ( ))} {out.x.map((xv, i) => ( ))} )}

{inView.length === 0 ? '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.' : `${inView.length} real pole${inView.length === 1 ? '' : 's'} in the window, at ${inView .map((v) => v.toFixed(3)) .join(', ')}.`}

) }