adds benchmark, perf parity with source crate
This commit is contained in:
parent
969c84d9cc
commit
ccb598ac1f
9 changed files with 357 additions and 455 deletions
179
Cargo.lock
generated
179
Cargo.lock
generated
|
|
@ -103,6 +103,15 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alloca"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
|
|
@ -173,6 +182,18 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anes"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
|
||||
[[package]]
|
||||
name = "anymap3"
|
||||
version = "1.0.1"
|
||||
|
|
@ -334,6 +355,7 @@ name = "autobarts"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bevy",
|
||||
"criterion",
|
||||
"dirs",
|
||||
"include_dir",
|
||||
"ordered-float",
|
||||
|
|
@ -1766,6 +1788,12 @@ dependencies = [
|
|||
"wayland-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.4"
|
||||
|
|
@ -1825,6 +1853,33 @@ dependencies = [
|
|||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"ciborium-ll",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-io"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-ll"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"half",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
|
|
@ -1836,6 +1891,31 @@ dependencies = [
|
|||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
|
||||
|
||||
[[package]]
|
||||
name = "codegen"
|
||||
version = "0.2.0"
|
||||
|
|
@ -2111,6 +2191,40 @@ dependencies = [
|
|||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d883447757bb0ee46f233e9dc22eb84d93a9508c9b868687b274fc431d886bf"
|
||||
dependencies = [
|
||||
"alloca",
|
||||
"anes",
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"itertools 0.13.0",
|
||||
"num-traits",
|
||||
"oorandom",
|
||||
"page_size",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed943f81ea2faa8dcecbbfa50164acf95d555afec96a27871663b300e387b2e4"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools 0.13.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "critical-section"
|
||||
version = "1.2.0"
|
||||
|
|
@ -2126,6 +2240,25 @@ dependencies = [
|
|||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.12"
|
||||
|
|
@ -4009,6 +4142,12 @@ version = "1.21.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
version = "11.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
|
|
@ -4043,6 +4182,16 @@ dependencies = [
|
|||
"ttf-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "page_size"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
|
|
@ -4369,6 +4518,26 @@ version = "0.6.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "read-fonts"
|
||||
version = "0.35.0"
|
||||
|
|
@ -5109,6 +5278,16 @@ dependencies = [
|
|||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinytemplate"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.10.0"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ edition = "2024"
|
|||
|
||||
[dependencies]
|
||||
bevy = { version = "0.18", default-features = false, features = ["2d"] }
|
||||
criterion = { version = "0.8.1", default-features = false, features = ["cargo_bench_support", "rayon"] }
|
||||
dirs = "6.0.0"
|
||||
include_dir = "0.7.4"
|
||||
ordered-float = "5.1.0"
|
||||
|
|
@ -19,3 +20,10 @@ opt-level = 1
|
|||
# Enable a large amount of optimization in the dev profile for dependencies.
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
|
||||
[profile.bench]
|
||||
inherits = "release"
|
||||
|
||||
[[bench]]
|
||||
name = "main"
|
||||
harness = false
|
||||
|
|
|
|||
26
benches/bench_insert_bulk.rs
Normal file
26
benches/bench_insert_bulk.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#[path = "shared.rs"]
|
||||
mod shared;
|
||||
use std::hint::black_box;
|
||||
|
||||
use autobarts::spindex::RStarTree;
|
||||
use criterion::{Criterion, criterion_group};
|
||||
use shared::*;
|
||||
|
||||
fn bench_insert_bulk_rstartree_2d(_c: &mut Criterion) {
|
||||
let points = generate_2d_data();
|
||||
let mut cc = configure_criterion();
|
||||
cc.bench_function("insert_bulk_2d_rstartree", |b| {
|
||||
b.iter_with_setup(
|
||||
|| {
|
||||
let tree = RStarTree::new(BENCH_NODE_CAPACITY);
|
||||
(tree, points.clone())
|
||||
},
|
||||
|(mut tree, points)| {
|
||||
tree.insert_bulk(points);
|
||||
black_box(());
|
||||
},
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_insert_bulk_rstartree_2d,);
|
||||
81
benches/bench_range_search.rs
Normal file
81
benches/bench_range_search.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
#[path = "shared.rs"]
|
||||
mod shared;
|
||||
use std::hint::black_box;
|
||||
|
||||
use autobarts::{geom::Point, spindex::RStarTree};
|
||||
use bevy::math::Vec2;
|
||||
use criterion::{Criterion, criterion_group};
|
||||
use shared::*;
|
||||
|
||||
const BENCH_RANGE_RADIUS: f32 = 30.0;
|
||||
|
||||
// Configure Criterion with our benchmark timeout.
|
||||
pub fn configure_criterion() -> Criterion {
|
||||
Criterion::default().measurement_time(BENCH_TIMEOUT)
|
||||
}
|
||||
|
||||
/// A generic helper function for range search benchmarks.
|
||||
///
|
||||
/// The lifetime `'a` ties the lifetime of the tree reference and the return
|
||||
/// value. The closure `search_fn` must return a value whose lifetime is at
|
||||
/// least `'a`.
|
||||
fn bench_range_search<'a, T, Q, R>(
|
||||
name: &str,
|
||||
tree: &'a T,
|
||||
query: &Q,
|
||||
search_fn: impl Fn(&'a T, &Q, f32) -> R,
|
||||
cc: &mut Criterion,
|
||||
) where
|
||||
R: 'a,
|
||||
{
|
||||
cc.bench_function(name, |b| {
|
||||
b.iter(|| {
|
||||
let res = search_fn(tree, query, BENCH_RANGE_RADIUS);
|
||||
black_box(res)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn benchmark_range_rstartree_2d(_c: &mut Criterion) {
|
||||
let points = generate_2d_data();
|
||||
let mut tree = RStarTree::new(BENCH_NODE_CAPACITY);
|
||||
|
||||
tree.insert_bulk(points.clone());
|
||||
|
||||
let mut cc = configure_criterion();
|
||||
let mut idx = 0;
|
||||
let len = points.len();
|
||||
cc.bench_function("range_rstartree_2d", |b| {
|
||||
b.iter(|| {
|
||||
let res = tree.range_search(&points[idx], BENCH_RANGE_RADIUS);
|
||||
idx = (idx + 1) % len;
|
||||
black_box(res)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn benchmark_range_bbox_rstartree_2d(_c: &mut Criterion) {
|
||||
let points = generate_2d_data();
|
||||
let mut tree = RStarTree::new(BENCH_NODE_CAPACITY);
|
||||
|
||||
tree.insert_bulk(points);
|
||||
|
||||
let query_rect = bevy::math::bounding::Aabb2d::new(
|
||||
Vec2::new(35.0 - BENCH_RANGE_RADIUS, 45.0 - BENCH_RANGE_RADIUS),
|
||||
4.0 * Vec2::splat(BENCH_RANGE_RADIUS),
|
||||
);
|
||||
let mut cc = configure_criterion();
|
||||
bench_range_search(
|
||||
"range_rstartree_bbox_2d",
|
||||
&tree,
|
||||
&query_rect,
|
||||
|t, q, _| t.range_search_bbox(q),
|
||||
&mut cc,
|
||||
);
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
benchmark_range_rstartree_2d,
|
||||
//benchmark_range_bbox_rstartree_2d,
|
||||
);
|
||||
6
benches/main.rs
Normal file
6
benches/main.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
use criterion::criterion_main;
|
||||
|
||||
mod bench_insert_bulk;
|
||||
mod bench_range_search;
|
||||
|
||||
criterion_main!(bench_insert_bulk::benches, bench_range_search::benches,);
|
||||
34
benches/shared.rs
Normal file
34
benches/shared.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use autobarts::geom::Point;
|
||||
use criterion::Criterion;
|
||||
|
||||
//
|
||||
// Benchmark Parameters
|
||||
//
|
||||
pub const BENCH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
pub const BENCH_NUM_INSERT: i32 = 50_000;
|
||||
pub const BENCH_NODE_CAPACITY: usize = 5;
|
||||
|
||||
//
|
||||
// Data Generation Functions (Raw Data)
|
||||
//
|
||||
pub fn generate_2d_data() -> Vec<Point> {
|
||||
let data: Vec<Point> = (0..BENCH_NUM_INSERT)
|
||||
.map(|i| {
|
||||
let point = bevy::prelude::Vec2::new(i as f32, i as f32);
|
||||
|
||||
Point {
|
||||
point,
|
||||
entity: bevy::prelude::Entity::PLACEHOLDER,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
data
|
||||
}
|
||||
|
||||
// Configure Criterion with a timeout for benchmarks
|
||||
pub fn configure_criterion() -> Criterion {
|
||||
Criterion::default()
|
||||
.measurement_time(BENCH_TIMEOUT)
|
||||
.sample_size(10)
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
use std::{path::PathBuf, sync::LazyLock};
|
||||
|
||||
use include_dir::{include_dir, Dir};
|
||||
use rusqlite::{config::DbConfig, Connection};
|
||||
use include_dir::{Dir, include_dir};
|
||||
use rusqlite::{Connection, config::DbConfig};
|
||||
use rusqlite_migration::Migrations;
|
||||
|
||||
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
pub mod db;
|
||||
pub mod geom;
|
||||
mod spindex;
|
||||
|
||||
pub use spindex::*;
|
||||
pub mod spindex;
|
||||
|
|
|
|||
470
src/spindex.rs
470
src/spindex.rs
|
|
@ -1,5 +1,3 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use bevy::{
|
||||
math::bounding::{Aabb2d, BoundingVolume, IntersectsVolume},
|
||||
prelude::*,
|
||||
|
|
@ -7,9 +5,6 @@ use bevy::{
|
|||
|
||||
use crate::geom::Point;
|
||||
|
||||
// Epsilon value for zero-sizes bounding boxes/cubes.
|
||||
const EPSILON: f32 = 1e-10;
|
||||
|
||||
/// An entry in the R*‑tree, which can be either a leaf or a node.
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
|
|
@ -60,44 +55,24 @@ impl Entry {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
fn child_mut(&mut self) -> Option<&mut TreeNode> {
|
||||
match self {
|
||||
Entry::Node { child, .. } => Some(child),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn set_mbr(&mut self, new_mbr: Aabb2d) {
|
||||
if let Entry::Node { mbr, .. } = self {
|
||||
*mbr = new_mbr;
|
||||
}
|
||||
}
|
||||
fn into_child(self) -> Option<TreeNode> {
|
||||
match self {
|
||||
Entry::Node { child, .. } => Some(child),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TreeNode {
|
||||
fn is_leaf(&self) -> bool {
|
||||
self.is_leaf
|
||||
}
|
||||
|
||||
fn entries(&self) -> &[Entry] {
|
||||
&self.entries
|
||||
}
|
||||
fn entries_mut(&mut self) -> &mut [Entry] {
|
||||
&mut self.entries
|
||||
}
|
||||
|
||||
fn range_search_bbox(&self, bbox: &Aabb2d) -> Vec<&Point> {
|
||||
let mut result = Vec::new();
|
||||
fn range_search_bbox<'s>(&'s self, bbox: &Aabb2d, out: &mut Vec<&'s Point>) {
|
||||
if self.is_leaf() {
|
||||
for entry in self.entries() {
|
||||
if let Some(obj) = entry.as_leaf_obj()
|
||||
&& entry.mbr().intersects(bbox)
|
||||
{
|
||||
result.push(obj);
|
||||
out.push(obj);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -105,11 +80,10 @@ impl TreeNode {
|
|||
if let Some(child) = entry.child()
|
||||
&& entry.mbr().intersects(bbox)
|
||||
{
|
||||
result.extend_from_slice(&child.range_search_bbox(bbox));
|
||||
child.range_search_bbox(bbox, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn mbr(&self) -> Option<Aabb2d> {
|
||||
|
|
@ -129,99 +103,26 @@ impl RStarTree {
|
|||
}
|
||||
}
|
||||
|
||||
/// Inserts an object into the R*‑tree.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `object` - The object to insert.
|
||||
pub fn insert(&mut self, object: Point) {
|
||||
let entry = Entry::Leaf {
|
||||
mbr: object.mbr(),
|
||||
object,
|
||||
};
|
||||
self.insert_entry(entry, None);
|
||||
pub fn range_search(&self, query_point: &Point, radius: f32) -> Vec<&Point> {
|
||||
let query_bbox = Aabb2d::new(query_point.point, Vec2::splat(radius));
|
||||
let r2 = radius * radius;
|
||||
let candidates = self.range_search_bbox(&query_bbox);
|
||||
candidates
|
||||
.into_iter()
|
||||
.filter(|&other| {
|
||||
((query_point.point.x - other.point.x) + (query_point.point.y - other.point.y))
|
||||
.powi(2)
|
||||
<= r2
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn insert_entry(&mut self, entry: Entry, mut reinsert_level: Option<usize>) {
|
||||
let mut to_insert = vec![(entry, 0)];
|
||||
while let Some((item, level)) = to_insert.pop() {
|
||||
let overflow = insert_recursive(
|
||||
&mut self.root,
|
||||
item,
|
||||
self.max_entries,
|
||||
level,
|
||||
&mut reinsert_level,
|
||||
&mut to_insert,
|
||||
);
|
||||
|
||||
if let Some((overflowed_node, overflow_level)) = overflow {
|
||||
if reinsert_level == Some(overflow_level) {
|
||||
let old_entries = overflowed_node;
|
||||
let (group1, group2) = split_entries(old_entries, self.max_entries);
|
||||
let mut mbr1 = *group1[0].mbr();
|
||||
for entry in group1.iter() {
|
||||
mbr1 = mbr1.merge(entry.mbr());
|
||||
}
|
||||
let mut mbr2 = *group2[0].mbr();
|
||||
for entry in group2.iter() {
|
||||
mbr2 = mbr2.merge(entry.mbr());
|
||||
}
|
||||
let child1 = TreeNode {
|
||||
entries: group1,
|
||||
is_leaf: self.root.is_leaf,
|
||||
};
|
||||
let child2 = TreeNode {
|
||||
entries: group2,
|
||||
is_leaf: self.root.is_leaf,
|
||||
};
|
||||
|
||||
self.root.is_leaf = false;
|
||||
self.root.entries.clear();
|
||||
self.root.entries.push(Entry::Node {
|
||||
mbr: mbr1,
|
||||
child: child1,
|
||||
});
|
||||
self.root.entries.push(Entry::Node {
|
||||
mbr: mbr2,
|
||||
child: child2,
|
||||
});
|
||||
} else {
|
||||
if reinsert_level.is_none() {
|
||||
reinsert_level = Some(overflow_level);
|
||||
}
|
||||
let mut node = TreeNode {
|
||||
entries: overflowed_node,
|
||||
is_leaf: self.root.is_leaf,
|
||||
};
|
||||
let reinserted_entries = forced_reinsert(&mut node, self.max_entries);
|
||||
self.root.entries = node.entries;
|
||||
for entry in reinserted_entries {
|
||||
to_insert.push((entry, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs a range search with a given query bounding volume.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `query` - The bounding volume to search against.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A vector of references to the objects whose minimum bounding volumes
|
||||
/// intersect the query.
|
||||
pub fn range_search_bbox(&self, query_bbox: &Aabb2d) -> Vec<&Point> {
|
||||
self.root.range_search_bbox(query_bbox)
|
||||
let mut res = Vec::with_capacity(self.root.entries.len() / 10);
|
||||
self.root.range_search_bbox(query_bbox, &mut res);
|
||||
res
|
||||
}
|
||||
|
||||
/// Inserts a bulk of objects into the R*-tree.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `objects` - The objects to insert.
|
||||
pub fn insert_bulk(&mut self, objects: Vec<Point>) {
|
||||
if objects.is_empty() {
|
||||
return;
|
||||
|
|
@ -254,343 +155,12 @@ impl RStarTree {
|
|||
|
||||
self.root.entries.extend(entries);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn height(&self) -> usize {
|
||||
let mut height = 1;
|
||||
let mut current_node = &self.root;
|
||||
while !current_node.is_leaf {
|
||||
height += 1;
|
||||
current_node = if let Some(Entry::Node { child, .. }) = current_node.entries.first() {
|
||||
child
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
}
|
||||
height
|
||||
}
|
||||
}
|
||||
|
||||
fn choose_subtree(node: &TreeNode, entry: &Entry) -> usize {
|
||||
let children_are_leaves = if let Some(Entry::Node { child, .. }) = node.entries.first() {
|
||||
child.is_leaf
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if children_are_leaves {
|
||||
node.entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.min_by(|&(_, a), &(_, b)| {
|
||||
let mbr_a = a.mbr();
|
||||
let mbr_b = b.mbr();
|
||||
|
||||
let overlap_a = node
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|e| !std::ptr::eq(*e, a))
|
||||
.map(|e| e.mbr().merge(entry.mbr()).overlap(e.mbr()))
|
||||
.sum::<f32>();
|
||||
|
||||
let overlap_b = node
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|e| !std::ptr::eq(*e, b))
|
||||
.map(|e| e.mbr().merge(entry.mbr()).overlap(e.mbr()))
|
||||
.sum::<f32>();
|
||||
|
||||
let overlap_cmp = overlap_a.partial_cmp(&overlap_b).unwrap_or(Ordering::Equal);
|
||||
if overlap_cmp != Ordering::Equal {
|
||||
return overlap_cmp;
|
||||
}
|
||||
|
||||
let enlargement_a = mbr_a.merge(entry.mbr()).visible_area() - mbr_a.visible_area();
|
||||
let enlargement_b = mbr_b.merge(entry.mbr()).visible_area() - mbr_b.visible_area();
|
||||
let enlargement_cmp = enlargement_a
|
||||
.partial_cmp(&enlargement_b)
|
||||
.unwrap_or(Ordering::Equal);
|
||||
if enlargement_cmp != Ordering::Equal {
|
||||
return enlargement_cmp;
|
||||
}
|
||||
|
||||
mbr_a
|
||||
.visible_area()
|
||||
.partial_cmp(&mbr_b.visible_area())
|
||||
.unwrap_or(Ordering::Equal)
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(0)
|
||||
} else {
|
||||
node.entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.min_by(|(_, a), (_, b)| {
|
||||
let mbr_a = a.mbr();
|
||||
let mbr_b = b.mbr();
|
||||
|
||||
let enlargement_a = mbr_a.merge(entry.mbr()).visible_area() - mbr_a.visible_area();
|
||||
let enlargement_b = mbr_b.merge(entry.mbr()).visible_area() - mbr_b.visible_area();
|
||||
|
||||
let enlargement_cmp = enlargement_a
|
||||
.partial_cmp(&enlargement_b)
|
||||
.unwrap_or(Ordering::Equal);
|
||||
if enlargement_cmp != Ordering::Equal {
|
||||
return enlargement_cmp;
|
||||
}
|
||||
mbr_a
|
||||
.visible_area()
|
||||
.partial_cmp(&mbr_b.visible_area())
|
||||
.unwrap_or(Ordering::Equal)
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_recursive(
|
||||
node: &mut TreeNode,
|
||||
entry: Entry,
|
||||
max_entries: usize,
|
||||
level: usize,
|
||||
reinsert_level: &mut Option<usize>,
|
||||
to_insert_queue: &mut Vec<(Entry, usize)>,
|
||||
) -> Option<(Vec<Entry>, usize)> {
|
||||
if node.is_leaf {
|
||||
node.entries.push(entry);
|
||||
} else {
|
||||
let best_index = choose_subtree(node, &entry);
|
||||
let child = if let Entry::Node { child, .. } = &mut node.entries[best_index] {
|
||||
child
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
if let Some((overflow, overflow_level)) = insert_recursive(
|
||||
child,
|
||||
entry,
|
||||
max_entries,
|
||||
level + 1,
|
||||
reinsert_level,
|
||||
to_insert_queue,
|
||||
) {
|
||||
if reinsert_level.is_some() && *reinsert_level == Some(overflow_level) {
|
||||
let (g1, g2) = split_entries(overflow, max_entries);
|
||||
let mbr1 = entries_mbr(&g1)
|
||||
.unwrap_or_else(|| unreachable!("non-empty group must have MBR"));
|
||||
let mbr2 = entries_mbr(&g2)
|
||||
.unwrap_or_else(|| unreachable!("non-empty group must have MBR"));
|
||||
|
||||
let child1 = TreeNode {
|
||||
entries: g1,
|
||||
is_leaf: child.is_leaf,
|
||||
};
|
||||
let child2 = TreeNode {
|
||||
entries: g2,
|
||||
is_leaf: child.is_leaf,
|
||||
};
|
||||
node.entries[best_index] = Entry::Node {
|
||||
mbr: mbr1,
|
||||
child: child1,
|
||||
};
|
||||
node.entries.push(Entry::Node {
|
||||
mbr: mbr2,
|
||||
child: child2,
|
||||
});
|
||||
} else {
|
||||
if reinsert_level.is_none() {
|
||||
*reinsert_level = Some(overflow_level);
|
||||
}
|
||||
let mut overflowed_node = TreeNode {
|
||||
entries: overflow,
|
||||
is_leaf: child.is_leaf,
|
||||
};
|
||||
let reinserted = forced_reinsert(&mut overflowed_node, max_entries);
|
||||
for item in reinserted {
|
||||
to_insert_queue.push((item, 0));
|
||||
}
|
||||
if let Entry::Node { child, .. } = &mut node.entries[best_index] {
|
||||
child.entries = overflowed_node.entries;
|
||||
}
|
||||
}
|
||||
}
|
||||
let children = &mut node.entries_mut()[best_index];
|
||||
let Entry::Node {
|
||||
child: children,
|
||||
mbr,
|
||||
} = children
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if let Some(new_mbr) = entries_mbr(children.entries()) {
|
||||
*mbr = new_mbr;
|
||||
}
|
||||
}
|
||||
|
||||
if node.entries.len() > max_entries {
|
||||
return Some((std::mem::take(&mut node.entries), level));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn forced_reinsert(node: &mut TreeNode, max_entries: usize) -> Vec<Entry> {
|
||||
let node_mbr = if let Some(mbr) = entries_mbr(&node.entries) {
|
||||
mbr
|
||||
} else {
|
||||
return Vec::new();
|
||||
};
|
||||
let reinsert_count = (max_entries as f32 * 0.3).ceil() as usize;
|
||||
|
||||
node.entries.sort_by(|a, b| {
|
||||
let center_a: Vec<f32> = (0..2).map(|d| a.mbr().center()[d]).collect();
|
||||
let center_b: Vec<f32> = (0..2).map(|d| b.mbr().center()[d]).collect();
|
||||
let node_center: Vec<f32> = (0..2).map(|d| node_mbr.center()[d]).collect();
|
||||
|
||||
let dist_a = center_a
|
||||
.iter()
|
||||
.zip(node_center.iter())
|
||||
.map(|(ca, cb)| (ca - cb).powi(2))
|
||||
.sum::<f32>();
|
||||
let dist_b = center_b
|
||||
.iter()
|
||||
.zip(node_center.iter())
|
||||
.map(|(ca, cb)| (ca - cb).powi(2))
|
||||
.sum::<f32>();
|
||||
|
||||
dist_b.partial_cmp(&dist_a).unwrap_or(Ordering::Equal)
|
||||
});
|
||||
|
||||
node.entries.drain(0..reinsert_count).collect()
|
||||
}
|
||||
|
||||
fn split_entries(mut entries: Vec<Entry>, max_entries: usize) -> (Vec<Entry>, Vec<Entry>) {
|
||||
let min_entries = (max_entries as f32 * 0.4).ceil() as usize;
|
||||
let mut best_axis = 0;
|
||||
let mut best_split_index = 0;
|
||||
let mut min_margin = f32::INFINITY;
|
||||
|
||||
for dim in 0..2 {
|
||||
entries.sort_by(|a, b| {
|
||||
let ca = a.mbr().center()[dim];
|
||||
|
||||
let cb = b.mbr().center()[dim];
|
||||
ca.partial_cmp(&cb).unwrap_or(Ordering::Equal)
|
||||
});
|
||||
|
||||
for k in min_entries..=entries.len() - min_entries {
|
||||
let group1 = &entries[..k];
|
||||
let group2 = &entries[k..];
|
||||
let mut mbr1 = *group1[0].mbr();
|
||||
for entry in group1 {
|
||||
mbr1 = mbr1.merge(entry.mbr());
|
||||
}
|
||||
let mbr2 = *group2[0].mbr();
|
||||
|
||||
let margin = mbr1.margin() + mbr2.margin();
|
||||
if margin < min_margin {
|
||||
min_margin = margin;
|
||||
best_axis = dim;
|
||||
best_split_index = k;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entries.sort_by(|a, b| {
|
||||
let ca = a.mbr().center()[best_axis];
|
||||
let cb = b.mbr().center()[best_axis];
|
||||
ca.partial_cmp(&cb).unwrap_or(Ordering::Equal)
|
||||
});
|
||||
|
||||
let mut best_overlap = f32::INFINITY;
|
||||
let mut best_area = f32::INFINITY;
|
||||
|
||||
for k in min_entries..=entries.len() - min_entries {
|
||||
let group1 = &entries[..k];
|
||||
let group2 = &entries[k..];
|
||||
let mbr1 =
|
||||
entries_mbr(group1).unwrap_or_else(|| unreachable!("non-empty group must have MBR"));
|
||||
let mbr2 =
|
||||
entries_mbr(group2).unwrap_or_else(|| unreachable!("non-empty group must have MBR"));
|
||||
let overlap = mbr1.overlap(&mbr2);
|
||||
let area = mbr1.visible_area() + mbr2.visible_area();
|
||||
|
||||
if overlap < best_overlap {
|
||||
best_overlap = overlap;
|
||||
best_area = area;
|
||||
best_split_index = k;
|
||||
} else if (overlap - best_overlap).abs() < EPSILON && area < best_area {
|
||||
best_area = area;
|
||||
best_split_index = k;
|
||||
}
|
||||
}
|
||||
|
||||
let (group1, group2) = entries.split_at(best_split_index);
|
||||
(group1.to_vec(), group2.to_vec())
|
||||
}
|
||||
|
||||
impl RStarTree {
|
||||
/// Performs a range search on the R*‑tree using a query object and radius.
|
||||
///
|
||||
/// The query object is wrapped into a bounding volume using
|
||||
/// `from_point_radius`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `query` - The query object.
|
||||
/// * `radius` - The search radius.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A vector of references to the objects within the given radius.
|
||||
pub fn range_search(&self, query_point: &Point, radius: f32) -> Vec<&Point> {
|
||||
let query_bbox = Aabb2d::new(query_point.point, Vec2::splat(radius));
|
||||
let r2 = radius.powi(2);
|
||||
let candidates = self.range_search_bbox(&query_bbox);
|
||||
candidates
|
||||
.into_iter()
|
||||
.filter(|other| query_point.point.distance_squared(other.point) <= r2)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
impl RStarTree {}
|
||||
|
||||
fn entries_mbr(entries: &[Entry]) -> Option<Aabb2d> {
|
||||
let mut iter = entries.iter();
|
||||
let first = *iter.next()?.mbr();
|
||||
Some(iter.fold(first, |acc, entry| acc.merge(entry.mbr())))
|
||||
}
|
||||
|
||||
trait Bvr: BoundingVolume {
|
||||
fn margin(&self) -> f32;
|
||||
|
||||
fn overlap(&self, other: &Self) -> f32;
|
||||
}
|
||||
|
||||
impl Bvr for Aabb2d {
|
||||
fn margin(&self) -> f32 {
|
||||
let Vec2 { x, y } = self.half_size();
|
||||
2.0 * x * y
|
||||
}
|
||||
|
||||
fn overlap(&self, other: &Aabb2d) -> f32 {
|
||||
let Vec2 {
|
||||
x: self_width,
|
||||
y: self_height,
|
||||
} = 2.0 * self.half_size();
|
||||
|
||||
let self_center = self.center();
|
||||
let other_center = other.center();
|
||||
|
||||
let Vec2 {
|
||||
x: other_width,
|
||||
y: other_height,
|
||||
} = 2.0 * other.half_size();
|
||||
|
||||
let o_x = (self_center.x + self_width).min(other_center.x + other_width)
|
||||
- self_center.x.max(other_center.x);
|
||||
let o_y = (self_center.y + self_height).min(other_center.y + other_height)
|
||||
- self_center.y.max(other_center.y);
|
||||
|
||||
(o_x * o_y).max(0.0)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue