Skip to main content

graph_explorer_layout/
quadtree.rs

1//! Barnes-Hut quadtree for many-body repulsion in `O(n log n)`.
2//!
3//! Each cell accumulates the mass and position-sum of the bodies beneath it.
4//! When a cell is far enough away relative to its size (`width / distance <
5//! theta`) it is treated as one body at its centroid instead of being descended
6//! into — which is the entire reason this is not `O(n²)`.
7//!
8//! The force law is d3-force's many-body law: the force on `a` from `b` is
9//! `strength * (b - a) / d²`, whose *magnitude* therefore falls off as `1/d`.
10//! Negative `strength` repels. Both properties are load-bearing — see the
11//! comment on [`QuadTree::repulsion`].
12
13/// Sentinel for "no cell" / "no body". `usize::MAX` cannot collide with a real
14/// index because the arena would have to hold `usize::MAX` cells first.
15const NONE: usize = usize::MAX;
16
17/// Subdivision depth cap. Without it, coincident or near-coincident bodies
18/// subdivide until the cell size underflows and the build never terminates.
19const MAX_DEPTH: u32 = 24;
20
21/// Below this squared distance two bodies are treated as exactly coincident and
22/// separated by a deterministic unit jitter rather than dividing by ~zero.
23const EPS2: f32 = 1e-6;
24
25/// Squared minimum interaction distance (d3's `distanceMin`, default 1).
26///
27/// Without it the `1/d` law is unbounded as `d -> 0`: two bodies 0.01 apart
28/// would exchange a force 100x larger than the strength, which in one tick
29/// launches them off-screen. Clamping bounds every pairwise force by
30/// `|strength|`.
31const MIN_DIST2: f32 = 1.0;
32
33#[derive(Clone)]
34struct Cell {
35    center: [f32; 2],
36    half: f32,
37    /// Number of bodies beneath this cell; every node weighs 1.
38    mass: f32,
39    /// Sum of member positions. Centroid is `sum / mass`.
40    sum: [f32; 2],
41    /// Child cells (NW, NE, SW, SE), `NONE` when absent.
42    kids: [usize; 4],
43    /// Head of this leaf's body chain, or `NONE` for an internal cell.
44    body: usize,
45}
46
47pub struct QuadTree {
48    cells: Vec<Cell>,
49    /// Intrusive linked list: `next[b]` is the next body sharing `b`'s leaf.
50    /// Lets a leaf hold several coincident bodies without allocating per leaf.
51    next: Vec<usize>,
52}
53
54impl QuadTree {
55    pub fn build(pos: &[[f32; 2]]) -> Self {
56        let mut t = QuadTree { cells: Vec::new(), next: vec![NONE; pos.len()] };
57        if pos.is_empty() {
58            return t;
59        }
60        // `f32::MIN` is the most-negative *finite* f32, so it is a valid
61        // identity for a running max over finite inputs (unlike, say,
62        // `f32::MIN_POSITIVE`). Non-finite inputs would poison it, but they
63        // would poison every downstream force anyway.
64        let (mut lo, mut hi) = ([f32::MAX; 2], [f32::MIN; 2]);
65        for p in pos {
66            lo[0] = lo[0].min(p[0]); lo[1] = lo[1].min(p[1]);
67            hi[0] = hi[0].max(p[0]); hi[1] = hi[1].max(p[1]);
68        }
69        // A square root cell keeps quadrants square, so `half` is a valid
70        // proxy for cell width in the theta test.
71        let half = ((hi[0] - lo[0]).max(hi[1] - lo[1]) * 0.5).max(1.0);
72        let center = [(lo[0] + hi[0]) * 0.5, (lo[1] + hi[1]) * 0.5];
73        t.cells.push(Cell { center, half, mass: 0.0, sum: [0.0, 0.0], kids: [NONE; 4], body: NONE });
74        for i in 0..pos.len() {
75            t.insert(0, i, pos, 0);
76        }
77        t
78    }
79
80    fn quadrant(center: [f32; 2], p: [f32; 2]) -> usize {
81        // 0 NW, 1 NE, 2 SW, 3 SE
82        (if p[0] >= center[0] { 1 } else { 0 }) | (if p[1] < center[1] { 2 } else { 0 })
83    }
84
85    fn child_cell(parent: &Cell, q: usize) -> Cell {
86        let h = parent.half * 0.5;
87        let dx = if q & 1 == 1 { h } else { -h };
88        let dy = if q & 2 == 2 { -h } else { h };
89        Cell {
90            center: [parent.center[0] + dx, parent.center[1] + dy],
91            half: h,
92            mass: 0.0,
93            sum: [0.0, 0.0],
94            kids: [NONE; 4],
95            body: NONE,
96        }
97    }
98
99    fn insert(&mut self, cell: usize, b: usize, pos: &[[f32; 2]], depth: u32) {
100        self.cells[cell].mass += 1.0;
101        self.cells[cell].sum[0] += pos[b][0];
102        self.cells[cell].sum[1] += pos[b][1];
103
104        let is_internal = self.cells[cell].kids.iter().any(|&k| k != NONE);
105        if is_internal {
106            let q = Self::quadrant(self.cells[cell].center, pos[b]);
107            let kid = self.ensure_kid(cell, q);
108            self.insert(kid, b, pos, depth + 1);
109            return;
110        }
111
112        let existing = self.cells[cell].body;
113        if existing == NONE {
114            self.cells[cell].body = b;
115            return;
116        }
117        if depth >= MAX_DEPTH {
118            // Coincident (or effectively so). Chain rather than subdivide —
119            // `repulsion` walks the chain and skips self, so a multi-body leaf
120            // stays exact instead of self-interacting.
121            self.next[b] = existing;
122            self.cells[cell].body = b;
123            return;
124        }
125
126        // Split: push the sitting body down, then the new one.
127        self.cells[cell].body = NONE;
128        for who in [existing, b] {
129            let q = Self::quadrant(self.cells[cell].center, pos[who]);
130            let kid = self.ensure_kid(cell, q);
131            // The parent's mass/sum already counted both, so recurse into the
132            // child directly rather than back through `insert` on the parent.
133            self.insert(kid, who, pos, depth + 1);
134        }
135    }
136
137    fn ensure_kid(&mut self, cell: usize, q: usize) -> usize {
138        let existing = self.cells[cell].kids[q];
139        if existing != NONE {
140            return existing;
141        }
142        let c = Self::child_cell(&self.cells[cell], q);
143        self.cells.push(c);
144        let idx = self.cells.len() - 1;
145        self.cells[cell].kids[q] = idx;
146        idx
147    }
148
149    /// Force on body `i`. Negative `strength` repels (d3's convention).
150    ///
151    /// The contribution of body `b` is `strength * (b - i) / d²`, so a negative
152    /// strength points the force *away* from `b`. The magnitude falls off as
153    /// `1/d`, not `1/d²`: that is d3's law, and it is what makes the Barnes-Hut
154    /// monopole approximation accurate enough to run at `theta = 0.9`.
155    pub fn repulsion(&self, i: usize, pos: &[[f32; 2]], theta: f32, strength: f32) -> [f32; 2] {
156        let mut f = [0.0f32, 0.0];
157        if self.cells.is_empty() || i >= pos.len() {
158            return f;
159        }
160        self.walk(0, i, pos, theta, strength, &mut f);
161        f
162    }
163
164    fn walk(&self, cell: usize, i: usize, pos: &[[f32; 2]], theta: f32, strength: f32, f: &mut [f32; 2]) {
165        let c = &self.cells[cell];
166        if c.mass == 0.0 {
167            return;
168        }
169        let is_internal = c.kids.iter().any(|&k| k != NONE);
170
171        if !is_internal {
172            // Leaf: apply each chained body individually, skipping self.
173            let mut b = c.body;
174            while b != NONE {
175                if b != i {
176                    Self::pair(pos[i], pos[b], i, b, strength, f);
177                }
178                b = self.next[b];
179            }
180            return;
181        }
182
183        let centroid = [c.sum[0] / c.mass, c.sum[1] / c.mass];
184        let dx = centroid[0] - pos[i][0];
185        let dy = centroid[1] - pos[i][1];
186        let d2 = dx * dx + dy * dy;
187        // `half * 2` is the cell width, since `child_cell` halves `half` at
188        // every level and a cell spans `center ± half`. So this is d3's
189        // `width / distance < theta`, compared in squared form.
190        if d2 > 0.0 && (c.half * 2.0) * (c.half * 2.0) < theta * theta * d2 {
191            let d2 = Self::clamp_near(d2);
192            let mag = strength * c.mass / d2;
193            f[0] += dx * mag;
194            f[1] += dy * mag;
195            return;
196        }
197        for &k in &c.kids {
198            if k != NONE {
199                self.walk(k, i, pos, theta, strength, f);
200            }
201        }
202    }
203
204    /// d3's `distanceMin` softening: pull sub-`MIN_DIST2` separations back up
205    /// toward the floor so the `1/d` law cannot produce an unbounded force.
206    fn clamp_near(d2: f32) -> f32 {
207        if d2 < MIN_DIST2 { (MIN_DIST2 * d2).sqrt() } else { d2 }
208    }
209
210    fn pair(a: [f32; 2], b: [f32; 2], ai: usize, bi: usize, strength: f32, f: &mut [f32; 2]) {
211        let mut dx = b[0] - a[0];
212        let mut dy = b[1] - a[1];
213        let mut d2 = dx * dx + dy * dy;
214        if d2 < EPS2 {
215            // Exactly (or all but) coincident: there is no meaningful direction,
216            // so pick one deterministically from the index pair — a rebuild of
217            // the same graph then produces the same layout, the seeded-layout
218            // property the now-deleted one-shot layout also guaranteed.
219            //
220            // The jitter is a *unit* vector applied at the minimum interaction
221            // distance, so the resulting force is exactly `|strength|`: the same
222            // nudge two bodies one unit apart would feel. Using the raw jitter
223            // length here instead would divide by ~1e-6 and yield a force ~1e5x
224            // the strength, which is a catapult, not a nudge.
225            let s = (ai.wrapping_mul(31).wrapping_add(bi)) as f32;
226            let (jx, jy) = ((s * 0.7).sin(), (s * 1.3).cos());
227            let n = (jx * jx + jy * jy).sqrt().max(1e-6);
228            dx = jx / n;
229            dy = jy / n;
230            d2 = MIN_DIST2;
231        }
232        let d2 = Self::clamp_near(d2);
233        let mag = strength / d2;
234        f[0] += dx * mag;
235        f[1] += dy * mag;
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    /// Reference implementation: every pair, no approximation.
244    ///
245    /// Mirrors d3-force's many-body law exactly — `strength * (p - i) / d²`,
246    /// magnitude `|strength| / d`. The direction is carried by the unnormalised
247    /// delta, so there is no separate unit-vector step; normalising *and*
248    /// dividing by `d²` would be a `1/d²` law, which is a different force.
249    fn brute_force(pos: &[[f32; 2]], i: usize, strength: f32) -> [f32; 2] {
250        let mut f = [0.0f32, 0.0];
251        for (j, p) in pos.iter().enumerate() {
252            if j == i { continue; }
253            let dx = p[0] - pos[i][0];
254            let dy = p[1] - pos[i][1];
255            let d2 = (dx * dx + dy * dy).max(1e-6);
256            let mag = strength / d2;
257            f[0] += dx * mag;
258            f[1] += dy * mag;
259        }
260        f
261    }
262
263    /// Deterministic scatter — no rand dependency, and reproducible failures.
264    fn scatter(n: usize) -> Vec<[f32; 2]> {
265        let mut s = 12345u64;
266        (0..n)
267            .map(|_| {
268                let mut nxt = || {
269                    s ^= s >> 12; s ^= s << 25; s ^= s >> 27;
270                    ((s.wrapping_mul(0x2545F4914F6CDD1D) >> 33) as f32) / (1u64 << 31) as f32
271                };
272                [(nxt() - 0.5) * 1000.0, (nxt() - 0.5) * 1000.0]
273            })
274            .collect()
275    }
276
277    #[test]
278    fn theta_zero_is_exact_because_no_cell_can_be_approximated() {
279        // theta = 0 forces full recursion, so the tree must reproduce
280        // brute force to floating-point noise. This is the test that proves
281        // the traversal visits every body exactly once.
282        let pos = scatter(60);
283        let t = QuadTree::build(&pos);
284        for i in 0..pos.len() {
285            let got = t.repulsion(i, &pos, 0.0, -100.0);
286            let want = brute_force(&pos, i, -100.0);
287            assert!(
288                (got[0] - want[0]).abs() < 0.05 && (got[1] - want[1]).abs() < 0.05,
289                "body {i}: tree {got:?} vs brute {want:?}"
290            );
291        }
292    }
293
294    #[test]
295    fn default_theta_approximates_brute_force_within_tolerance() {
296        let pos = scatter(200);
297        let t = QuadTree::build(&pos);
298        let mut worst = 0.0f32;
299        for i in 0..pos.len() {
300            let got = t.repulsion(i, &pos, 0.9, -100.0);
301            let want = brute_force(&pos, i, -100.0);
302            let mag = (want[0] * want[0] + want[1] * want[1]).sqrt().max(1e-3);
303            let err = ((got[0] - want[0]).powi(2) + (got[1] - want[1]).powi(2)).sqrt() / mag;
304            worst = worst.max(err);
305        }
306        assert!(worst < 0.30, "worst relative error {worst} exceeds 30% at theta=0.9");
307    }
308
309    /// The monopole approximation should degrade smoothly with theta, and
310    /// vanish at theta = 0. A tree whose mass or centroid accumulation is wrong
311    /// can still pass the theta = 0 test (which never reads either) but will
312    /// break this one, because every approximated cell then carries the error.
313    #[test]
314    fn accuracy_degrades_monotonically_with_theta() {
315        let pos = scatter(200);
316        let t = QuadTree::build(&pos);
317        let err_at = |theta: f32| {
318            let mut worst = 0.0f32;
319            for i in 0..pos.len() {
320                let got = t.repulsion(i, &pos, theta, -100.0);
321                let want = brute_force(&pos, i, -100.0);
322                let mag = (want[0] * want[0] + want[1] * want[1]).sqrt().max(1e-3);
323                let e = ((got[0] - want[0]).powi(2) + (got[1] - want[1]).powi(2)).sqrt() / mag;
324                worst = worst.max(e);
325            }
326            worst
327        };
328        let (e0, e3, e5, e9) = (err_at(0.0), err_at(0.3), err_at(0.5), err_at(0.9));
329        assert!(e0 < 1e-4, "theta=0 must be exact, got {e0}");
330        assert!(e3 < e5 && e5 < e9, "error must grow with theta: {e3} {e5} {e9}");
331    }
332
333    /// Mass and centroid are only exercised by *approximated* cells, so the
334    /// theta = 0 test cannot catch a double-count. Check the accumulation
335    /// directly: every internal cell must equal the sum of its children, and
336    /// the root must hold every body exactly once.
337    #[test]
338    fn every_cell_holds_each_body_beneath_it_exactly_once() {
339        let pos = scatter(200);
340        let t = QuadTree::build(&pos);
341        let n = pos.len() as f32;
342        assert_eq!(t.cells[0].mass, n, "root mass must be the body count");
343        let mean = [
344            pos.iter().map(|p| p[0]).sum::<f32>() / n,
345            pos.iter().map(|p| p[1]).sum::<f32>() / n,
346        ];
347        assert!((t.cells[0].sum[0] / n - mean[0]).abs() < 0.01);
348        assert!((t.cells[0].sum[1] / n - mean[1]).abs() < 0.01);
349
350        let mut reachable = 0usize;
351        for c in &t.cells {
352            if c.kids.iter().any(|&k| k != NONE) {
353                let km: f32 = c.kids.iter().filter(|&&k| k != NONE).map(|&k| t.cells[k].mass).sum();
354                assert!((km - c.mass).abs() < 1e-3, "cell mass {} vs kids {km}", c.mass);
355            } else {
356                // Leaf: its mass must equal the length of its body chain.
357                let mut b = c.body;
358                let mut chain = 0.0f32;
359                while b != NONE {
360                    reachable += 1;
361                    chain += 1.0;
362                    b = t.next[b];
363                }
364                assert!((chain - c.mass).abs() < 1e-3, "leaf mass {} vs chain {chain}", c.mass);
365            }
366        }
367        assert_eq!(reachable, pos.len(), "every body must sit in exactly one leaf");
368    }
369
370    #[test]
371    fn a_body_never_repels_itself() {
372        // One body: the only possible force is a self-interaction, so any
373        // non-zero result means the traversal failed to exclude it.
374        let pos = vec![[10.0, 10.0]];
375        let t = QuadTree::build(&pos);
376        assert_eq!(t.repulsion(0, &pos, 0.9, -100.0), [0.0, 0.0]);
377    }
378
379    #[test]
380    fn coincident_bodies_do_not_produce_nan() {
381        // Two nodes at the identical point is not exotic — it is what a fresh
382        // seed or a degenerate layout produces, and a 1/0 here poisons every
383        // position downstream for the rest of the session.
384        let pos = vec![[5.0, 5.0], [5.0, 5.0], [5.0, 5.0]];
385        let t = QuadTree::build(&pos);
386        for i in 0..3 {
387            let f = t.repulsion(i, &pos, 0.9, -100.0);
388            assert!(f[0].is_finite() && f[1].is_finite(), "body {i} produced {f:?}");
389        }
390    }
391
392    /// A degenerate cluster must produce a *nudge*, not a catapult. Without a
393    /// minimum interaction distance the coincident case divides by ~1e-6 and
394    /// returns a force ~1e5x the strength, which ejects the cluster to infinity
395    /// on the first tick while still being perfectly finite.
396    #[test]
397    fn coincident_bodies_are_nudged_not_launched() {
398        let pos = vec![[5.0, 5.0], [5.0, 5.0], [5.0, 5.0]];
399        let t = QuadTree::build(&pos);
400        for i in 0..3 {
401            let f = t.repulsion(i, &pos, 0.9, -100.0);
402            let mag = (f[0] * f[0] + f[1] * f[1]).sqrt();
403            assert!(mag <= 200.0, "body {i} force {mag} exceeds two bodies' worth of strength");
404        }
405        // And the nudge must be deterministic across rebuilds.
406        let t2 = QuadTree::build(&pos);
407        assert_eq!(t.repulsion(0, &pos, 0.9, -100.0), t2.repulsion(0, &pos, 0.9, -100.0));
408    }
409
410    /// The `distanceMin` softening in [`QuadTree::clamp_near`] is what makes a
411    /// near-coincident pair a nudge rather than a catapult, and *deleting it
412    /// passes every other test in this file* — the `1/d` law stays perfectly
413    /// finite as it runs away. At `d = 0.01` with the shipping charge strength
414    /// the softened force is 800; unsoftened it is 80,000, which ejects the
415    /// pair off-screen on the very first tick.
416    #[test]
417    fn a_near_coincident_pair_is_softened_to_about_the_strength() {
418        let pos = vec![[0.0, 0.0], [0.01, 0.0]];
419        let t = QuadTree::build(&pos);
420        let f = t.repulsion(0, &pos, 0.9, -800.0);
421        let mag = (f[0] * f[0] + f[1] * f[1]).sqrt();
422        assert!(
423            (mag - 800.0).abs() < 1.0,
424            "expected the softened |strength| = 800, got {mag}; unsoftened this is 80000"
425        );
426        assert!(f[0] < 0.0, "and it must still point away from the other body, got {f:?}");
427    }
428
429    /// The coincident-body jitter is keyed on the *index pair*, so each member
430    /// of a degenerate cluster is pushed a different way and the cluster comes
431    /// apart. Replacing the key with a constant also passes every other test
432    /// here: the forces stay finite and bounded, but become identical, so the
433    /// cluster translates rigidly and never separates — which looks, on screen,
434    /// exactly like nodes that have fused.
435    #[test]
436    fn coincident_bodies_are_pushed_in_different_directions() {
437        let pos = vec![[5.0, 5.0], [5.0, 5.0]];
438        let t = QuadTree::build(&pos);
439        let f0 = t.repulsion(0, &pos, 0.9, -100.0);
440        let f1 = t.repulsion(1, &pos, 0.9, -100.0);
441        let spread = ((f0[0] - f1[0]).powi(2) + (f0[1] - f1[1]).powi(2)).sqrt();
442        assert!(
443            spread > 1.0,
444            "bodies got near-identical pushes {f0:?} and {f1:?}; a rigid cluster never separates"
445        );
446    }
447
448    #[test]
449    fn deeply_clustered_bodies_terminate() {
450        // All-but-identical points would subdivide forever without a depth cap.
451        let pos: Vec<[f32; 2]> = (0..50).map(|i| [1.0 + i as f32 * 1e-7, 1.0]).collect();
452        let t = QuadTree::build(&pos);
453        let f = t.repulsion(0, &pos, 0.9, -100.0);
454        assert!(f[0].is_finite() && f[1].is_finite());
455    }
456
457    #[test]
458    fn an_empty_set_builds_and_answers_nothing() {
459        let pos: Vec<[f32; 2]> = vec![];
460        let t = QuadTree::build(&pos);
461        assert_eq!(t.repulsion(0, &pos, 0.9, -100.0), [0.0, 0.0]);
462    }
463
464    #[test]
465    fn repulsion_pushes_apart_and_attraction_pulls_together() {
466        // Sign convention: negative strength repels, which is d3's.
467        let pos = vec![[0.0, 0.0], [10.0, 0.0]];
468        let t = QuadTree::build(&pos);
469        let f = t.repulsion(0, &pos, 0.9, -100.0);
470        assert!(f[0] < 0.0, "body 0 should be pushed away from body 1 (leftward), got {f:?}");
471        let a = t.repulsion(0, &pos, 0.9, 100.0);
472        assert!(a[0] > 0.0, "positive strength should attract, got {a:?}");
473    }
474
475    /// The force must fall off as `1/d`, not `1/d²` — doubling the separation
476    /// must halve the force. This is the property the `theta = 0.9` accuracy
477    /// budget depends on, and it is invisible to every other test here because
478    /// they all compare the tree against a reference that shares its law.
479    #[test]
480    fn force_magnitude_falls_off_as_inverse_distance() {
481        let near = vec![[0.0, 0.0], [10.0, 0.0]];
482        let far = vec![[0.0, 0.0], [20.0, 0.0]];
483        let fn_ = QuadTree::build(&near).repulsion(0, &near, 0.0, -100.0)[0].abs();
484        let ff = QuadTree::build(&far).repulsion(0, &far, 0.0, -100.0)[0].abs();
485        assert!((fn_ - 10.0).abs() < 1e-3, "expected |strength|/d = 10, got {fn_}");
486        assert!((ff - 5.0).abs() < 1e-3, "expected |strength|/d = 5, got {ff}");
487    }
488}