/ concept-collection / barycentric-rational
Sign in
concept-collection / barycentric-rational
barycentric-rational / src / App.tsx
314 lines · 10.5 KBCodeBlameHistory
5392320Interactive illustration of Floater-Hormann barycentric rational interpolationJeremy Magland 1import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2import ScriptEditor from './editor/ScriptEditor.tsx'
3import Controls, { type Settings } from './panels/Controls.tsx'
4import InterpolantPanel from './panels/InterpolantPanel.tsx'
5import BlendingPanel from './panels/BlendingPanel.tsx'
6import PolesPanel from './panels/PolesPanel.tsx'
7import ConvergencePanel from './panels/ConvergencePanel.tsx'
8import { Engine } from './engine/runner.ts'
9import type { ConvergeOut, ConvergeParams, ExploreOut, ExploreParams, Want } from './engine/types.ts'
10import { DEFAULT_METHOD, METHODS, findMethod } from './methods/index.ts'
12type TabId = 'interpolant' | 'blending' | 'poles' | 'convergence'
14const TABS: { id: TabId; label: string }[] = [
15 { id: 'interpolant', label: 'Interpolant' },
16 { id: 'blending', label: 'Blending & weights' },
17 { id: 'poles', label: 'Poles' },
18 { id: 'convergence', label: 'Convergence' },
21const NGRID = 1200
22const NGRID_WIDE = 1400
23const ROOTS_MAX_N = 40
25function nSeries(maxN: number): number[] {
26 const all = [10, 20, 40, 80, 160, 320, 640]
27 return all.filter((n) => n <= maxN)
30export default function App() {
31 const [methodId, setMethodId] = useState(DEFAULT_METHOD.id)
32 const [script, setScript] = useState(() => DEFAULT_METHOD.source)
33 const [dirty, setDirty] = useState(false)
34 const [tab, setTab] = useState<TabId>('interpolant')
35 const [settings, setSettings] = useState<Settings>({
36 f: 'runge',
37 fexpr: 'exp(-x.^2) .* cos(3*x)',
38 a: -5,
39 b: 5,
40 n: 20,
41 d: 3,
42 nodes: 'uniform',
43 seed: 1,
44 })
46 const [showPoly, setShowPoly] = useState(false)
47 const [showSpline, setShowSpline] = useState(false)
48 const [showClassical, setShowClassical] = useState(true)
50 const [explore, setExplore] = useState<ExploreOut | null>(null)
51 const [error, setError] = useState<string | null>(null)
52 const [output, setOutput] = useState('')
53 const [busy, setBusy] = useState(true)
54 const [ms, setMs] = useState<number | null>(null)
56 // the convergence study runs only when asked
57 const [convDs, setConvDs] = useState([0, 1, 2, 3, 4])
58 const [convMaxN, setConvMaxN] = useState(320)
59 const [convSpline, setConvSpline] = useState(true)
60 const [convPoly, setConvPoly] = useState(false)
61 const [conv, setConv] = useState<ConvergeOut | null>(null)
62 const [convKey, setConvKey] = useState<string | null>(null)
63 const [convRunning, setConvRunning] = useState(false)
65 const engineRef = useRef<Engine | null>(null)
66 if (!engineRef.current) engineRef.current = new Engine()
67 useEffect(() => () => engineRef.current?.dispose(), [])
69 const want: Want = useMemo(
70 () => ({
71 poly: tab === 'interpolant' && showPoly,
72 spline: tab === 'interpolant' && showSpline,
73 blend: tab === 'blending',
74 poles: tab === 'poles',
75 classical: tab === 'poles' && showClassical,
76 }),
77 [tab, showPoly, showSpline, showClassical],
78 )
80 const exploreParams: ExploreParams = useMemo(
81 () => ({
82 mode: 'explore',
83 f: settings.f,
84 fexpr: settings.fexpr,
85 a: settings.a,
86 b: settings.b,
87 n: settings.n,
88 d: Math.min(settings.d, settings.n),
89 nodes: settings.nodes,
90 seed: settings.seed,
91 ngrid: NGRID,
92 ngridwide: NGRID_WIDE,
93 rootsMaxN: ROOTS_MAX_N,
94 want,
95 }),
96 [settings, want],
97 )
99 // Re-run whenever the script or any parameter changes. The debounce keeps a
100 // dragged slider from queueing a run per pixel; the engine serialises what
101 // does get through, so the last one always wins.
102 const [runToken, setRunToken] = useState(0)
103 const paramsKey = JSON.stringify(exploreParams)
104 useEffect(() => {
105 if (tab === 'convergence') return
106 let cancelled = false
107 setBusy(true)
108 const timer = setTimeout(async () => {
109 const res = await engineRef.current!.run<ExploreOut>(script, exploreParams)
110 if (cancelled) return
111 setBusy(false)
112 setMs(res.ms)
113 setOutput(res.output)
114 if (res.ok) {
115 setExplore(res.data)
116 setError(null)
117 } else {
118 setError(res.error)
119 }
120 }, 110)
121 return () => {
122 cancelled = true
123 clearTimeout(timer)
124 }
125 // paramsKey stands in for exploreParams, which is rebuilt every render
126 // eslint-disable-next-line react-hooks/exhaustive-deps
127 }, [script, paramsKey, tab, runToken])
129 const convParams: ConvergeParams = useMemo(
130 () => ({
131 mode: 'converge',
132 f: settings.f,
133 fexpr: settings.fexpr,
134 a: settings.a,
135 b: settings.b,
136 nodes: settings.nodes,
137 seed: settings.seed,
138 ngrid: 4001,
139 ns: nSeries(convMaxN),
140 ds: convDs,
141 want: { poly: convPoly, spline: convSpline, blend: false, poles: false, classical: false },
142 }),
143 [settings, convMaxN, convDs, convPoly, convSpline],
144 )
145 const convParamsKey = JSON.stringify(convParams) + script
147 const runConvergence = useCallback(async () => {
148 setConvRunning(true)
149 setError(null)
150 const key = convParamsKey
151 const res = await engineRef.current!.run<ConvergeOut>(script, convParams)
152 setConvRunning(false)
153 setMs(res.ms)
154 setOutput(res.output)
155 if (res.ok) {
156 setConv(res.data)
157 setConvKey(key)
158 setError(null)
159 } else {
160 setError(res.error)
161 }
162 }, [convParams, convParamsKey, script])
164 const pickMethod = (id: string) => {
165 const m = findMethod(id)
166 if (!m) return
167 setMethodId(id)
168 setScript(m.source)
169 setDirty(false)
170 }
172 const patch = (p: Partial<Settings>) => setSettings((s) => ({ ...s, ...p }))
173 const method = findMethod(methodId)
175 return (
176 <div className="app">
177 <header className="header">
178 <div className="title">
179 <h1>Barycentric rational interpolation</h1>
180 <p>
181 Floater &amp; Hormann,{' '}
182 <a href="https://doi.org/10.1007/s00211-007-0093-y" target="_blank" rel="noreferrer">
183 Numer. Math. <b>107</b> (2007) 315&ndash;331
184 </a>
185 . The method is the script on the left; it runs in your browser through{' '}
186 <a href="https://numbl.org" target="_blank" rel="noreferrer">
187 numbl
188 </a>
189 .
190 </p>
191 </div>
192 <a
193 className="repo-link"
194 href="https://github.com/concept-collection/barycentric-rational"
195 target="_blank"
196 rel="noreferrer"
197 >
198 source
199 </a>
200 </header>
202 <div className="body">
203 <section className="left">
204 <div className="script-head">
205 <label className="field">
206 <span className="field-label">method</span>
207 <select value={dirty ? '' : methodId} onChange={(e) => pickMethod(e.target.value)}>
208 {dirty && <option value="">(edited)</option>}
209 {METHODS.map((m) => (
210 <option key={m.id} value={m.id}>
211 {m.name}
212 </option>
213 ))}
214 </select>
215 </label>
216 <button
217 className="primary"
218 onClick={() => setRunToken((v) => v + 1)}
219 disabled={busy || convRunning}
220 title="⌘/Ctrl+Enter"
221 >
222 {busy || convRunning ? 'Running…' : 'Run ▶'}
223 </button>
224 </div>
225 {method && !dirty && <p className="method-blurb">{method.blurb}</p>}
226 {dirty && <p className="method-blurb edited">Edited. Pick a method above to start over.</p>}
228 <ScriptEditor
229 value={script}
230 onChange={(s) => {
231 setScript(s)
232 setDirty(s !== findMethod(methodId)?.source)
233 }}
234 onRun={() => setRunToken((v) => v + 1)}
235 />
237 <div className="contract">
238 <div className="contract-head">What the app calls</div>
239 <code>w = bary_weights(x, d)</code>
240 <code>r = bary_eval(x, y, w, t)</code>
241 <code className="opt">[P, L] = local_blend(x, y, d, t)</code>
242 <span className="contract-note">the third is optional; without it the second tab is empty</span>
243 </div>
245 {error && (
246 <div className="error-box">
247 <div className="error-head">The script failed</div>
248 <pre>{error}</pre>
249 </div>
250 )}
251 {output.trim() && !error && (
252 <details className="console">
253 <summary>console output</summary>
254 <pre>{output}</pre>
255 </details>
256 )}
257 </section>
259 <section className="right">
260 <nav className="tabs">
261 {TABS.map((t) => (
262 <button key={t.id} className={`tab ${tab === t.id ? 'on' : ''}`} onClick={() => setTab(t.id)}>
263 {t.label}
264 </button>
265 ))}
266 <span className="run-status">
267 {busy || convRunning ? 'running…' : ms != null ? `${Math.round(ms)} ms` : ''}
268 </span>
269 </nav>
271 <Controls value={settings} onChange={patch} showN={tab !== 'convergence'} busy={false} />
273 <div className="panel-scroll">
274 {tab === 'convergence' ? (
275 <ConvergencePanel
276 out={conv}
277 running={convRunning}
278 stale={conv != null && convKey !== convParamsKey}
279 f={settings.f}
280 nodes={settings.nodes}
281 ds={convDs}
282 maxN={convMaxN}
283 showSpline={convSpline}
284 showPoly={convPoly}
285 onChange={(p) => {
286 if (p.ds) setConvDs(p.ds)
287 if (p.maxN) setConvMaxN(p.maxN)
288 if (p.showSpline !== undefined) setConvSpline(p.showSpline)
289 if (p.showPoly !== undefined) setConvPoly(p.showPoly)
290 }}
291 onRun={runConvergence}
292 />
293 ) : explore == null ? (
294 <div className="panel">
295 <p className="panel-note muted">{error ? 'Fix the script to see the plots.' : 'Starting numbl…'}</p>
296 </div>
297 ) : tab === 'interpolant' ? (
298 <InterpolantPanel
299 out={explore}
300 showPoly={showPoly}
301 showSpline={showSpline}
302 onToggle={(which, on) => (which === 'poly' ? setShowPoly(on) : setShowSpline(on))}
303 />
304 ) : tab === 'blending' ? (
305 <BlendingPanel out={explore} />
306 ) : (
307 <PolesPanel out={explore} showClassical={showClassical} onToggleClassical={setShowClassical} />
308 )}
309 </div>
310 </section>
311 </div>
312 </div>
313 )
moveopenescclose