/ concept-collection / barycentric-rational
Sign in
concept-collection / barycentric-rational
barycentric-rational / src / App.tsx
349 lines · 11.7 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')
1de956dSimplify the interface for first-time visitorsJeremy Magland 35 // The editor is the point of the app for a reader of the paper, but a wall of
36 // MATLAB for anyone else, so it starts collapsed and the choice persists.
37 const [editorOpen, setEditorOpen] = useState(() => {
38 try {
39 return localStorage.getItem('br-editor-open') === '1'
40 } catch {
41 return false
42 }
43 })
44 useEffect(() => {
45 try {
46 localStorage.setItem('br-editor-open', editorOpen ? '1' : '0')
47 } catch {
48 /* private mode */
49 }
50 }, [editorOpen])
5392320Interactive illustration of Floater-Hormann barycentric rational interpolationJeremy Magland 51 const [settings, setSettings] = useState<Settings>({
52 f: 'runge',
53 fexpr: 'exp(-x.^2) .* cos(3*x)',
54 a: -5,
55 b: 5,
56 n: 20,
57 d: 3,
58 nodes: 'uniform',
59 seed: 1,
60 })
62 const [showPoly, setShowPoly] = useState(false)
63 const [showSpline, setShowSpline] = useState(false)
64 const [showClassical, setShowClassical] = useState(true)
66 const [explore, setExplore] = useState<ExploreOut | null>(null)
67 const [error, setError] = useState<string | null>(null)
68 const [output, setOutput] = useState('')
69 const [busy, setBusy] = useState(true)
70 const [ms, setMs] = useState<number | null>(null)
72 // the convergence study runs only when asked
73 const [convDs, setConvDs] = useState([0, 1, 2, 3, 4])
74 const [convMaxN, setConvMaxN] = useState(320)
75 const [convSpline, setConvSpline] = useState(true)
76 const [convPoly, setConvPoly] = useState(false)
77 const [conv, setConv] = useState<ConvergeOut | null>(null)
78 const [convKey, setConvKey] = useState<string | null>(null)
79 const [convRunning, setConvRunning] = useState(false)
81 const engineRef = useRef<Engine | null>(null)
82 if (!engineRef.current) engineRef.current = new Engine()
83 useEffect(() => () => engineRef.current?.dispose(), [])
85 const want: Want = useMemo(
86 () => ({
87 poly: tab === 'interpolant' && showPoly,
88 spline: tab === 'interpolant' && showSpline,
89 blend: tab === 'blending',
90 poles: tab === 'poles',
91 classical: tab === 'poles' && showClassical,
92 }),
93 [tab, showPoly, showSpline, showClassical],
94 )
96 const exploreParams: ExploreParams = useMemo(
97 () => ({
98 mode: 'explore',
99 f: settings.f,
100 fexpr: settings.fexpr,
101 a: settings.a,
102 b: settings.b,
103 n: settings.n,
104 d: Math.min(settings.d, settings.n),
105 nodes: settings.nodes,
106 seed: settings.seed,
107 ngrid: NGRID,
108 ngridwide: NGRID_WIDE,
109 rootsMaxN: ROOTS_MAX_N,
110 want,
111 }),
112 [settings, want],
113 )
115 // Re-run whenever the script or any parameter changes. The debounce keeps a
116 // dragged slider from queueing a run per pixel; the engine serialises what
117 // does get through, so the last one always wins.
118 const [runToken, setRunToken] = useState(0)
119 const paramsKey = JSON.stringify(exploreParams)
120 useEffect(() => {
121 if (tab === 'convergence') return
122 let cancelled = false
123 setBusy(true)
124 const timer = setTimeout(async () => {
125 const res = await engineRef.current!.run<ExploreOut>(script, exploreParams)
126 if (cancelled) return
127 setBusy(false)
128 setMs(res.ms)
129 setOutput(res.output)
130 if (res.ok) {
131 setExplore(res.data)
132 setError(null)
133 } else {
134 setError(res.error)
135 }
136 }, 110)
137 return () => {
138 cancelled = true
139 clearTimeout(timer)
140 }
141 // paramsKey stands in for exploreParams, which is rebuilt every render
142 // eslint-disable-next-line react-hooks/exhaustive-deps
143 }, [script, paramsKey, tab, runToken])
145 const convParams: ConvergeParams = useMemo(
146 () => ({
147 mode: 'converge',
148 f: settings.f,
149 fexpr: settings.fexpr,
150 a: settings.a,
151 b: settings.b,
152 nodes: settings.nodes,
153 seed: settings.seed,
154 ngrid: 4001,
155 ns: nSeries(convMaxN),
156 ds: convDs,
157 want: { poly: convPoly, spline: convSpline, blend: false, poles: false, classical: false },
158 }),
159 [settings, convMaxN, convDs, convPoly, convSpline],
160 )
161 const convParamsKey = JSON.stringify(convParams) + script
163 const runConvergence = useCallback(async () => {
164 setConvRunning(true)
165 setError(null)
166 const key = convParamsKey
167 const res = await engineRef.current!.run<ConvergeOut>(script, convParams)
168 setConvRunning(false)
169 setMs(res.ms)
170 setOutput(res.output)
171 if (res.ok) {
172 setConv(res.data)
173 setConvKey(key)
174 setError(null)
175 } else {
176 setError(res.error)
177 }
178 }, [convParams, convParamsKey, script])
180 const pickMethod = (id: string) => {
181 const m = findMethod(id)
182 if (!m) return
183 setMethodId(id)
184 setScript(m.source)
185 setDirty(false)
186 }
188 const patch = (p: Partial<Settings>) => setSettings((s) => ({ ...s, ...p }))
189 const method = findMethod(methodId)
191 return (
192 <div className="app">
193 <header className="header">
194 <div className="title">
195 <h1>Barycentric rational interpolation</h1>
196 <p>
197 Floater &amp; Hormann,{' '}
198 <a href="https://doi.org/10.1007/s00211-007-0093-y" target="_blank" rel="noreferrer">
199 Numer. Math. <b>107</b> (2007) 315&ndash;331
200 </a>
1de956dSimplify the interface for first-time visitorsJeremy Magland 201 . Every plot is computed live by a short MATLAB script you can open and edit; it runs in
202 your browser through{' '}
5392320Interactive illustration of Floater-Hormann barycentric rational interpolationJeremy Magland 203 <a href="https://numbl.org" target="_blank" rel="noreferrer">
204 numbl
205 </a>
206 .
207 </p>
208 </div>
209 <a
210 className="repo-link"
211 href="https://github.com/concept-collection/barycentric-rational"
212 target="_blank"
213 rel="noreferrer"
214 >
215 source
216 </a>
217 </header>
1de956dSimplify the interface for first-time visitorsJeremy Magland 219 {!editorOpen && (
220 <button className="editor-strip" onClick={() => setEditorOpen(true)}>
221 <span aria-hidden="true"></span>
222 <span>
223 Open the MATLAB script that computes these plots
224 {dirty && <span className="strip-note"> (edited)</span>}
225 </span>
226 {error && <span className="strip-alert">the script failed</span>}
227 </button>
228 )}
1de956dSimplify the interface for first-time visitorsJeremy Magland 231 {editorOpen && (
233 <div className="script-head">
234 <label className="field">
235 <span className="field-label">method</span>
236 <select value={dirty ? '' : methodId} onChange={(e) => pickMethod(e.target.value)}>
237 {dirty && <option value="">(edited)</option>}
238 {METHODS.map((m) => (
239 <option key={m.id} value={m.id}>
240 {m.name}
241 </option>
242 ))}
243 </select>
244 </label>
1de956dSimplify the interface for first-time visitorsJeremy Magland 245 <div className="script-actions">
246 <button
247 className="primary"
248 onClick={() => setRunToken((v) => v + 1)}
249 disabled={busy || convRunning}
250 title="⌘/Ctrl+Enter"
251 >
252 {busy || convRunning ? 'Running…' : 'Run ▶'}
253 </button>
254 <button className="ghost" onClick={() => setEditorOpen(false)} title="collapse the editor">
255 hide ◂
256 </button>
257 </div>
259 {method && !dirty && <p className="method-blurb">{method.blurb}</p>}
260 {dirty && <p className="method-blurb edited">Edited. Pick a method above to start over.</p>}
262 <ScriptEditor
263 value={script}
264 onChange={(s) => {
265 setScript(s)
266 setDirty(s !== findMethod(methodId)?.source)
267 }}
268 onRun={() => setRunToken((v) => v + 1)}
269 />
271 <div className="contract">
272 <div className="contract-head">What the app calls</div>
273 <code>w = bary_weights(x, d)</code>
274 <code>r = bary_eval(x, y, w, t)</code>
275 <code className="opt">[P, L] = local_blend(x, y, d, t)</code>
276 <span className="contract-note">the third is optional; without it the second tab is empty</span>
277 </div>
279 {error && (
280 <div className="error-box">
281 <div className="error-head">The script failed</div>
282 <pre>{error}</pre>
283 </div>
284 )}
285 {output.trim() && !error && (
286 <details className="console">
287 <summary>console output</summary>
288 <pre>{output}</pre>
289 </details>
290 )}
291 </section>
294 <section className="right">
295 <nav className="tabs">
296 {TABS.map((t) => (
297 <button key={t.id} className={`tab ${tab === t.id ? 'on' : ''}`} onClick={() => setTab(t.id)}>
298 {t.label}
299 </button>
300 ))}
301 <span className="run-status">
302 {busy || convRunning ? 'running…' : ms != null ? `${Math.round(ms)} ms` : ''}
303 </span>
304 </nav>
306 <Controls value={settings} onChange={patch} showN={tab !== 'convergence'} busy={false} />
308 <div className="panel-scroll">
309 {tab === 'convergence' ? (
310 <ConvergencePanel
311 out={conv}
312 running={convRunning}
313 stale={conv != null && convKey !== convParamsKey}
314 f={settings.f}
315 nodes={settings.nodes}
316 ds={convDs}
317 maxN={convMaxN}
318 showSpline={convSpline}
319 showPoly={convPoly}
320 onChange={(p) => {
321 if (p.ds) setConvDs(p.ds)
322 if (p.maxN) setConvMaxN(p.maxN)
323 if (p.showSpline !== undefined) setConvSpline(p.showSpline)
324 if (p.showPoly !== undefined) setConvPoly(p.showPoly)
325 }}
326 onRun={runConvergence}
327 />
328 ) : explore == null ? (
329 <div className="panel">
330 <p className="panel-note muted">{error ? 'Fix the script to see the plots.' : 'Starting numbl…'}</p>
331 </div>
332 ) : tab === 'interpolant' ? (
333 <InterpolantPanel
334 out={explore}
335 showPoly={showPoly}
336 showSpline={showSpline}
337 onToggle={(which, on) => (which === 'poly' ? setShowPoly(on) : setShowSpline(on))}
338 />
339 ) : tab === 'blending' ? (
340 <BlendingPanel out={explore} />
341 ) : (
342 <PolesPanel out={explore} showClassical={showClassical} onToggleClassical={setShowClassical} />
343 )}
344 </div>
345 </section>
346 </div>
347 </div>
348 )
moveopenescclose