Skip to main content

geometry_rtree/
split.rs

1//! Node-split strategies — the type parameter that governs tree shape.
2//!
3//! Mirrors `boost/geometry/index/parameters.hpp` and the
4//! `index/detail/rtree/{linear,quadratic,rstar}/redistribute_elements.hpp`
5//! family. When a node overflows, the split strategy decides how to
6//! partition its children into two nodes. Boost exposes this as a
7//! template parameter; the port uses a sealed [`SplitParameters`] trait
8//! carried on `Rtree<T, Params>`.
9//!
10//! Two strategies ship: [`Quadratic`] (the default — Boost's textbook
11//! split, good all-round trees) and [`Linear`] (cheaper inserts, looser
12//! trees). R\* is a future opt-in; the trait surface already admits it.
13
14use alloc::vec::Vec;
15
16use crate::bounds::{Bounds, union_all};
17
18/// How a full node is partitioned when it overflows.
19///
20/// Mirrors `index::parameters` + the split policy
21/// (`index/parameters.hpp`). `MAX` is the node capacity; `MIN` the
22/// minimum fill each half must reach. The `split` method takes the
23/// overflowing children (as `(Bounds, index)` pairs, so the strategy is
24/// payload-agnostic) and returns the two groups by index.
25pub trait SplitParameters {
26    /// Maximum children per node before it must split.
27    const MAX: usize;
28    /// Minimum children each node must keep after a split.
29    const MIN: usize;
30
31    /// Partition `entries` (a bounds-per-child list) into two groups of
32    /// indices, each of size at least [`Self::MIN`].
33    fn split(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>);
34}
35
36/// Quadratic split — Boost's textbook default.
37///
38/// `O(n²)`: pick the two children whose combined bounding box wastes the
39/// most area as the seeds of the two groups, then assign each remaining
40/// child to whichever group's box it enlarges least. Mirrors
41/// `index/detail/rtree/quadratic/redistribute_elements.hpp`.
42#[derive(Debug, Clone, Copy, Default)]
43pub struct Quadratic<const MAX: usize = 8, const MIN: usize = 3>;
44
45impl<const MAX: usize, const MIN: usize> SplitParameters for Quadratic<MAX, MIN> {
46    const MAX: usize = MAX;
47    const MIN: usize = MIN;
48
49    fn split(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>) {
50        let n = entries.len();
51        let (s1, s2) = quadratic_seeds(entries);
52
53        let mut g1 = Vec::from([s1]);
54        let mut g2 = Vec::from([s2]);
55        let mut b1 = entries[s1];
56        let mut b2 = entries[s2];
57
58        let mut remaining: Vec<usize> = (0..n).filter(|&i| i != s1 && i != s2).collect();
59
60        while let Some(idx) = remaining.pop() {
61            // Force the smaller group to fill if the rest are needed to
62            // reach MIN.
63            if g1.len() + remaining.len() + 1 == MIN {
64                g1.push(idx);
65                b1 = b1.union(&entries[idx]);
66                continue;
67            }
68            if g2.len() + remaining.len() + 1 == MIN {
69                g2.push(idx);
70                b2 = b2.union(&entries[idx]);
71                continue;
72            }
73            // Otherwise assign to the group whose box enlarges least.
74            let e1 = b1.enlargement(&entries[idx]);
75            let e2 = b2.enlargement(&entries[idx]);
76            if e1 <= e2 {
77                g1.push(idx);
78                b1 = b1.union(&entries[idx]);
79            } else {
80                g2.push(idx);
81                b2 = b2.union(&entries[idx]);
82            }
83        }
84
85        (g1, g2)
86    }
87}
88
89/// Linear split — cheap inserts, looser trees.
90///
91/// `O(n)`: pick the two children furthest apart along the axis of
92/// greatest spread as seeds, then distribute the rest greedily by
93/// least enlargement. Mirrors
94/// `index/detail/rtree/linear/redistribute_elements.hpp`.
95#[derive(Debug, Clone, Copy, Default)]
96pub struct Linear<const MAX: usize = 8, const MIN: usize = 3>;
97
98impl<const MAX: usize, const MIN: usize> SplitParameters for Linear<MAX, MIN> {
99    const MAX: usize = MAX;
100    const MIN: usize = MIN;
101
102    fn split(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>) {
103        let n = entries.len();
104        let (s1, s2) = linear_seeds(entries);
105
106        let mut g1 = Vec::from([s1]);
107        let mut g2 = Vec::from([s2]);
108        let mut b1 = entries[s1];
109        let mut b2 = entries[s2];
110
111        let mut remaining: Vec<usize> = (0..n).filter(|&i| i != s1 && i != s2).collect();
112        while let Some(idx) = remaining.pop() {
113            if g1.len() + remaining.len() + 1 == MIN {
114                g1.push(idx);
115                b1 = b1.union(&entries[idx]);
116                continue;
117            }
118            if g2.len() + remaining.len() + 1 == MIN {
119                g2.push(idx);
120                b2 = b2.union(&entries[idx]);
121                continue;
122            }
123            let e1 = b1.enlargement(&entries[idx]);
124            let e2 = b2.enlargement(&entries[idx]);
125            if e1 <= e2 {
126                g1.push(idx);
127                b1 = b1.union(&entries[idx]);
128            } else {
129                g2.push(idx);
130                b2 = b2.union(&entries[idx]);
131            }
132        }
133        (g1, g2)
134    }
135}
136
137/// The pair of children whose combined box wastes the most area — the
138/// quadratic-split seeds (`PickSeeds` in Boost's terms).
139fn quadratic_seeds(entries: &[Bounds]) -> (usize, usize) {
140    let mut worst = (0, 1, f64::NEG_INFINITY);
141    for i in 0..entries.len() {
142        for j in (i + 1)..entries.len() {
143            let combined = entries[i].union(&entries[j]).area();
144            let waste = combined - entries[i].area() - entries[j].area();
145            if waste > worst.2 {
146                worst = (i, j, waste);
147            }
148        }
149    }
150    (worst.0, worst.1)
151}
152
153/// The two children furthest apart along the widest axis — the
154/// linear-split seeds.
155fn linear_seeds(entries: &[Bounds]) -> (usize, usize) {
156    let all = union_all(entries);
157    let width = [all.max[0] - all.min[0], all.max[1] - all.min[1]];
158    let axis = usize::from(width[1] > width[0]);
159
160    // Extreme children: lowest high-side and highest low-side along axis.
161    let mut lo_idx = 0;
162    let mut hi_idx = 0;
163    let mut max_low = f64::NEG_INFINITY;
164    let mut min_high = f64::INFINITY;
165    for (i, b) in entries.iter().enumerate() {
166        if b.min[axis] > max_low {
167            max_low = b.min[axis];
168            hi_idx = i;
169        }
170        if b.max[axis] < min_high {
171            min_high = b.max[axis];
172            lo_idx = i;
173        }
174    }
175    if lo_idx == hi_idx {
176        // Degenerate: fall back to the first two.
177        (0, usize::from(entries.len() > 1))
178    } else {
179        (lo_idx, hi_idx)
180    }
181}
182
183#[cfg(test)]
184#[allow(
185    clippy::cast_precision_loss,
186    reason = "small test indices convert exactly to f64"
187)]
188mod tests {
189    use super::{Linear, Quadratic, SplitParameters};
190    use crate::bounds::Bounds;
191
192    fn line_of_boxes(n: usize) -> Vec<Bounds> {
193        (0..n)
194            .map(|i| Bounds::point([i as f64, 0.0]))
195            .collect::<Vec<_>>()
196    }
197
198    #[test]
199    fn quadratic_splits_into_two_min_sized_groups() {
200        let entries = line_of_boxes(9);
201        let (g1, g2) = <Quadratic<8, 3>>::split(&entries);
202        assert!(g1.len() >= 3 && g2.len() >= 3);
203        assert_eq!(g1.len() + g2.len(), 9);
204    }
205
206    #[test]
207    fn linear_splits_into_two_min_sized_groups() {
208        let entries = line_of_boxes(9);
209        let (g1, g2) = <Linear<8, 3>>::split(&entries);
210        assert!(g1.len() >= 3 && g2.len() >= 3);
211        assert_eq!(g1.len() + g2.len(), 9);
212    }
213
214    #[test]
215    fn every_index_assigned_exactly_once() {
216        let entries = line_of_boxes(9);
217        let (mut g1, g2) = <Quadratic<8, 3>>::split(&entries);
218        g1.extend(g2);
219        g1.sort_unstable();
220        assert_eq!(g1, (0..9).collect::<Vec<_>>());
221    }
222}