use alloc::vec::Vec;
use crate::bounds::{Bounds, union_all};
pub trait SplitParameters {
const MAX: usize;
const MIN: usize;
const LEAF_MAX: usize = Self::MAX;
const LEAF_MIN: usize = Self::MIN;
const BRANCH_MAX: usize = Self::MAX;
const BRANCH_MIN: usize = Self::MIN;
const BULK_LEAF_MAX: usize = Self::LEAF_MAX;
const BULK_BRANCH_MAX: usize = Self::BRANCH_MAX;
fn split(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>);
#[must_use]
fn split_leaf(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>) {
Self::split(entries)
}
#[must_use]
fn split_branch(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>) {
Self::split(entries)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Quadratic<const MAX: usize = 32, const MIN: usize = 9>;
impl<const MAX: usize, const MIN: usize> SplitParameters for Quadratic<MAX, MIN> {
const MAX: usize = MAX;
const MIN: usize = MIN;
fn split(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>) {
quadratic_partition(entries, MIN)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct AsymmetricQuadratic<
const BRANCH_MAX: usize,
const BRANCH_MIN: usize,
const LEAF_MAX: usize,
const LEAF_MIN: usize,
>;
impl<const BRANCH_MAX: usize, const BRANCH_MIN: usize, const LEAF_MAX: usize, const LEAF_MIN: usize>
SplitParameters for AsymmetricQuadratic<BRANCH_MAX, BRANCH_MIN, LEAF_MAX, LEAF_MIN>
{
const MAX: usize = BRANCH_MAX;
const MIN: usize = BRANCH_MIN;
const LEAF_MAX: usize = LEAF_MAX;
const LEAF_MIN: usize = LEAF_MIN;
const BRANCH_MAX: usize = BRANCH_MAX;
const BRANCH_MIN: usize = BRANCH_MIN;
fn split(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>) {
quadratic_partition(entries, BRANCH_MIN)
}
fn split_leaf(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>) {
quadratic_partition(entries, LEAF_MIN)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct RStarSplit<const MAX: usize = 32, const MIN: usize = 9>;
impl<const MAX: usize, const MIN: usize> SplitParameters for RStarSplit<MAX, MIN> {
const MAX: usize = MAX;
const MIN: usize = MIN;
fn split(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>) {
rstar_partition(entries, MIN)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct AsymmetricRStarSplit<
const BRANCH_MAX: usize,
const BRANCH_MIN: usize,
const LEAF_MAX: usize,
const LEAF_MIN: usize,
const PACKED_BRANCH_MAX: usize = 0,
const PACKED_LEAF_MAX: usize = 0,
>;
impl<
const BRANCH_MAX: usize,
const BRANCH_MIN: usize,
const LEAF_MAX: usize,
const LEAF_MIN: usize,
const PACKED_BRANCH_MAX: usize,
const PACKED_LEAF_MAX: usize,
> SplitParameters
for AsymmetricRStarSplit<
BRANCH_MAX,
BRANCH_MIN,
LEAF_MAX,
LEAF_MIN,
PACKED_BRANCH_MAX,
PACKED_LEAF_MAX,
>
{
const MAX: usize = BRANCH_MAX;
const MIN: usize = BRANCH_MIN;
const LEAF_MAX: usize = LEAF_MAX;
const LEAF_MIN: usize = LEAF_MIN;
const BRANCH_MAX: usize = BRANCH_MAX;
const BRANCH_MIN: usize = BRANCH_MIN;
const BULK_LEAF_MAX: usize = if PACKED_LEAF_MAX == 0 {
LEAF_MAX
} else {
PACKED_LEAF_MAX
};
const BULK_BRANCH_MAX: usize = if PACKED_BRANCH_MAX == 0 {
BRANCH_MAX
} else {
PACKED_BRANCH_MAX
};
fn split(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>) {
rstar_partition(entries, BRANCH_MIN)
}
fn split_leaf(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>) {
rstar_partition(entries, LEAF_MIN)
}
}
fn quadratic_partition(entries: &[Bounds], min: usize) -> (Vec<usize>, Vec<usize>) {
let n = entries.len();
let (s1, s2) = quadratic_seeds(entries);
let mut g1 = Vec::from([s1]);
let mut g2 = Vec::from([s2]);
let mut b1 = entries[s1];
let mut b2 = entries[s2];
let mut remaining: Vec<usize> = (0..n).filter(|&i| i != s1 && i != s2).collect();
while !remaining.is_empty() {
if g1.len() + remaining.len() == min {
for idx in remaining.drain(..) {
g1.push(idx);
b1 = b1.union(&entries[idx]);
}
break;
}
if g2.len() + remaining.len() == min {
for idx in remaining.drain(..) {
g2.push(idx);
b2 = b2.union(&entries[idx]);
}
break;
}
let mut next_pos = 0;
let mut best_difference = f64::NEG_INFINITY;
for (pos, &idx) in remaining.iter().enumerate() {
let e1 = b1.enlargement(&entries[idx]);
let e2 = b2.enlargement(&entries[idx]);
let difference = (e1 - e2).abs();
if difference > best_difference {
next_pos = pos;
best_difference = difference;
}
}
let idx = remaining.swap_remove(next_pos);
let e1 = b1.enlargement(&entries[idx]);
let e2 = b2.enlargement(&entries[idx]);
let assign_first = match e1.total_cmp(&e2) {
core::cmp::Ordering::Less => true,
core::cmp::Ordering::Greater => false,
core::cmp::Ordering::Equal => match b1.area().total_cmp(&b2.area()) {
core::cmp::Ordering::Less => true,
core::cmp::Ordering::Greater => false,
core::cmp::Ordering::Equal => g1.len() <= g2.len(),
},
};
if assign_first {
g1.push(idx);
b1 = b1.union(&entries[idx]);
} else {
g2.push(idx);
b2 = b2.union(&entries[idx]);
}
}
(g1, g2)
}
#[allow(
clippy::float_cmp,
reason = "exact R*-split tie-break between equal overlap and area"
)]
fn rstar_partition(entries: &[Bounds], min: usize) -> (Vec<usize>, Vec<usize>) {
let mut best_axis = 0;
let mut best_margin = f64::INFINITY;
for axis in 0..2 {
let ordered = sorted_indices(entries, axis);
let suffixes = suffix_bounds(entries, &ordered);
let mut left = partition_bounds(entries, &ordered[..min]);
let mut margin = left.half_perimeter() + suffixes[min].half_perimeter();
for split in (min + 1)..=entries.len() - min {
left = left.union(&entries[ordered[split - 1]]);
margin += left.half_perimeter() + suffixes[split].half_perimeter();
}
if margin < best_margin {
best_axis = axis;
best_margin = margin;
}
}
let ordered = sorted_indices(entries, best_axis);
let suffixes = suffix_bounds(entries, &ordered);
let mut best_split = min;
let mut best_overlap = f64::INFINITY;
let mut best_area = f64::INFINITY;
let mut left = partition_bounds(entries, &ordered[..min]);
for split in min..=entries.len() - min {
if split > min {
left = left.union(&entries[ordered[split - 1]]);
}
let right = suffixes[split];
let overlap = intersection_area(&left, &right);
let area = left.area() + right.area();
if overlap < best_overlap || (overlap == best_overlap && area < best_area) {
best_split = split;
best_overlap = overlap;
best_area = area;
}
}
(
ordered[..best_split].to_vec(),
ordered[best_split..].to_vec(),
)
}
fn sorted_indices(entries: &[Bounds], axis: usize) -> Vec<usize> {
let mut indices: Vec<usize> = (0..entries.len()).collect();
indices.sort_unstable_by(|&left, &right| {
entries[left].min[axis].total_cmp(&entries[right].min[axis])
});
indices
}
fn partition_bounds(entries: &[Bounds], indices: &[usize]) -> Bounds {
let mut bounds = entries[indices[0]];
for &index in &indices[1..] {
bounds = bounds.union(&entries[index]);
}
bounds
}
fn suffix_bounds(entries: &[Bounds], indices: &[usize]) -> Vec<Bounds> {
let mut suffixes = Vec::with_capacity(indices.len());
let mut bounds = entries[*indices.last().expect("a split has at least two entries")];
suffixes.push(bounds);
for &index in indices[..indices.len() - 1].iter().rev() {
bounds = bounds.union(&entries[index]);
suffixes.push(bounds);
}
suffixes.reverse();
suffixes
}
fn intersection_area(left: &Bounds, right: &Bounds) -> f64 {
let width = left.max[0].min(right.max[0]) - left.min[0].max(right.min[0]);
let height = left.max[1].min(right.max[1]) - left.min[1].max(right.min[1]);
width.max(0.0) * height.max(0.0)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Linear<const MAX: usize = 32, const MIN: usize = 9>;
impl<const MAX: usize, const MIN: usize> SplitParameters for Linear<MAX, MIN> {
const MAX: usize = MAX;
const MIN: usize = MIN;
fn split(entries: &[Bounds]) -> (Vec<usize>, Vec<usize>) {
let n = entries.len();
let (s1, s2) = linear_seeds(entries);
let mut g1 = Vec::from([s1]);
let mut g2 = Vec::from([s2]);
let mut b1 = entries[s1];
let mut b2 = entries[s2];
let mut remaining: Vec<usize> = (0..n).filter(|&i| i != s1 && i != s2).collect();
while let Some(idx) = remaining.pop() {
if g1.len() + remaining.len() + 1 == MIN {
g1.push(idx);
b1 = b1.union(&entries[idx]);
continue;
}
if g2.len() + remaining.len() + 1 == MIN {
g2.push(idx);
b2 = b2.union(&entries[idx]);
continue;
}
let e1 = b1.enlargement(&entries[idx]);
let e2 = b2.enlargement(&entries[idx]);
if e1 <= e2 {
g1.push(idx);
b1 = b1.union(&entries[idx]);
} else {
g2.push(idx);
b2 = b2.union(&entries[idx]);
}
}
(g1, g2)
}
}
fn quadratic_seeds(entries: &[Bounds]) -> (usize, usize) {
let mut worst = (0, 1, f64::NEG_INFINITY);
for i in 0..entries.len() {
for j in (i + 1)..entries.len() {
let combined = entries[i].union(&entries[j]).area();
let waste = combined - entries[i].area() - entries[j].area();
if waste > worst.2 {
worst = (i, j, waste);
}
}
}
(worst.0, worst.1)
}
fn linear_seeds(entries: &[Bounds]) -> (usize, usize) {
let all = union_all(entries);
let width = [all.max[0] - all.min[0], all.max[1] - all.min[1]];
let axis = usize::from(width[1] > width[0]);
let mut lo_idx = 0;
let mut hi_idx = 0;
let mut max_low = f64::NEG_INFINITY;
let mut min_high = f64::INFINITY;
for (i, b) in entries.iter().enumerate() {
if b.min[axis] > max_low {
max_low = b.min[axis];
hi_idx = i;
}
if b.max[axis] < min_high {
min_high = b.max[axis];
lo_idx = i;
}
}
if lo_idx == hi_idx {
(0, usize::from(entries.len() > 1))
} else {
(lo_idx, hi_idx)
}
}
#[cfg(test)]
#[allow(
clippy::cast_precision_loss,
reason = "small test indices convert exactly to f64"
)]
mod tests {
use super::{
AsymmetricQuadratic, AsymmetricRStarSplit, Linear, Quadratic, RStarSplit, SplitParameters,
};
use crate::bounds::Bounds;
fn line_of_boxes(n: usize) -> Vec<Bounds> {
(0..n)
.map(|i| Bounds::point([i as f64, 0.0]))
.collect::<Vec<_>>()
}
#[test]
fn quadratic_splits_into_two_min_sized_groups() {
let entries = line_of_boxes(9);
let (g1, g2) = <Quadratic<8, 3>>::split(&entries);
assert!(g1.len() >= 3 && g2.len() >= 3);
assert_eq!(g1.len() + g2.len(), 9);
}
#[test]
fn linear_splits_into_two_min_sized_groups() {
let entries = line_of_boxes(9);
let (g1, g2) = <Linear<8, 3>>::split(&entries);
assert!(g1.len() >= 3 && g2.len() >= 3);
assert_eq!(g1.len() + g2.len(), 9);
}
#[test]
fn rstar_splits_into_two_min_sized_groups() {
let entries = line_of_boxes(9);
let (g1, g2) = <RStarSplit<8, 3>>::split(&entries);
assert!(g1.len() >= 3 && g2.len() >= 3);
assert_eq!(g1.len() + g2.len(), 9);
}
#[test]
fn every_index_assigned_exactly_once() {
let entries = line_of_boxes(9);
let (mut g1, g2) = <Quadratic<8, 3>>::split(&entries);
g1.extend(g2);
g1.sort_unstable();
assert_eq!(g1, (0..9).collect::<Vec<_>>());
}
#[test]
fn asymmetric_leaf_and_branch_minimums_are_independent() {
type Params = AsymmetricQuadratic<8, 3, 32, 9>;
let branch_entries = line_of_boxes(9);
let (b1, b2) = Params::split_branch(&branch_entries);
assert!(b1.len() >= Params::BRANCH_MIN && b2.len() >= Params::BRANCH_MIN);
let leaf_entries = line_of_boxes(33);
let (l1, l2) = Params::split_leaf(&leaf_entries);
assert!(l1.len() >= Params::LEAF_MIN && l2.len() >= Params::LEAF_MIN);
}
#[test]
fn asymmetric_rstar_leaf_and_branch_minimums_are_independent() {
type Params = AsymmetricRStarSplit<8, 3, 32, 9>;
let branch_entries = line_of_boxes(9);
let (b1, b2) = Params::split_branch(&branch_entries);
assert!(b1.len() >= Params::BRANCH_MIN && b2.len() >= Params::BRANCH_MIN);
let leaf_entries = line_of_boxes(33);
let (l1, l2) = Params::split_leaf(&leaf_entries);
assert!(l1.len() >= Params::LEAF_MIN && l2.len() >= Params::LEAF_MIN);
}
#[test]
fn asymmetric_rstar_bulk_capacities_are_independent_and_optional() {
type Inherited = AsymmetricRStarSplit<6, 2, 12, 4>;
type Tuned = AsymmetricRStarSplit<6, 2, 12, 4, 4, 4>;
assert_eq!(Inherited::BULK_BRANCH_MAX, 6);
assert_eq!(Inherited::BULK_LEAF_MAX, 12);
assert_eq!(Tuned::BRANCH_MAX, 6);
assert_eq!(Tuned::LEAF_MAX, 12);
assert_eq!(Tuned::BULK_BRANCH_MAX, 4);
assert_eq!(Tuned::BULK_LEAF_MAX, 4);
}
}