/ concept-collection / qhull-wasm-demo
Sign in
concept-collection / qhull-wasm-demo
Fix stale-index crash on shrinking point count; add desktop reference timings
Jeremy Magland <jmagland@flatironinstitute.org> committed commit 435c5b9575f7 parent 283cae2 Browse files
4 changed files+82−19
README.mdmodified+16−1View file
@@ -14,7 +14,22 @@ in the browser.
1414 - **Benchmarks** — times Delaunay triangulation in the browser, alongside a
1515 `.m` script that runs identically in MATLAB, Octave, and
1616 [numbl](https://numbl.org) (all triangulate via Qhull) for an apples-to-apples
17- desktop-vs-browser comparison. See [scripts/qhull_benchmark.m](scripts/qhull_benchmark.m).
17+ desktop-vs-browser comparison. See [scripts/qhull_benchmark.m](scripts/qhull_benchmark.m):
18+
19+ ```
20+ matlab -batch qhull_benchmark # MATLAB
21+ octave qhull_benchmark.m # Octave
22+ numbl run qhull_benchmark.m # numbl
23+ ```
24+
25+ Reference desktop timings, Delaunay in ms (one Linux laptop — hardware-dependent):
26+
27+ | N | numbl 2D | numbl 3D | MATLAB 2D | MATLAB 3D |
28+ |--------|---------:|---------:|----------:|----------:|
29+ | 1,000 | 4.1 | 16.5 | 21.1 | 21.3 |
30+ | 5,000 | 18.6 | 54.3 | 28.6 | 185.5 |
31+ | 20,000 | 65.5 | 287.3 | 179.9 | 624.2 |
32+ | 50,000 | 154.7 | 757.0 | 373.8 | 1703.9 |
1833
1934 ## Develop
2035
src/components/Benchmarks.tsxmodified+42−0View file
@@ -9,6 +9,15 @@ import benchScript from '../../scripts/qhull_benchmark.m?raw'
99
1010 const SIZES = [1000, 5000, 20000, 50000]
1111
12+// Reference timings from running scripts/qhull_benchmark.m on a Linux laptop.
13+// Hardware-dependent — for rough comparison only.
14+const REFERENCE: { n: number; numbl: [number, number]; matlab: [number, number] }[] = [
15+ { n: 1000, numbl: [4.1, 16.5], matlab: [21.1, 21.3] },
16+ { n: 5000, numbl: [18.6, 54.3], matlab: [28.6, 185.5] },
17+ { n: 20000, numbl: [65.5, 287.3], matlab: [179.9, 624.2] },
18+ { n: 50000, numbl: [154.7, 757.0], matlab: [373.8, 1703.9] },
19+]
20+
1221 interface Row { n: number; ms2: number; ms3: number; simp2: number; simp3: number }
1322
1423 const yield_ = () => new Promise((r) => setTimeout(r, 0))
@@ -101,6 +110,39 @@ export function Benchmarks() {
101110 <Paper variant="outlined" sx={{ p: 2, bgcolor: '#0f1722', overflow: 'auto' }}>
102111 <pre style={{ margin: 0, color: '#e0e0e0', fontSize: 13, lineHeight: 1.45 }}>{benchScript}</pre>
103112 </Paper>
113+ <Typography variant="caption" color="text.secondary">
114+ Run it with <code>matlab -batch qhull_benchmark</code>, <code>octave qhull_benchmark.m</code>,
115+ or <code>numbl run qhull_benchmark.m</code>.
116+ </Typography>
117+
118+ <Typography variant="subtitle2" sx={{ pt: 1 }}>Reference desktop timings (Delaunay, ms)</Typography>
119+ <Typography variant="caption" color="text.secondary">
120+ Measured on one Linux laptop — hardware-dependent, for rough comparison only.
121+ </Typography>
122+ <TableContainer component={Paper} variant="outlined" sx={{ maxWidth: 560 }}>
123+ <Table size="small">
124+ <TableHead>
125+ <TableRow>
126+ <TableCell>N</TableCell>
127+ <TableCell align="right">numbl 2D</TableCell>
128+ <TableCell align="right">numbl 3D</TableCell>
129+ <TableCell align="right">MATLAB 2D</TableCell>
130+ <TableCell align="right">MATLAB 3D</TableCell>
131+ </TableRow>
132+ </TableHead>
133+ <TableBody>
134+ {REFERENCE.map((r) => (
135+ <TableRow key={r.n}>
136+ <TableCell>{r.n.toLocaleString()}</TableCell>
137+ <TableCell align="right">{r.numbl[0].toFixed(1)}</TableCell>
138+ <TableCell align="right">{r.numbl[1].toFixed(1)}</TableCell>
139+ <TableCell align="right">{r.matlab[0].toFixed(1)}</TableCell>
140+ <TableCell align="right">{r.matlab[1].toFixed(1)}</TableCell>
141+ </TableRow>
142+ ))}
143+ </TableBody>
144+ </Table>
145+ </TableContainer>
104146 </Stack>
105147 )
106148 }
src/components/Delaunay2D.tsxmodified+17−14View file
@@ -9,6 +9,8 @@ import { points2D, type Dist2D } from '../points'
99 const SIZE = 520
1010 const PAD = 24
1111
12+interface Result { pts: number[][]; tris: number[][]; hull: number[][] }
13+
1214 export function Delaunay2D() {
1315 const [dist, setDist] = useState<Dist2D>('uniform')
1416 const [n, setN] = useState(60)
@@ -23,20 +25,20 @@ export function Delaunay2D() {
2325 [n, dist, seed, extra],
2426 )
2527
26- const [tris, setTris] = useState<number[][]>([])
27- const [hull, setHull] = useState<number[][]>([])
28+ // The triangulation/hull are stored together with the exact points they were
29+ // computed from, so facet indices can never reference a stale (out-of-range)
30+ // point set while qhull is recomputing.
31+ const [result, setResult] = useState<Result>({ pts: [], tris: [], hull: [] })
2832
2933 useEffect(() => {
3034 let cancelled = false
3135 getQhull().then((q) => {
32- if (cancelled || pts.length < 3) {
33- setTris([]); setHull([]); return
34- }
36+ if (cancelled) return
37+ if (pts.length < 3) { setResult({ pts, tris: [], hull: [] }); return }
3538 try {
36- setTris(q.delaunay(pts, 2).facets)
37- setHull(q.convexHull(pts, 2).facets)
39+ setResult({ pts, tris: q.delaunay(pts, 2).facets, hull: q.convexHull(pts, 2).facets })
3840 } catch {
39- setTris([]); setHull([])
41+ setResult({ pts, tris: [], hull: [] })
4042 }
4143 })
4244 return () => { cancelled = true }
@@ -60,9 +62,10 @@ export function Delaunay2D() {
6062 setExtra((cur) => [...cur, [x, y]])
6163 }
6264
65+ const { pts: rpts, tris, hull } = result
6366 const circles = useMemo(
64- () => (showCircles ? tris.map((t) => circumcircle(pts[t[0]], pts[t[1]], pts[t[2]])) : []),
65- [showCircles, tris, pts],
67+ () => (showCircles ? tris.map((t) => circumcircle(rpts[t[0]], rpts[t[1]], rpts[t[2]])) : []),
68+ [showCircles, tris, rpts],
6669 )
6770
6871 return (
@@ -105,15 +108,15 @@ export function Delaunay2D() {
105108 r={c.r * (SIZE - 2 * PAD)} fill="none" stroke="#26a69a" strokeWidth={0.5} opacity={0.4} />
106109 ))}
107110 {showTri && tris.map((t, i) => {
108- const a = toScreen(pts[t[0]]), b = toScreen(pts[t[1]]), c = toScreen(pts[t[2]])
111+ const a = toScreen(rpts[t[0]]), b = toScreen(rpts[t[1]]), c = toScreen(rpts[t[2]])
109112 return <polygon key={`t${i}`} points={`${a[0]},${a[1]} ${b[0]},${b[1]} ${c[0]},${c[1]}`}
110113 fill="#1976d2" fillOpacity={0.07} stroke="#1976d2" strokeWidth={0.8} />
111114 })}
112115 {showHull && hull.map((e, i) => {
113- const a = toScreen(pts[e[0]]), b = toScreen(pts[e[1]])
116+ const a = toScreen(rpts[e[0]]), b = toScreen(rpts[e[1]])
114117 return <line key={`h${i}`} x1={a[0]} y1={a[1]} x2={b[0]} y2={b[1]} stroke="#f57c00" strokeWidth={2.2} />
115118 })}
116- {pts.map((p, i) => {
119+ {rpts.map((p, i) => {
117120 const s = toScreen(p)
118121 return <circle key={`p${i}`} cx={s[0]} cy={s[1]} r={2.4} fill="#222" />
119122 })}
@@ -121,7 +124,7 @@ export function Delaunay2D() {
121124 </Box>
122125
123126 <Typography variant="body2" color="text.secondary">
124- {pts.length} points → {tris.length} triangles, {hull.length} hull edges.
127+ {rpts.length} points → {tris.length} triangles, {hull.length} hull edges.
125128 </Typography>
126129 </Stack>
127130 )
src/components/Hull3D.tsxmodified+7−4View file
@@ -17,13 +17,15 @@ export function Hull3D() {
1717 const [spin, setSpin] = useState(true)
1818
1919 const pts = useMemo(() => points3D(n, dist, seed), [n, dist, seed])
20- const [hull, setHull] = useState<number[][]>([])
20+ // Hull facets are kept together with the points they were computed from, so
21+ // facet indices never reference a stale point set while qhull recomputes.
22+ const [data, setData] = useState<{ pts: number[][]; hull: number[][] }>({ pts: [], hull: [] })
2123
2224 useEffect(() => {
2325 let cancelled = false
2426 getQhull().then((q) => {
2527 if (cancelled) return
26- try { setHull(q.convexHull(pts, 3).facets) } catch { setHull([]) }
28+ try { setData({ pts, hull: q.convexHull(pts, 3).facets }) } catch { setData({ pts, hull: [] }) }
2729 })
2830 return () => { cancelled = true }
2931 }, [pts])
@@ -107,6 +109,7 @@ export function Hull3D() {
107109 })
108110 group.clear()
109111
112+ const { pts, hull } = data
110113 // center & scale points to fit a unit-ish box
111114 const flat = pts.flat()
112115 const c = [0, 1, 2].map((k) => mean(pts.map((p) => p[k])))
@@ -143,7 +146,7 @@ export function Hull3D() {
143146 pts.flatMap((p) => { const v = xf(p); return [v.x, v.y, v.z] }), 3))
144147 group.add(new THREE.Points(pg, new THREE.PointsMaterial({ color: 0xffd54f, size: 0.04 })))
145148 }
146- }, [pts, hull, showPoints, wireframe])
149+ }, [data, showPoints, wireframe])
147150
148151 return (
149152 <Stack spacing={2}>
@@ -170,7 +173,7 @@ export function Hull3D() {
170173 </Stack>
171174 <Box ref={mountRef} sx={{ width: '100%', borderRadius: 1, overflow: 'hidden', lineHeight: 0 }} />
172175 <Typography variant="body2" color="text.secondary">
173- {pts.length} points → hull with {hull.length} triangular facets.
176+ {pts.length} points → hull with {data.hull.length} triangular facets.
174177 </Typography>
175178 </Stack>
176179 )
moveopenescclose