/ concept-collection / barycentric-rational
Sign in
concept-collection / barycentric-rational
barycentric-rational / src / panels / BlendingPanel.tsx
328 lines · 10.9 KBBlameHistoryRaw
1import { useMemo, useState } from 'react'
2import Plot from '../plot/Plot.tsx'
3import Legend from '../plot/Legend.tsx'
4import More from './More.tsx'
5import { diverging, ink, series } from '../plot/palette.ts'
6import { extent, linePath, padDomain, type Frame } from '../plot/scales.ts'
7import type { ExploreOut, Num } from '../engine/types.ts'
9interface Props {
10 out: ExploreOut
13/** The samples of t lying in [lo, hi], as a slice of both arrays. */
14function slice(t: number[], v: readonly Num[], lo: number, hi: number): [number[], Num[]] {
15 const ts: number[] = []
16 const vs: Num[] = []
17 for (let i = 0; i < t.length; i++) {
18 if (t[i] >= lo && t[i] <= hi) {
19 ts.push(t[i])
20 vs.push(v[i])
21 }
22 }
23 return [ts, vs]
26export default function BlendingPanel({ out }: Props) {
27 const [pinned, setPinned] = useState<number | null>(null)
28 const [hoverX, setHoverX] = useState<number | null>(null)
30 const P = out.P
31 const L = out.L
32 const wlo = out.wlo
33 const whi = out.whi
34 const m = P?.length ?? 0
36 // which local polynomial the reader is following: the pinned one, or the one
37 // whose window is nearest the pointer
38 const hovered = useMemo(() => {
39 if (!wlo || !whi || hoverX == null) return null
40 let best = 0
41 let bestD = Infinity
42 for (let i = 0; i < wlo.length; i++) {
43 const dd = Math.abs((wlo[i] + whi[i]) / 2 - hoverX)
44 if (dd < bestD) {
45 bestD = dd
46 best = i
47 }
48 }
49 return best
50 }, [wlo, whi, hoverX])
51 const sel = pinned ?? hovered ?? Math.floor(m / 2)
53 const xd: [number, number] = [out.t[0], out.t[out.t.length - 1]]
54 const yd = padDomain(extent(out.ft, out.r), 0.12)
55 const ld = useMemo(() => {
56 if (!L) return [0, 1] as [number, number]
57 const e = extent(...L)
58 return [Math.max(-1.2, Math.min(-0.15, e[0] * 1.1)), Math.min(1.6, Math.max(1.05, e[1] * 1.05))] as [
59 number,
60 number,
61 ]
62 }, [L])
64 if (!out.hasBlend || !P || !L || !wlo || !whi) {
65 return (
66 <div className="panel">
67 <p className="panel-lede">
68 This script does not define <code>local_blend</code>, so the construction cannot be drawn here.
69 That is allowed: a script only has to supply <code>bary_weights</code> and <code>bary_eval</code>,
70 and the other three tabs still work.
71 </p>
72 {out.blendError && <pre className="error-box">{out.blendError}</pre>}
73 <WeightsSection out={out} />
74 </div>
75 )
76 }
78 const shade = (f: Frame) => (
79 <rect
80 x={f.sx(wlo[sel])}
81 width={Math.max(1, f.sx(whi[sel]) - f.sx(wlo[sel]))}
82 y={0}
83 height={f.ih}
84 fill={ink.familyHi}
85 opacity={0.1}
86 />
87 )
89 return (
90 <div className="panel">
91 <p className="panel-lede">
92 The interpolant is built from {m} simple pieces: a polynomial of degree {out.d} fitted to each run of{' '}
93 {out.d + 1} neighbouring points, all blended into one smooth curve. Hover the plot to follow a single
94 piece; click to pin it.
95 </p>
96 <More>
97 <p>
98 This is equation (4), r = &Sigma;<sub>i</sub> &lambda;<sub>i</sub> p<sub>i</sub> / &Sigma;
99 <sub>i</sub> &lambda;<sub>i</sub>: each p<sub>i</sub> interpolates the d+1 points x<sub>i</sub>,
100 &hellip;, x<sub>i+d</sub>, and the blending functions &lambda;<sub>i</sub> of equation (5) carry the
101 alternating signs that make the poles cancel.
102 </p>
103 </More>
105 <div className="row-controls">
106 <label className="slider-label">
107 <span>
108 window i = <b>{sel}</b> &nbsp;[x<sub>{sel}</sub>, x<sub>{sel + out.d}</sub>] = [
109 {wlo[sel].toFixed(2)}, {whi[sel].toFixed(2)}]
110 </span>
111 <input
112 type="range"
113 min={0}
114 max={m - 1}
115 value={sel}
116 onChange={(e) => setPinned(Number(e.target.value))}
117 />
118 </label>
119 {pinned != null && (
120 <button className="ghost" onClick={() => setPinned(null)}>
121 unpin
122 </button>
123 )}
124 </div>
126 <Legend
127 items={[
128 { label: 'f(x)', color: ink.reference, dash: '5 4' },
129 { label: 'rational r(x)', color: series.r },
130 { label: `the ${m} local polynomials p_i`, color: ink.family },
131 { label: `p_${sel}, on its own window`, color: ink.familyHi },
132 ]}
133 />
135 <Plot
136 height={300}
137 xDomain={xd}
138 yDomain={yd}
139 xLabel="x"
140 yLabel="f, r, local p_i"
141 onHoverX={setHoverX}
142 hoverX={hoverX}
143 under={shade}
144 >
145 {(f) => (
146 <>
147 {P.map((row, i) => {
148 // each p_i is only drawn a little beyond the d+1 points it
149 // interpolates: a degree-d polynomial extrapolated across the
150 // whole interval is a wall of noise
151 const pad = Math.max((whi[i] - wlo[i]) * 0.35, (xd[1] - xd[0]) / (out.n * 2))
152 const [ts, vs] = slice(out.t, row, wlo[i] - pad, whi[i] + pad)
153 return i === sel ? null : (
154 <path key={i} d={linePath(ts, vs, f)} fill="none" stroke={ink.family} strokeWidth={1.2} />
155 )
156 })}
157 <path d={linePath(out.t, out.ft, f)} fill="none" stroke={ink.reference} strokeWidth={2} strokeDasharray="5 4" />
158 <path d={linePath(out.t, out.r, f)} fill="none" stroke={series.r} strokeWidth={2.5} />
159 {(() => {
160 const pad = (whi[sel] - wlo[sel]) * 0.9 + (xd[1] - xd[0]) / (out.n * 2)
161 const [ts, vs] = slice(out.t, P[sel], wlo[sel] - pad, whi[sel] + pad)
162 return <path d={linePath(ts, vs, f)} fill="none" stroke={ink.familyHi} strokeWidth={2.5} />
163 })()}
164 {out.x.map((xv, i) => {
165 const inWin = xv >= wlo[sel] - 1e-12 && xv <= whi[sel] + 1e-12
166 return (
167 <circle
168 key={i}
169 cx={f.sx(xv)}
170 cy={f.sy(out.y[i])}
171 r={inWin ? 4.2 : 3}
172 fill={inWin ? ink.familyHi : ink.node}
173 stroke="#151a21"
174 strokeWidth={1.5}
175 />
176 )
177 })}
178 </>
179 )}
180 </Plot>
182 <h4 className="sub">Blending functions</h4>
183 <p className="panel-note">
184 How much each piece counts at each x. The blending functions sum to 1 everywhere, and each is largest
185 over its own window.
186 </p>
187 <More>
188 <p>
189 Each normalised &lambda;<sub>i</sub> decays away from its window, but with a tail that oscillates in
190 sign and never quite reaches zero: these functions have no local support, which the paper names as
191 the price of the construction. What they do have is that their common denominator never vanishes, so
192 they are infinitely smooth.
193 </p>
194 </More>
195 <Plot
196 height={220}
197 xDomain={xd}
198 yDomain={ld}
199 xLabel="x"
200 yLabel="normalised lambda_i"
201 onHoverX={setHoverX}
202 hoverX={hoverX}
203 under={(f) => (
204 <>
205 {shade(f)}
206 <line x1={0} x2={f.iw} y1={f.sy(0)} y2={f.sy(0)} stroke={ink.axis} strokeWidth={1} />
207 <line
208 x1={0}
209 x2={f.iw}
210 y1={f.sy(1)}
211 y2={f.sy(1)}
212 stroke={ink.axis}
213 strokeWidth={1}
214 strokeDasharray="2 4"
215 />
216 </>
217 )}
218 >
219 {(f) => (
220 <>
221 {L.map((row, i) =>
222 i === sel ? null : (
223 <path key={i} d={linePath(out.t, row, f)} fill="none" stroke={ink.family} strokeWidth={1.2} />
224 ),
225 )}
226 <path d={linePath(out.t, L[sel], f)} fill="none" stroke={ink.familyHi} strokeWidth={2.5} />
227 {out.x.map((xv, i) => (
228 <line
229 key={i}
230 x1={f.sx(xv)}
231 x2={f.sx(xv)}
232 y1={f.ih}
233 y2={f.ih - 5}
234 stroke={ink.node}
235 strokeWidth={1.5}
236 />
237 ))}
238 </>
239 )}
240 </Plot>
242 <WeightsSection out={out} />
243 </div>
244 )
247function WeightsSection({ out }: { out: ExploreOut }) {
248 const wmax = Math.max(...out.w.map(Math.abs), Number.MIN_VALUE)
249 const norm = out.w.map((v) => v / wmax)
250 const spread = Math.max(...out.wscaled) / Math.min(...out.wscaled.filter((v) => v > 0))
252 return (
253 <>
254 <h4 className="sub">Barycentric weights</h4>
255 <p className="panel-note">
256 The whole construction collapses to a single weight per node. Alternating signs are what pole-freedom
257 requires, and these weights {out.wAlternates ? 'alternate' : 'do not alternate'}.
258 </p>
259 <More>
260 <p>
261 This is the barycentric form of equation (1) with the weights of equation (18), which is how the
262 interpolant is actually evaluated. Schneider and Werner proved that a barycentric rational
263 interpolant with no poles in the interval must have weights that alternate in sign, so the sign
264 pattern here is not a coincidence.
265 </p>
266 </More>
267 <Legend
268 items={[
269 { label: 'w_k > 0', color: diverging.pos },
270 { label: 'w_k < 0', color: diverging.neg },
271 ]}
272 />
273 <Plot
274 height={170}
275 xDomain={[out.x[0], out.x[out.x.length - 1]]}
276 yDomain={[-1.15, 1.15]}
277 xLabel="x_k"
278 yLabel="w_k / max |w|"
279 margin={{ l: 62 }}
280 under={(f) => <line x1={0} x2={f.iw} y1={f.sy(0)} y2={f.sy(0)} stroke={ink.axis} strokeWidth={1} />}
281 >
282 {(f) =>
283 norm.map((v, k) => (
284 <g key={k}>
285 <line
286 x1={f.sx(out.x[k])}
287 x2={f.sx(out.x[k])}
288 y1={f.sy(0)}
289 y2={f.sy(v)}
290 stroke={v >= 0 ? diverging.pos : diverging.neg}
291 strokeWidth={2}
292 />
293 <circle
294 cx={f.sx(out.x[k])}
295 cy={f.sy(v)}
296 r={3}
297 fill={v >= 0 ? diverging.pos : diverging.neg}
298 stroke="#151a21"
299 strokeWidth={1}
300 />
301 </g>
302 ))
303 }
304 </Plot>
306 {out.wIsInteger ? (
307 <div className="weights-int">
308 <div className="weights-int-head">
309 &delta;<sub>k</sub> = |w<sub>k</sub>| / min |w|, which Section 4 predicts are integers on a uniform
310 mesh:
311 </div>
312 <div className="weights-int-values">
313 {out.wscaled.map((v, k) => (
314 <span key={k}>{Math.round(v)}</span>
315 ))}
316 </div>
317 </div>
318 ) : (
319 <div className="weights-int">
320 <div className="weights-int-head">
321 The weights are not integer multiples of the smallest one, so this is not a uniform mesh. Their
322 magnitudes span a factor of {spread < 1e5 ? spread.toFixed(0) : spread.toExponential(1)}.
323 </div>
324 </div>
325 )}
326 </>
327 )
moveopenescclose