Skip to content

Commit 77abf81

Browse files
committed
feat: add dtype-specific default max-depths + require max depth for inner quadtree creation
1 parent 8e8f184 commit 77abf81

15 files changed

+157
-94
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "fastquadtree"
3-
version = "1.4.2"
3+
version = "1.4.3"
44
edition = "2021"
55

66
[lib]

pysrc/fastquadtree/_base_quadtree.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,5 +525,19 @@ def count_items(self) -> int:
525525
"""
526526
return self._native.count_items()
527527

528+
def get_inner_max_depth(self) -> int:
529+
"""
530+
Return the maximum depth of the quadtree core.
531+
Useful if you let the core chose the default max depth based on dtype
532+
by constructing with max_depth=None.
533+
534+
Example:
535+
```python
536+
depth = qt.get_inner_max_depth()
537+
assert isinstance(depth, int)
538+
```
539+
"""
540+
return self._native.get_max_depth()
541+
528542
def __len__(self) -> int:
529543
return self._count

src/lib.rs

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,25 @@ fn rect_to_tuple<T: Coord + Copy>(r: Rect<T>) -> (T, T, T, T) {
2525
(r.min_x, r.min_y, r.max_x, r.max_y)
2626
}
2727

28+
fn default_max_depth_for<T: 'static>() -> usize {
29+
// Caps aligned with meaningful resolution per dtype.
30+
// f32: 24 mantissa bits -> deeper splits stop helping.
31+
// f64: 53 mantissa bits.
32+
// ints: cap at bit width, beyond that subdivision degenerates anyway.
33+
if TypeId::of::<T>() == TypeId::of::<f32>() {
34+
24
35+
} else if TypeId::of::<T>() == TypeId::of::<f64>() {
36+
53
37+
} else if TypeId::of::<T>() == TypeId::of::<i32>() {
38+
32
39+
} else if TypeId::of::<T>() == TypeId::of::<i64>() {
40+
64
41+
} else {
42+
32
43+
}
44+
}
45+
46+
2847
// Reusable core for point QuadTrees
2948
macro_rules! define_point_quadtree_pyclass {
3049
($t:ty, $rs_name:ident, $py_name:literal) => {
@@ -41,8 +60,12 @@ macro_rules! define_point_quadtree_pyclass {
4160
let (min_x, min_y, max_x, max_y) = bounds;
4261
let rect = Rect { min_x, min_y, max_x, max_y };
4362
let inner = match max_depth {
44-
Some(d) => QuadTree::new_with_max_depth(rect, capacity, d),
45-
None => QuadTree::new(rect, capacity),
63+
Some(d) => QuadTree::new(rect, capacity, d),
64+
None => QuadTree::new(
65+
rect,
66+
capacity,
67+
default_max_depth_for::<$t>(),
68+
),
4669
};
4770
Self { inner }
4871
}
@@ -265,6 +288,10 @@ macro_rules! define_point_quadtree_pyclass {
265288
pub fn count_items(&self) -> usize {
266289
self.inner.count_items()
267290
}
291+
292+
pub fn get_max_depth(&self) -> usize {
293+
self.inner.get_max_depth()
294+
}
268295
}
269296
};
270297
}
@@ -285,8 +312,12 @@ macro_rules! define_rect_quadtree_pyclass {
285312
let (min_x, min_y, max_x, max_y) = bounds;
286313
let rect = Rect { min_x, min_y, max_x, max_y };
287314
let inner = match max_depth {
288-
Some(d) => RectQuadTree::new_with_max_depth(rect, capacity, d),
289-
None => RectQuadTree::new(rect, capacity),
315+
Some(d) => RectQuadTree::new(rect, capacity, d),
316+
None => RectQuadTree::new(
317+
rect,
318+
capacity,
319+
default_max_depth_for::<$t>(),
320+
),
290321
};
291322
Self { inner }
292323
}
@@ -478,6 +509,10 @@ macro_rules! define_rect_quadtree_pyclass {
478509
pub fn count_items(&self) -> usize {
479510
self.inner.count_items()
480511
}
512+
513+
pub fn get_max_depth(&self) -> usize {
514+
self.inner.get_max_depth()
515+
}
481516
}
482517
};
483518
}

src/quadtree.rs

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,7 @@ fn child_index_for_point<T: Coord>(b: &Rect<T>, p: &Point<T>) -> usize {
3737
}
3838

3939
impl<T: Coord> QuadTree<T> {
40-
pub fn new(boundary: Rect<T>, capacity: usize) -> Self {
41-
QuadTree {
42-
boundary,
43-
items: Vec::with_capacity(capacity),
44-
capacity,
45-
children: None,
46-
depth: 0,
47-
max_depth: usize::MAX,
48-
}
49-
}
50-
51-
pub fn new_with_max_depth(boundary: Rect<T>, capacity: usize, max_depth: usize) -> Self {
40+
pub fn new(boundary: Rect<T>, capacity: usize, max_depth: usize) -> Self {
5241
QuadTree {
5342
boundary,
5443
items: Vec::with_capacity(capacity),
@@ -392,5 +381,9 @@ impl<T: Coord> QuadTree<T> {
392381
count
393382
}
394383

384+
pub fn get_max_depth(&self) -> usize {
385+
self.max_depth
386+
}
387+
395388

396389
}

src/rect_quadtree.rs

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,7 @@ fn rects_touch_or_intersect<T: Coord>(a: &Rect<T>, b: &Rect<T>) -> bool {
5252
}
5353

5454
impl<T: Coord> RectQuadTree<T> {
55-
pub fn new(boundary: Rect<T>, capacity: usize) -> Self {
56-
RectQuadTree {
57-
boundary,
58-
items: Vec::with_capacity(capacity),
59-
capacity,
60-
children: None,
61-
depth: 0,
62-
max_depth: usize::MAX,
63-
}
64-
}
65-
66-
pub fn new_with_max_depth(boundary: Rect<T>, capacity: usize, max_depth: usize) -> Self {
55+
pub fn new(boundary: Rect<T>, capacity: usize, max_depth: usize) -> Self {
6756
RectQuadTree {
6857
boundary,
6958
items: Vec::with_capacity(capacity),
@@ -301,4 +290,8 @@ impl<T: Coord> RectQuadTree<T> {
301290
}
302291
}
303292
}
293+
294+
pub fn get_max_depth(&self) -> usize {
295+
self.max_depth
296+
}
304297
}

tests/insertions.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ fn rect_contains_half_open() {
2727

2828
#[test]
2929
fn insert_inside_leaf_until_capacity() {
30-
let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 2);
30+
let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 2, 8);
3131

3232
let ok1 = qt.insert(Item { id: 1, point: pt(10.0, 10.0) });
3333
let ok2 = qt.insert(Item { id: 2, point: pt(20.0, 20.0) });
@@ -37,7 +37,7 @@ fn insert_inside_leaf_until_capacity() {
3737

3838
#[test]
3939
fn insert_outside_returns_false() {
40-
let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 2);
40+
let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 2, 8);
4141

4242
assert!(!qt.insert(Item { id: 1, point: pt(-1.0, 50.0) }));
4343
assert!(!qt.insert(Item { id: 2, point: pt(50.0, 101.0) }));
@@ -46,7 +46,7 @@ fn insert_outside_returns_false() {
4646
#[test]
4747
fn split_then_midline_inserts_succeed() {
4848
// capacity 1 forces a split on the second insert
49-
let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 1);
49+
let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 1, 8);
5050

5151
// first insert goes to parent as a leaf
5252
assert!(qt.insert(Item { id: 1, point: pt(25.0, 25.0) }));
@@ -66,7 +66,7 @@ fn split_then_midline_inserts_succeed() {
6666

6767
#[test]
6868
fn many_inserts_all_succeed_when_inside() {
69-
let mut qt = QuadTree::new(r(0.0, 0.0, 1000.0, 1000.0), 4);
69+
let mut qt = QuadTree::new(r(0.0, 0.0, 1000.0, 1000.0), 4, 8);
7070

7171
// generate a grid of interior points
7272
let mut ok = 0usize;
@@ -84,7 +84,7 @@ fn many_inserts_all_succeed_when_inside() {
8484

8585
#[test]
8686
fn boundary_points_respect_half_open_rule() {
87-
let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 2);
87+
let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 2, 8);
8888

8989
// Allowed: on min edges
9090
assert!(qt.insert(Item { id: 1, point: pt(0.0, 0.0) }));

tests/nearest_neighbor.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,28 @@ fn dist2(a: Point<f32>, b: Point<f32>) -> f32 {
1818
#[test]
1919
fn nearest_neighbor_zero_poins() {
2020
// keep user's original test name
21-
let qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 4);
21+
let qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 4, 8);
2222
assert!(qt.nearest_neighbor(pt(10.0, 10.0)).is_none());
2323
}
2424

2525
#[test]
2626
fn nearest_neighbor_one_point() {
27-
let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 4);
27+
let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 4, 8);
2828
qt.insert(Item { id: 1, point: pt(50.0, 50.0) });
2929
assert_eq!(qt.nearest_neighbor(pt(10.0, 10.0)).unwrap().id, 1);
3030
}
3131

3232
#[test]
3333
fn nearest_neighbor_two_points() {
34-
let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 4);
34+
let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 4, 8);
3535
qt.insert(Item { id: 1, point: pt(50.0, 50.0) });
3636
qt.insert(Item { id: 2, point: pt(60.0, 60.0) });
3737
assert_eq!(qt.nearest_neighbor(pt(10.0, 10.0)).unwrap().id, 1);
3838
}
3939

4040
#[test]
4141
fn nn_exact_hit_distance_zero() {
42-
let mut qt = QuadTree::new(r(0.0, 0.0, 20.0, 20.0), 8);
42+
let mut qt = QuadTree::new(r(0.0, 0.0, 20.0, 20.0), 8, 8);
4343
qt.insert(Item { id: 7, point: pt(10.0, 10.0) });
4444
qt.insert(Item { id: 8, point: pt(3.0, 4.0) });
4545
let q = pt(10.0, 10.0);
@@ -56,7 +56,7 @@ fn nn_exact_hit_distance_zero() {
5656
#[test]
5757
fn knn_basic_ordering_no_split() {
5858
// capacity high so we do not split
59-
let mut qt = QuadTree::new(r(0.0, 0.0, 40.0, 40.0), 16);
59+
let mut qt = QuadTree::new(r(0.0, 0.0, 40.0, 40.0), 16, 8);
6060
// unique distances from query (6, 6)
6161
qt.insert(Item { id: 1, point: pt(5.0, 5.0) }); // d2 = 2
6262
qt.insert(Item { id: 2, point: pt(6.5, 6.0) }); // d2 = 0.25
@@ -78,7 +78,7 @@ fn knn_basic_ordering_no_split() {
7878
#[test]
7979
fn knn_respects_capacity_and_split() {
8080
// capacity small so we force splits across all quadrants
81-
let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 1);
81+
let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 1, 8);
8282
qt.insert(Item { id: 1, point: pt(10.0, 10.0) });
8383
qt.insert(Item { id: 2, point: pt(90.0, 10.0) });
8484
qt.insert(Item { id: 3, point: pt(10.0, 90.0) });
@@ -98,7 +98,7 @@ fn knn_respects_capacity_and_split() {
9898

9999
#[test]
100100
fn knn_k_greater_than_len_no_duplicates() {
101-
let mut qt = QuadTree::new(r(0.0, 0.0, 10.0, 10.0), 2);
101+
let mut qt = QuadTree::new(r(0.0, 0.0, 10.0, 10.0), 2, 8);
102102
qt.insert(Item { id: 1, point: pt(1.0, 1.0) });
103103
qt.insert(Item { id: 2, point: pt(2.0, 2.0) });
104104
qt.insert(Item { id: 3, point: pt(9.0, 9.0) });
@@ -115,7 +115,7 @@ fn knn_k_greater_than_len_no_duplicates() {
115115
#[test]
116116
fn within_strictly_less_than_max_distance() {
117117
// current implementation uses d2 < max_d2, not <=
118-
let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 4);
118+
let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 4, 8);
119119
let p = pt(10.0, 10.0);
120120
qt.insert(Item { id: 1, point: pt(13.0, 14.0) }); // distance = 5.0
121121
qt.insert(Item { id: 2, point: pt(30.0, 30.0) });
@@ -133,7 +133,7 @@ fn within_strictly_less_than_max_distance() {
133133
#[test]
134134
fn equidistant_tie_returns_one_of_the_candidates() {
135135
// two points equidistant from query
136-
let mut qt = QuadTree::new(r(0.0, 0.0, 50.0, 50.0), 4);
136+
let mut qt = QuadTree::new(r(0.0, 0.0, 50.0, 50.0), 4, 8);
137137
qt.insert(Item { id: 10, point: pt(10.0, 10.0) });
138138
qt.insert(Item { id: 20, point: pt(10.0, 12.0) });
139139
let q = pt(10.0, 11.0);
@@ -145,7 +145,7 @@ fn equidistant_tie_returns_one_of_the_candidates() {
145145
fn identical_locations_two_items_pick_one_due_to_strict_lt() {
146146
// both items at the same coordinates
147147
// with current algorithm and strict <, k=2 will only return one of them
148-
let mut qt = QuadTree::new(r(0.0, 0.0, 10.0, 10.0), 4);
148+
let mut qt = QuadTree::new(r(0.0, 0.0, 10.0, 10.0), 4, 8);
149149
qt.insert(Item { id: 1, point: pt(5.0, 5.0) });
150150
qt.insert(Item { id: 2, point: pt(5.0, 5.0) });
151151

@@ -157,7 +157,7 @@ fn identical_locations_two_items_pick_one_due_to_strict_lt() {
157157

158158
#[test]
159159
fn query_far_outside_root_works() {
160-
let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 4);
160+
let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 4, 8);
161161
qt.insert(Item { id: 1, point: pt(0.0, 0.0) });
162162
qt.insert(Item { id: 2, point: pt(100.0, 100.0) });
163163
qt.insert(Item { id: 3, point: pt(100.0, 0.0) });
@@ -171,7 +171,7 @@ fn query_far_outside_root_works() {
171171

172172
#[test]
173173
fn ordering_is_by_distance_even_after_splits() {
174-
let mut qt = QuadTree::new(r(0.0, 0.0, 64.0, 64.0), 1); // force deep subdivisions
174+
let mut qt = QuadTree::new(r(0.0, 0.0, 64.0, 64.0), 1, 8); // force deep subdivisions
175175
// place a small grid of points
176176
let mut id = 1u64;
177177
for y in (4..=60).step_by(8) {
@@ -200,7 +200,7 @@ fn ordering_is_by_distance_even_after_splits() {
200200
#[test]
201201
fn midline_and_center_points_are_handled() {
202202
// insert points that lie on child midlines and the exact center
203-
let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 2);
203+
let mut qt = QuadTree::new(r(0.0, 0.0, 100.0, 100.0), 2, 8);
204204
let center = pt(50.0, 50.0);
205205
qt.insert(Item { id: 1, point: center });
206206
qt.insert(Item { id: 2, point: pt(50.0, 10.0) }); // vertical midline

0 commit comments

Comments
 (0)