Skip to main content

bonsai/backends/
quadtree.rs

1//! Loose 2^D-tree (Quadtree / Octree / Hexadecatree) spatial index backend.
2//!
3//! Generalises the 2D quadtree, 3D octree, and 4D hexadecatree under a single
4//! generic struct parameterised by `D`. Each internal node has exactly `2^D`
5//! children. The "loose" variant expands each node's effective bounds by 2×
6//! the cell half-size so that a point near a boundary is always contained in
7//! exactly one child's loose bounds.
8
9use std::collections::HashMap;
10
11use crate::backends::SpatialBackend;
12use crate::types::{BBox, BackendKind, CoordType, EntryId, Point};
13
14const LEAF_CAPACITY: usize = 8;
15const MAX_DEPTH: usize = 20;
16
17struct QuadNode<C, const D: usize> {
18    bounds: BBox<C, D>,
19    loose_bounds: BBox<C, D>,
20    entries: Vec<(Point<C, D>, EntryId)>,
21    children: Option<Vec<QuadNode<C, D>>>,
22    depth: usize,
23}
24
25impl<C: CoordType, const D: usize> QuadNode<C, D> {
26    fn new_leaf(bounds: BBox<C, D>, loose_bounds: BBox<C, D>, depth: usize) -> Self {
27        Self {
28            bounds,
29            loose_bounds,
30            entries: Vec::new(),
31            children: None,
32            depth,
33        }
34    }
35
36    /// Assigns a point to one of the `2^D` children by comparing each axis
37    /// coordinate against the node midpoint. Bit `d` is set if the point is
38    /// on the upper half of axis `d`.
39    fn child_index(&self, point: &Point<C, D>) -> usize {
40        let mut idx = 0usize;
41        for d in 0..D {
42            let lo: f64 = self.bounds.min.coords()[d].into();
43            let hi: f64 = self.bounds.max.coords()[d].into();
44            let mid: C = C::from(((lo + hi) * 0.5) as f32);
45            if point.coords()[d] >= mid {
46                idx |= 1 << d;
47            }
48        }
49        idx
50    }
51
52    fn make_children(&self) -> Vec<QuadNode<C, D>> {
53        let num_children = 1usize << D;
54        let mut children = Vec::with_capacity(num_children);
55        for ci in 0..num_children {
56            let mut child_min = [C::zero(); D];
57            let mut child_max = [C::zero(); D];
58            for d in 0..D {
59                let lo: f64 = self.bounds.min.coords()[d].into();
60                let hi: f64 = self.bounds.max.coords()[d].into();
61                let mid = (lo + hi) * 0.5;
62                if (ci >> d) & 1 == 0 {
63                    child_min[d] = C::from(lo as f32);
64                    child_max[d] = C::from(mid as f32);
65                } else {
66                    child_min[d] = C::from(mid as f32);
67                    child_max[d] = C::from(hi as f32);
68                }
69            }
70            let child_bounds = BBox::new(Point::new(child_min), Point::new(child_max));
71            let mut loose_min = [C::zero(); D];
72            let mut loose_max = [C::zero(); D];
73            for d in 0..D {
74                let lo: f64 = child_bounds.min.coords()[d].into();
75                let hi: f64 = child_bounds.max.coords()[d].into();
76                let half = (hi - lo) * 0.5;
77                loose_min[d] = C::from((lo - half) as f32);
78                loose_max[d] = C::from((hi + half) as f32);
79            }
80            let loose_bounds = BBox::new(Point::new(loose_min), Point::new(loose_max));
81            children.push(QuadNode::new_leaf(
82                child_bounds,
83                loose_bounds,
84                self.depth + 1,
85            ));
86        }
87        children
88    }
89
90    fn insert(&mut self, point: Point<C, D>, id: EntryId) {
91        #[allow(clippy::unnecessary_unwrap)]
92        if self.children.is_some() {
93            let ci = self.child_index(&point);
94            self.children.as_mut().unwrap()[ci].insert(point, id);
95            return;
96        }
97        self.entries.push((point, id));
98        if self.entries.len() > LEAF_CAPACITY && self.depth < MAX_DEPTH {
99            self.split();
100        }
101    }
102
103    fn split(&mut self) {
104        let mut children = self.make_children();
105        let entries: Vec<(Point<C, D>, EntryId)> = self.entries.drain(..).collect();
106        for (pt, id) in entries {
107            let ci = self.child_index(&pt);
108            children[ci].entries.push((pt, id));
109        }
110        self.children = Some(children);
111    }
112
113    fn remove(&mut self, id: EntryId) -> bool {
114        if let Some(ref mut children) = self.children {
115            for child in children.iter_mut() {
116                if child.remove(id) {
117                    return true;
118                }
119            }
120            return false;
121        }
122        if let Some(pos) = self.entries.iter().position(|(_, eid)| *eid == id) {
123            self.entries.swap_remove(pos);
124            return true;
125        }
126        false
127    }
128
129    fn range_query(&self, bbox: &BBox<C, D>, out: &mut Vec<EntryId>) {
130        if !self.loose_bounds.intersects(bbox) {
131            return;
132        }
133        if let Some(ref children) = self.children {
134            for child in children {
135                child.range_query(bbox, out);
136            }
137            return;
138        }
139        for (pt, id) in &self.entries {
140            if bbox.contains_point(pt) {
141                out.push(*id);
142            }
143        }
144    }
145
146    fn collect_all(&self, out: &mut Vec<(Point<C, D>, EntryId)>) {
147        if let Some(ref children) = self.children {
148            for child in children {
149                child.collect_all(out);
150            }
151            return;
152        }
153        out.extend_from_slice(&self.entries);
154    }
155
156    fn node_count(&self) -> usize {
157        match &self.children {
158            None => 1,
159            Some(children) => 1 + children.iter().map(|c| c.node_count()).sum::<usize>(),
160        }
161    }
162
163    fn max_depth(&self) -> usize {
164        match &self.children {
165            None => self.depth,
166            Some(children) => children
167                .iter()
168                .map(|c| c.max_depth())
169                .max()
170                .unwrap_or(self.depth),
171        }
172    }
173}
174
175/// Loose 2^D-tree spatial index.
176///
177/// Generalises the 2D quadtree (4 children), 3D octree (8 children), and 4D
178/// hexadecatree (16 children) under a single generic struct parameterised by
179/// `D`. The policy engine excludes this backend from migration candidates when
180/// `D > 4`.
181pub struct Quadtree<T, C, const D: usize> {
182    root: QuadNode<C, D>,
183    id_to_payload: HashMap<u64, T>,
184    id_to_point: HashMap<u64, Point<C, D>>,
185    len: usize,
186    next_id: u64,
187}
188
189impl<T, C: CoordType, const D: usize> Quadtree<T, C, D> {
190    /// Create an empty Quadtree covering the unit hypercube `[0, 1]^D`.
191    pub fn new() -> Self {
192        Self::with_bounds(
193            Point::new([C::zero(); D]),
194            Point::new([C::from(1.0_f32); D]),
195        )
196    }
197
198    /// Create an empty Quadtree with explicit root bounds.
199    pub fn with_bounds(min: Point<C, D>, max: Point<C, D>) -> Self {
200        let bounds = BBox::new(min, max);
201        let mut loose_min = [C::zero(); D];
202        let mut loose_max = [C::zero(); D];
203        for d in 0..D {
204            let lo: f64 = min.coords()[d].into();
205            let hi: f64 = max.coords()[d].into();
206            let half = (hi - lo) * 0.5;
207            loose_min[d] = C::from((lo - half) as f32);
208            loose_max[d] = C::from((hi + half) as f32);
209        }
210        let loose_bounds = BBox::new(Point::new(loose_min), Point::new(loose_max));
211        Self {
212            root: QuadNode::new_leaf(bounds, loose_bounds, 0),
213            id_to_payload: HashMap::new(),
214            id_to_point: HashMap::new(),
215            len: 0,
216            next_id: 0,
217        }
218    }
219
220    fn alloc_id(&mut self) -> EntryId {
221        let id = EntryId(self.next_id);
222        self.next_id += 1;
223        id
224    }
225
226    fn ensure_bounds(&mut self, point: &Point<C, D>) {
227        let mut needs_expand = false;
228        for d in 0..D {
229            let v: f64 = point.coords()[d].into();
230            let lo: f64 = self.root.bounds.min.coords()[d].into();
231            let hi: f64 = self.root.bounds.max.coords()[d].into();
232            if v < lo || v > hi {
233                needs_expand = true;
234                break;
235            }
236        }
237        if !needs_expand {
238            return;
239        }
240        let mut all: Vec<(Point<C, D>, EntryId)> = Vec::with_capacity(self.len);
241        self.root.collect_all(&mut all);
242
243        let mut new_min = [C::zero(); D];
244        let mut new_max = [C::zero(); D];
245        for d in 0..D {
246            let lo: f64 = self.root.bounds.min.coords()[d].into();
247            let hi: f64 = self.root.bounds.max.coords()[d].into();
248            let v: f64 = point.coords()[d].into();
249            let span = (hi - lo).max(1.0);
250            let new_lo = lo.min(v - span * 0.1);
251            let new_hi = hi.max(v + span * 0.1);
252            new_min[d] = C::from(new_lo as f32);
253            new_max[d] = C::from(new_hi as f32);
254        }
255
256        let mut new_tree = Quadtree::with_bounds(Point::new(new_min), Point::new(new_max));
257        new_tree.next_id = self.next_id;
258        for (pt, id) in all {
259            new_tree.root.insert(pt, id);
260            new_tree.id_to_point.insert(id.0, pt);
261        }
262        new_tree.id_to_payload = std::mem::take(&mut self.id_to_payload);
263        new_tree.len = self.len;
264        *self = new_tree;
265    }
266
267    pub fn insert_entry(&mut self, point: Point<C, D>, payload: T) -> EntryId {
268        self.ensure_bounds(&point);
269        let id = self.alloc_id();
270        self.id_to_point.insert(id.0, point);
271        self.id_to_payload.insert(id.0, payload);
272        self.root.insert(point, id);
273        self.len += 1;
274        id
275    }
276
277    pub fn remove_entry(&mut self, id: EntryId) -> Option<T> {
278        self.id_to_point.remove(&id.0)?;
279        let payload = self.id_to_payload.remove(&id.0)?;
280        self.root.remove(id);
281        self.len -= 1;
282        Some(payload)
283    }
284
285    pub fn range_query_impl<'a>(&'a self, bbox: &BBox<C, D>) -> Vec<(EntryId, &'a T)> {
286        let mut ids = Vec::new();
287        self.root.range_query(bbox, &mut ids);
288        ids.into_iter()
289            .filter_map(|id| self.id_to_payload.get(&id.0).map(|p| (id, p)))
290            .collect()
291    }
292
293    pub fn knn_query_impl<'a>(
294        &'a self,
295        point: &Point<C, D>,
296        k: usize,
297    ) -> Vec<(f64, EntryId, &'a T)> {
298        if k == 0 {
299            return Vec::new();
300        }
301        let mut all: Vec<(Point<C, D>, EntryId)> = Vec::with_capacity(self.len);
302        self.root.collect_all(&mut all);
303        let mut results: Vec<(f64, EntryId, &'a T)> = all
304            .into_iter()
305            .filter_map(|(p, id)| {
306                self.id_to_payload.get(&id.0).map(|payload| {
307                    let dist = point_dist_sq(&p, point).sqrt();
308                    (dist, id, payload)
309                })
310            })
311            .collect();
312        results.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
313        results.truncate(k);
314        results
315    }
316
317    fn collect_all_with_payload(&self) -> Vec<(Point<C, D>, EntryId, &T)> {
318        let mut raw: Vec<(Point<C, D>, EntryId)> = Vec::with_capacity(self.len);
319        self.root.collect_all(&mut raw);
320        raw.into_iter()
321            .filter_map(|(p, id)| {
322                self.id_to_payload
323                    .get(&id.0)
324                    .map(|payload| (p, id, payload))
325            })
326            .collect()
327    }
328
329    /// Return the total number of nodes in the tree.
330    pub fn node_count(&self) -> usize {
331        self.root.node_count()
332    }
333
334    /// Return the maximum depth of the tree.
335    pub fn max_depth(&self) -> usize {
336        self.root.max_depth()
337    }
338}
339
340fn point_dist_sq<C: CoordType, const D: usize>(a: &Point<C, D>, b: &Point<C, D>) -> f64 {
341    let mut sum = 0.0_f64;
342    for d in 0..D {
343        let da: f64 = a.coords()[d].into();
344        let db: f64 = b.coords()[d].into();
345        let diff = da - db;
346        sum += diff * diff;
347    }
348    sum
349}
350
351impl<T, C: CoordType, const D: usize> Default for Quadtree<T, C, D> {
352    fn default() -> Self {
353        Self::new()
354    }
355}
356
357impl<T: Send + Sync + 'static, C: CoordType, const D: usize> SpatialBackend<T, C, D>
358    for Quadtree<T, C, D>
359{
360    fn insert(&mut self, point: Point<C, D>, payload: T) -> EntryId {
361        self.insert_entry(point, payload)
362    }
363
364    fn remove(&mut self, id: EntryId) -> Option<T> {
365        self.remove_entry(id)
366    }
367
368    fn range_query(&self, bbox: &BBox<C, D>) -> Vec<(EntryId, &T)> {
369        self.range_query_impl(bbox)
370    }
371
372    fn knn_query(&self, point: &Point<C, D>, k: usize) -> Vec<(f64, EntryId, &T)> {
373        self.knn_query_impl(point, k)
374    }
375
376    fn spatial_join(&self, other: &dyn SpatialBackend<T, C, D>) -> Vec<(EntryId, EntryId)> {
377        let self_entries = self.collect_all_with_payload();
378        let other_entries = other.all_entries();
379        let mut pairs = Vec::new();
380        for (pa, id_a, _) in &self_entries {
381            let bbox_a = BBox::new(*pa, *pa);
382            for (pb, id_b, _) in &other_entries {
383                if bbox_a.intersects(&BBox::new(*pb, *pb)) {
384                    pairs.push((*id_a, *id_b));
385                }
386            }
387        }
388        pairs
389    }
390
391    fn bulk_load(entries: Vec<(Point<C, D>, T)>) -> Self {
392        if entries.is_empty() {
393            return Self::new();
394        }
395        let mut min_c = *entries[0].0.coords();
396        let mut max_c = *entries[0].0.coords();
397        for (p, _) in &entries {
398            for d in 0..D {
399                let v = p.coords()[d];
400                if v < min_c[d] {
401                    min_c[d] = v;
402                }
403                if v > max_c[d] {
404                    max_c[d] = v;
405                }
406            }
407        }
408        let mut padded_min = [C::zero(); D];
409        let mut padded_max = [C::zero(); D];
410        for d in 0..D {
411            let lo: f64 = min_c[d].into();
412            let hi: f64 = max_c[d].into();
413            let margin = ((hi - lo) * 0.01).max(1.0);
414            padded_min[d] = C::from((lo - margin) as f32);
415            padded_max[d] = C::from((hi + margin) as f32);
416        }
417        let mut tree = Self::with_bounds(Point::new(padded_min), Point::new(padded_max));
418        for (point, payload) in entries {
419            tree.insert_entry(point, payload);
420        }
421        tree
422    }
423
424    fn len(&self) -> usize {
425        self.len
426    }
427
428    fn kind(&self) -> BackendKind {
429        BackendKind::Quadtree
430    }
431
432    fn all_entries(&self) -> Vec<(Point<C, D>, EntryId, &T)> {
433        self.collect_all_with_payload()
434    }
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440    use proptest::prelude::*;
441
442    struct Lcg(u64);
443    impl Lcg {
444        fn new(seed: u64) -> Self {
445            Self(seed)
446        }
447        fn next_f64(&mut self) -> f64 {
448            self.0 = self
449                .0
450                .wrapping_mul(6_364_136_223_846_793_005)
451                .wrapping_add(1_442_695_040_888_963_407);
452            (self.0 >> 11) as f64 / (1u64 << 53) as f64
453        }
454    }
455
456    fn rand_pts<const D: usize>(n: usize, seed: u64) -> Vec<Point<f64, D>> {
457        let mut rng = Lcg::new(seed);
458        (0..n)
459            .map(|_| {
460                let coords: [f64; D] = std::array::from_fn(|_| rng.next_f64() * 1000.0);
461                Point::new(coords)
462            })
463            .collect()
464    }
465
466    fn brute_range<C: CoordType, const D: usize>(
467        pts: &[(Point<C, D>, EntryId)],
468        bbox: &BBox<C, D>,
469    ) -> Vec<EntryId> {
470        let mut ids: Vec<EntryId> = pts
471            .iter()
472            .filter(|(p, _)| bbox.contains_point(p))
473            .map(|(_, id)| *id)
474            .collect();
475        ids.sort_by_key(|id| id.0);
476        ids
477    }
478
479    #[test]
480    fn children_count_d2() {
481        let pts = rand_pts::<2>(200, 1);
482        let mut tree = Quadtree::<usize, f64, 2>::bulk_load(
483            pts.into_iter().enumerate().map(|(i, p)| (p, i)).collect(),
484        );
485        for i in 200..300usize {
486            tree.insert(rand_pts::<2>(1, i as u64 + 9999)[0], i);
487        }
488        assert!(tree.root.children.is_some());
489        assert_eq!(tree.root.children.as_ref().unwrap().len(), 1 << 2);
490    }
491
492    #[test]
493    fn children_count_d3() {
494        let pts = rand_pts::<3>(200, 2);
495        let mut tree = Quadtree::<usize, f64, 3>::bulk_load(
496            pts.into_iter().enumerate().map(|(i, p)| (p, i)).collect(),
497        );
498        for i in 200..300usize {
499            tree.insert(rand_pts::<3>(1, i as u64 + 8888)[0], i);
500        }
501        assert!(tree.root.children.is_some());
502        assert_eq!(tree.root.children.as_ref().unwrap().len(), 1 << 3);
503    }
504
505    #[test]
506    fn children_count_d4() {
507        let pts = rand_pts::<4>(200, 3);
508        let mut tree = Quadtree::<usize, f64, 4>::bulk_load(
509            pts.into_iter().enumerate().map(|(i, p)| (p, i)).collect(),
510        );
511        for i in 200..300usize {
512            tree.insert(rand_pts::<4>(1, i as u64 + 7777)[0], i);
513        }
514        assert!(tree.root.children.is_some());
515        assert_eq!(tree.root.children.as_ref().unwrap().len(), 1 << 4);
516    }
517
518    #[test]
519    fn each_point_assigned_to_exactly_one_child_d2() {
520        let pts = rand_pts::<2>(300, 42);
521        let tree = Quadtree::<usize, f64, 2>::bulk_load(
522            pts.iter()
523                .cloned()
524                .enumerate()
525                .map(|(i, p)| (p, i))
526                .collect(),
527        );
528        if let Some(ref children) = tree.root.children {
529            let mut all_ids: Vec<EntryId> = Vec::new();
530            for child in children {
531                let mut child_entries: Vec<(Point<f64, 2>, EntryId)> = Vec::new();
532                child.collect_all(&mut child_entries);
533                all_ids.extend(child_entries.iter().map(|(_, id)| *id));
534            }
535            all_ids.sort_by_key(|id| id.0);
536            let before = all_ids.len();
537            all_ids.dedup();
538            assert_eq!(before, all_ids.len());
539        }
540    }
541
542    #[test]
543    fn range_query_basic() {
544        let mut tree = Quadtree::<u32, f64, 2>::new();
545        let id1 = tree.insert(Point::new([0.5, 0.5]), 1u32);
546        let id2 = tree.insert(Point::new([1.5, 1.5]), 2u32);
547        let _id3 = tree.insert(Point::new([5.0, 5.0]), 3u32);
548        let bbox = BBox::new(Point::new([0.0, 0.0]), Point::new([2.0, 2.0]));
549        let mut got: Vec<EntryId> = tree
550            .range_query(&bbox)
551            .into_iter()
552            .map(|(id, _)| id)
553            .collect();
554        got.sort_by_key(|id| id.0);
555        assert_eq!(got, vec![id1, id2]);
556    }
557
558    #[test]
559    fn remove_works() {
560        let mut tree = Quadtree::<u32, f64, 2>::new();
561        let id1 = tree.insert(Point::new([1.0, 1.0]), 10u32);
562        let id2 = tree.insert(Point::new([2.0, 2.0]), 20u32);
563        assert_eq!(tree.len(), 2);
564        assert_eq!(tree.remove(id1), Some(10u32));
565        assert_eq!(tree.len(), 1);
566        let bbox = BBox::new(Point::new([0.0, 0.0]), Point::new([3.0, 3.0]));
567        let ids: Vec<EntryId> = tree
568            .range_query(&bbox)
569            .into_iter()
570            .map(|(id, _)| id)
571            .collect();
572        assert!(!ids.contains(&id1));
573        assert!(ids.contains(&id2));
574    }
575
576    #[test]
577    fn kind_is_quadtree() {
578        assert_eq!(Quadtree::<u32, f64, 2>::new().kind(), BackendKind::Quadtree);
579    }
580
581    #[test]
582    fn range_query_vs_brute_force_2d() {
583        let pts = rand_pts::<2>(1000, 99);
584        let mut tree = Quadtree::<usize, f64, 2>::new();
585        let mut pt_ids: Vec<(Point<f64, 2>, EntryId)> = Vec::new();
586        for (i, &p) in pts.iter().enumerate() {
587            let id = tree.insert(p, i);
588            pt_ids.push((p, id));
589        }
590        let bbox = BBox::new(Point::new([0.0, 0.0]), Point::new([500.0, 500.0]));
591        let mut got: Vec<EntryId> = tree
592            .range_query(&bbox)
593            .into_iter()
594            .map(|(id, _)| id)
595            .collect();
596        got.sort_by_key(|id| id.0);
597        let expected = brute_range(&pt_ids, &bbox);
598        assert_eq!(got, expected);
599    }
600
601    fn pt2d() -> impl Strategy<Value = Point<f64, 2>> {
602        (0.0_f64..1000.0, 0.0_f64..1000.0).prop_map(|(x, y)| Point::new([x, y]))
603    }
604
605    fn bbox2d() -> impl Strategy<Value = BBox<f64, 2>> {
606        (
607            0.0_f64..900.0,
608            0.0_f64..900.0,
609            10.0_f64..200.0,
610            10.0_f64..200.0,
611        )
612            .prop_map(|(x, y, w, h)| BBox::new(Point::new([x, y]), Point::new([x + w, y + h])))
613    }
614
615    // A split node must have exactly 2^D children, and every point must appear
616    // in exactly one child.
617    proptest! {
618        #![proptest_config(proptest::test_runner::Config {
619            cases: 100,
620            ..Default::default()
621        })]
622
623        #[test]
624        fn prop_quadtree_children_count_d2(
625            pts in prop::collection::vec(pt2d(), (LEAF_CAPACITY + 1)..50),
626        ) {
627            let mut tree = Quadtree::<usize, f64, 2>::new();
628            for (i, p) in pts.iter().enumerate() {
629                tree.insert(*p, i);
630            }
631            if let Some(ref children) = tree.root.children {
632                prop_assert_eq!(children.len(), 1 << 2usize);
633                for child in children {
634                    if let Some(ref gc) = child.children {
635                        prop_assert_eq!(gc.len(), 1 << 2usize);
636                    }
637                }
638            }
639            let mut raw: Vec<(Point<f64, 2>, EntryId)> = Vec::new();
640            tree.root.collect_all(&mut raw);
641            let mut all_ids: Vec<EntryId> = raw.iter().map(|(_, id)| *id).collect();
642            all_ids.sort_by_key(|id| id.0);
643            let before = all_ids.len();
644            all_ids.dedup();
645            prop_assert_eq!(before, all_ids.len());
646            prop_assert_eq!(all_ids.len(), pts.len());
647        }
648    }
649
650    // Insert-Remove Round Trip
651    proptest! {
652        #![proptest_config(proptest::test_runner::Config {
653            cases: 100,
654            ..Default::default()
655        })]
656
657        #[test]
658        fn prop_insert_remove_round_trip_quadtree(
659            pts in prop::collection::vec(pt2d(), 1..50),
660            remove_indices in prop::collection::vec(0usize..50, 0..25),
661        ) {
662            let mut tree = Quadtree::<usize, f64, 2>::new();
663            let mut inserted: Vec<(Point<f64, 2>, EntryId)> = Vec::new();
664            for (i, &p) in pts.iter().enumerate() {
665                let id = tree.insert(p, i);
666                inserted.push((p, id));
667            }
668            let mut removed_ids: Vec<EntryId> = Vec::new();
669            for &ri in &remove_indices {
670                let idx = ri % inserted.len();
671                let (_, id) = inserted[idx];
672                if !removed_ids.contains(&id) {
673                    let result = tree.remove(id);
674                    prop_assert!(result.is_some(), "remove returned None for inserted id");
675                    removed_ids.push(id);
676                }
677            }
678            let full_bbox = BBox::new(Point::new([-1.0e9, -1.0e9]), Point::new([1.0e9, 1.0e9]));
679            let remaining_ids: Vec<EntryId> = tree.range_query(&full_bbox)
680                .into_iter()
681                .map(|(id, _)| id)
682                .collect();
683            for &removed_id in &removed_ids {
684                prop_assert!(
685                    !remaining_ids.contains(&removed_id),
686                    "removed entry {:?} still appears in range query",
687                    removed_id
688                );
689            }
690            let expected_len = inserted.len() - removed_ids.len();
691            prop_assert_eq!(tree.len(), expected_len);
692        }
693    }
694
695    // For any dataset and bbox, range_query result must equal brute-force scan.
696    proptest! {
697        #![proptest_config(proptest::test_runner::Config {
698            cases: 100,
699            ..Default::default()
700        })]
701
702        #[test]
703        fn prop_range_query_oracle_quadtree(
704            pts in prop::collection::vec(pt2d(), 1..100),
705            bbox in bbox2d(),
706        ) {
707            let mut tree = Quadtree::<usize, f64, 2>::new();
708            let mut pt_ids: Vec<(Point<f64, 2>, EntryId)> = Vec::new();
709            for (i, p) in pts.iter().enumerate() {
710                let id = tree.insert(*p, i);
711                pt_ids.push((*p, id));
712            }
713            let mut got: Vec<EntryId> =
714                tree.range_query(&bbox).into_iter().map(|(id, _)| id).collect();
715            got.sort_by_key(|id| id.0);
716            let expected = brute_range(&pt_ids, &bbox);
717            prop_assert_eq!(got, expected);
718        }
719    }
720}