use std::collections::BinaryHeap;
use std::num::NonZeroUsize;
use std::ops::{Deref, Index};
#[cfg(feature = "multi-threaded")]
use std::sync::Arc;
#[cfg(feature = "multi-threaded")]
use rayon::prelude::*;
#[cfg(feature = "multi-threaded")]
pub use rayon::{ThreadPool, ThreadPoolBuilder};
#[cfg(feature = "multi-threaded")]
pub const DEFAULT_STATIC_CHUNK_THREAD_MULTIPLIER: NonZeroUsize = NonZeroUsize::new(4).unwrap();
#[cfg(feature = "multi-threaded")]
pub const MAX_STATIC_CHUNK_QUERIES: usize = 128;
#[cfg(feature = "multi-threaded")]
pub const DEFAULT_SERIAL_FALLBACK_THRESHOLD: usize = 128;
use super::builder::{
ApproxQueryBuilder, BestNWithinQueryBuilder, ExclusiveBoundariesQueryBuilder,
ExecuteQueryBuilder, NearestNQueryBuilder, NearestOneQueryBuilder,
PeriodicBoundaryConditionQueryBuilder, UnsortedQueryBuilder, WithDistancesQueryBuilder,
WithItemsQueryBuilder, WithPointsQueryBuilder, WithResultCapacityQueryBuilder,
WithinQueryBuilder, WithoutDistancesQueryBuilder, WithoutItemsQueryBuilder,
WithoutPointsQueryBuilder,
};
use crate::dist::DistanceMetric;
use crate::results::query_result_item::QueryResultItem;
#[derive(Clone, Debug)]
pub struct Executor {
kind: ExecutorKind,
#[cfg(feature = "multi-threaded")]
static_chunk_thread_multiplier: Option<NonZeroUsize>,
#[cfg(feature = "multi-threaded")]
serial_fallback_threshold: Option<usize>,
}
#[derive(Clone, Debug)]
enum ExecutorKind {
Serial,
#[cfg(feature = "multi-threaded")]
Parallel,
#[cfg(feature = "multi-threaded")]
ParallelInPool(Arc<ThreadPool>),
}
impl PartialEq for ExecutorKind {
#[inline]
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Serial, Self::Serial) => true,
#[cfg(feature = "multi-threaded")]
(Self::Parallel, Self::Parallel) => true,
#[cfg(feature = "multi-threaded")]
(Self::ParallelInPool(left), Self::ParallelInPool(right)) => Arc::ptr_eq(left, right),
#[cfg(feature = "multi-threaded")]
_ => false,
}
}
}
impl Eq for ExecutorKind {}
impl PartialEq for Executor {
#[inline]
fn eq(&self, other: &Self) -> bool {
#[cfg(feature = "multi-threaded")]
{
self.kind == other.kind
&& self.static_chunk_thread_multiplier == other.static_chunk_thread_multiplier
&& self.serial_fallback_threshold == other.serial_fallback_threshold
}
#[cfg(not(feature = "multi-threaded"))]
{
self.kind == other.kind
}
}
}
impl Eq for Executor {}
impl Default for Executor {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl Executor {
#[inline]
pub fn new() -> Self {
#[cfg(feature = "multi-threaded")]
{
Self::parallel()
}
#[cfg(not(feature = "multi-threaded"))]
{
Self::serial()
}
}
#[inline]
pub fn serial() -> Self {
Self {
kind: ExecutorKind::Serial,
#[cfg(feature = "multi-threaded")]
static_chunk_thread_multiplier: None,
#[cfg(feature = "multi-threaded")]
serial_fallback_threshold: None,
}
}
#[cfg(feature = "multi-threaded")]
#[inline]
pub fn parallel() -> Self {
Self {
kind: ExecutorKind::Parallel,
static_chunk_thread_multiplier: None,
serial_fallback_threshold: None,
}
}
#[cfg(feature = "multi-threaded")]
#[inline]
pub fn parallel_in_pool(pool: Arc<ThreadPool>) -> Self {
Self {
kind: ExecutorKind::ParallelInPool(pool),
static_chunk_thread_multiplier: None,
serial_fallback_threshold: None,
}
}
#[cfg(feature = "multi-threaded")]
#[inline]
pub fn with_static_chunk_thread_multiplier(mut self, multiplier: NonZeroUsize) -> Self {
self.static_chunk_thread_multiplier = Some(multiplier);
self
}
#[cfg(feature = "multi-threaded")]
#[inline]
pub fn with_default_static_chunking(self) -> Self {
self.with_static_chunk_thread_multiplier(DEFAULT_STATIC_CHUNK_THREAD_MULTIPLIER)
}
#[cfg(feature = "multi-threaded")]
#[inline]
pub fn with_serial_fallback_threshold(mut self, threshold: usize) -> Self {
self.serial_fallback_threshold = Some(threshold);
self
}
#[cfg(feature = "multi-threaded")]
#[inline]
pub fn with_serial_fallback(self) -> Self {
self.with_serial_fallback_threshold(DEFAULT_SERIAL_FALLBACK_THRESHOLD)
}
#[cfg(feature = "multi-threaded")]
#[inline]
fn is_parallel(&self, query_count: usize) -> bool {
if matches!(self.kind, ExecutorKind::Serial) {
return false;
}
self.serial_fallback_threshold
.is_none_or(|threshold| query_count >= threshold)
}
#[cfg(feature = "multi-threaded")]
#[inline]
fn pool(&self) -> Option<&ThreadPool> {
match &self.kind {
ExecutorKind::ParallelInPool(pool) => Some(pool),
_ => None,
}
}
#[cfg(feature = "multi-threaded")]
#[inline]
fn static_chunk_len(&self, query_count: usize) -> usize {
let Some(multiplier) = self.static_chunk_thread_multiplier else {
return 1;
};
let threads = self
.pool()
.map_or_else(rayon::current_num_threads, ThreadPool::current_num_threads);
let chunks = threads.max(1).saturating_mul(multiplier.get());
query_count
.div_ceil(chunks)
.clamp(1, MAX_STATIC_CHUNK_QUERIES)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BatchResults<R> {
results: Vec<R>,
}
impl<R> BatchResults<R> {
#[inline]
pub(crate) fn from_vec(results: Vec<R>) -> Self {
Self { results }
}
#[inline]
pub fn len(&self) -> usize {
self.results.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.results.is_empty()
}
#[inline]
pub fn get(&self, index: usize) -> Option<&R> {
self.results.get(index)
}
#[inline]
pub fn as_slice(&self) -> &[R] {
&self.results
}
#[inline]
pub fn iter(&self) -> std::slice::Iter<'_, R> {
self.results.iter()
}
#[inline]
pub fn into_vec(self) -> Vec<R> {
self.results
}
}
impl<R> Deref for BatchResults<R> {
type Target = [R];
#[inline]
fn deref(&self) -> &[R] {
&self.results
}
}
impl<R> Index<usize> for BatchResults<R> {
type Output = R;
#[inline]
fn index(&self, index: usize) -> &R {
&self.results[index]
}
}
impl<R> IntoIterator for BatchResults<R> {
type Item = R;
type IntoIter = std::vec::IntoIter<R>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.results.into_iter()
}
}
impl<'r, R> IntoIterator for &'r BatchResults<R> {
type Item = &'r R;
type IntoIter = std::slice::Iter<'r, R>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.results.iter()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BatchGroups<R> {
groups: Vec<Vec<R>>,
}
impl<R> BatchGroups<R> {
#[inline]
pub(crate) fn from_nested_vec(groups: Vec<Vec<R>>) -> Self {
Self { groups }
}
#[inline]
pub fn len(&self) -> usize {
self.groups.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.groups.is_empty()
}
#[inline]
pub fn total_len(&self) -> usize {
self.groups.iter().map(Vec::len).sum()
}
#[inline]
pub fn get(&self, index: usize) -> Option<&[R]> {
self.groups.get(index).map(Vec::as_slice)
}
#[inline]
pub fn iter(&self) -> GroupIter<'_, R> {
GroupIter {
inner: self.groups.iter(),
}
}
#[inline]
pub fn into_nested_vec(self) -> Vec<Vec<R>> {
self.groups
}
#[inline]
pub fn into_flat_vec(self) -> Vec<R> {
let mut flat = Vec::with_capacity(self.total_len());
for group in self.groups {
flat.extend(group);
}
flat
}
}
impl<R> Index<usize> for BatchGroups<R> {
type Output = [R];
#[inline]
fn index(&self, index: usize) -> &[R] {
&self.groups[index]
}
}
impl<'g, R> IntoIterator for &'g BatchGroups<R> {
type Item = &'g [R];
type IntoIter = GroupIter<'g, R>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
#[derive(Clone, Debug)]
pub struct GroupIter<'g, R> {
inner: std::slice::Iter<'g, Vec<R>>,
}
impl<'g, R> Iterator for GroupIter<'g, R> {
type Item = &'g [R];
#[inline]
fn next(&mut self) -> Option<&'g [R]> {
self.inner.next().map(Vec::as_slice)
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<R> ExactSizeIterator for GroupIter<'_, R> {}
impl<R> DoubleEndedIterator for GroupIter<'_, R> {
#[inline]
fn next_back(&mut self) -> Option<Self::Item> {
self.inner.next_back().map(Vec::as_slice)
}
}
#[doc(hidden)]
pub trait BatchCollect: Sized {
#[doc(hidden)]
type Batch;
#[doc(hidden)]
fn collect_batch(results: Vec<Self>) -> Self::Batch;
}
impl<P, T, D> BatchCollect for QueryResultItem<P, T, D> {
type Batch = BatchResults<Self>;
#[inline]
fn collect_batch(results: Vec<Self>) -> Self::Batch {
BatchResults::from_vec(results)
}
}
impl<R> BatchCollect for Vec<R> {
type Batch = BatchGroups<R>;
#[inline]
fn collect_batch(results: Vec<Self>) -> Self::Batch {
BatchGroups::from_nested_vec(results)
}
}
impl<R: Ord> BatchCollect for BinaryHeap<R> {
type Batch = BatchGroups<R>;
#[inline]
fn collect_batch(results: Vec<Self>) -> Self::Batch {
BatchGroups::from_nested_vec(
results
.into_iter()
.map(BinaryHeap::into_sorted_vec)
.collect(),
)
}
}
#[doc(hidden)]
pub trait BatchQuerySink<'a, A, const K: usize>: Copy {
#[doc(hidden)]
fn set_query_point(&mut self, query: &'a [A; K]);
}
pub struct BatchQueryBuilder<'a, Qb, A, const K: usize> {
prototype: Option<Qb>,
queries: &'a [[A; K]],
executor: Executor,
}
impl<'a, Qb, A, const K: usize> BatchQueryBuilder<'a, Qb, A, K> {
#[inline]
pub(crate) fn new(prototype: Option<Qb>, queries: &'a [[A; K]]) -> Self {
Self {
prototype,
queries,
executor: Executor::new(),
}
}
#[inline]
fn map<Qb2>(self, transition: impl FnOnce(Qb) -> Qb2) -> BatchQueryBuilder<'a, Qb2, A, K> {
BatchQueryBuilder {
prototype: self.prototype.map(transition),
queries: self.queries,
executor: self.executor,
}
}
#[inline]
pub fn len(&self) -> usize {
self.queries.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.queries.is_empty()
}
#[inline]
pub fn with_executor(mut self, executor: &Executor) -> Self {
self.executor = executor.clone();
self
}
}
impl<'a, Qb, A, const K: usize> BatchQueryBuilder<'a, Qb, A, K> {
#[inline]
pub fn without_points(self) -> BatchQueryBuilder<'a, Qb::Output, A, K>
where
Qb: WithoutPointsQueryBuilder,
{
self.map(Qb::without_points)
}
#[inline]
pub fn with_points(self) -> BatchQueryBuilder<'a, Qb::Output, A, K>
where
Qb: WithPointsQueryBuilder,
{
self.map(Qb::with_points)
}
#[inline]
pub fn with_items(self) -> BatchQueryBuilder<'a, Qb::Output, A, K>
where
Qb: WithItemsQueryBuilder,
{
self.map(Qb::with_items)
}
#[inline]
pub fn without_items(self) -> BatchQueryBuilder<'a, Qb::Output, A, K>
where
Qb: WithoutItemsQueryBuilder,
{
self.map(Qb::without_items)
}
#[inline]
pub fn with_distances(self) -> BatchQueryBuilder<'a, Qb::Output, A, K>
where
Qb: WithDistancesQueryBuilder,
{
self.map(Qb::with_distances)
}
#[inline]
pub fn without_distances(self) -> BatchQueryBuilder<'a, Qb::Output, A, K>
where
Qb: WithoutDistancesQueryBuilder,
{
self.map(Qb::without_distances)
}
#[inline]
pub fn periodic_boundary_condition(
self,
box_size: &'a [A; K],
) -> BatchQueryBuilder<'a, Qb::Output, A, K>
where
Qb: PeriodicBoundaryConditionQueryBuilder<'a, A, K>,
{
self.map(|prototype| prototype.periodic_boundary_condition(box_size))
}
#[inline]
pub fn nearest_one<Dq>(self) -> BatchQueryBuilder<'a, Qb::Output, A, K>
where
A: Copy,
Dq: DistanceMetric<A>,
Qb: NearestOneQueryBuilder<A, K, Dq>,
{
self.map(Qb::nearest_one)
}
#[inline]
pub fn nearest_n<Dq>(self, max_qty: NonZeroUsize) -> BatchQueryBuilder<'a, Qb::Output, A, K>
where
A: Copy,
Dq: DistanceMetric<A>,
Qb: NearestNQueryBuilder<A, K, Dq>,
{
self.map(|prototype| prototype.nearest_n(max_qty))
}
#[inline]
pub fn within<Dq>(self, radius: Dq::Output) -> BatchQueryBuilder<'a, Qb::Output, A, K>
where
A: Copy,
Dq: DistanceMetric<A>,
Qb: WithinQueryBuilder<A, K, Dq>,
{
self.map(|prototype| prototype.within(radius))
}
#[inline]
pub fn best_n_within<Dq>(
self,
radius: Dq::Output,
max_qty: NonZeroUsize,
) -> BatchQueryBuilder<'a, Qb::Output, A, K>
where
A: Copy,
Dq: DistanceMetric<A>,
Qb: BestNWithinQueryBuilder<A, K, Dq>,
{
self.map(|prototype| prototype.best_n_within(radius, max_qty))
}
#[inline]
pub fn approx(self) -> BatchQueryBuilder<'a, Qb::Output, A, K>
where
Qb: ApproxQueryBuilder,
{
self.map(Qb::approx)
}
#[inline]
pub fn unsorted(self) -> BatchQueryBuilder<'a, Qb::Output, A, K>
where
Qb: UnsortedQueryBuilder,
{
self.map(Qb::unsorted)
}
#[inline]
pub fn exclusive_boundaries(self) -> BatchQueryBuilder<'a, Qb::Output, A, K>
where
Qb: ExclusiveBoundariesQueryBuilder,
{
self.map(Qb::exclusive_boundaries)
}
#[inline]
pub fn with_result_capacity(self, result_capacity: usize) -> Self
where
Qb: WithResultCapacityQueryBuilder,
{
self.map(|prototype| prototype.with_result_capacity(result_capacity))
}
}
impl<'a, Qb, A, const K: usize> BatchQueryBuilder<'a, Qb, A, K>
where
A: Sync,
Qb: ExecuteQueryBuilder + BatchQuerySink<'a, A, K> + Send + Sync,
Qb::Output: BatchCollect + Send,
{
#[inline]
pub fn execute(self) -> <Qb::Output as BatchCollect>::Batch {
let Some(prototype) = self.prototype else {
return <Qb::Output as BatchCollect>::collect_batch(Vec::new());
};
let run = |query: &'a [A; K]| {
let mut query_builder = prototype;
query_builder.set_query_point(query);
query_builder.execute()
};
#[cfg(feature = "multi-threaded")]
if self.executor.is_parallel(self.queries.len()) {
let collect = || {
self.queries
.par_iter()
.with_min_len(self.executor.static_chunk_len(self.queries.len()))
.map(run)
.collect::<Vec<_>>()
};
let results = match self.executor.pool() {
Some(pool) => pool.install(collect),
None => collect(),
};
return <Qb::Output as BatchCollect>::collect_batch(results);
}
<Qb::Output as BatchCollect>::collect_batch(self.queries.iter().map(run).collect())
}
#[inline]
pub fn for_each<F>(self, visitor: F)
where
F: Fn(usize, Qb::Output) + Send + Sync,
{
let Some(prototype) = self.prototype else {
return;
};
let run = |(index, query): (usize, &'a [A; K])| {
let mut query_builder = prototype;
query_builder.set_query_point(query);
visitor(index, query_builder.execute());
};
#[cfg(feature = "multi-threaded")]
if self.executor.is_parallel(self.queries.len()) {
let visit = || {
self.queries
.par_iter()
.enumerate()
.with_min_len(self.executor.static_chunk_len(self.queries.len()))
.for_each(run)
};
match self.executor.pool() {
Some(pool) => pool.install(visit),
None => visit(),
}
return;
}
self.queries.iter().enumerate().for_each(run);
}
}
#[cfg(test)]
mod tests {
use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
use std::num::NonZeroUsize;
use std::sync::Mutex;
use crate::batch::Executor;
use crate::dist::{Manhattan, SquaredEuclidean};
use crate::kd_tree::KdTree;
use crate::leaf_strategy::{FlatVec, VecOfArenas, VecOfArrays};
use crate::Eytzinger;
const RNG_SEED: u64 = 42;
const K: usize = 3;
const BUCKET: usize = 32;
type Tree = KdTree<f64, u32, Eytzinger, VecOfArenas<f64, u32, K, BUCKET>, K, BUCKET>;
type SmallTree = KdTree<f64, u32, Eytzinger, FlatVec<f64, u32, 2, 4>, 2, 4>;
pub(super) fn tree_and_queries(points: usize, queries: usize) -> (Tree, Vec<[f64; K]>) {
let mut rng = StdRng::seed_from_u64(RNG_SEED);
let content: Vec<[f64; K]> = (0..points).map(|_| rng.random()).collect();
let queries: Vec<[f64; K]> = (0..queries).map(|_| rng.random()).collect();
(Tree::new_from_slice(&content).unwrap(), queries)
}
fn executors() -> Vec<Executor> {
#[cfg(feature = "multi-threaded")]
{
vec![
Executor::serial(),
Executor::parallel(),
Executor::new(),
Executor::parallel().with_default_static_chunking(),
Executor::parallel()
.with_static_chunk_thread_multiplier(NonZeroUsize::new(1).unwrap()),
Executor::parallel()
.with_static_chunk_thread_multiplier(NonZeroUsize::new(7).unwrap()),
Executor::parallel().with_serial_fallback_threshold(usize::MAX),
Executor::parallel().with_serial_fallback_threshold(0),
Executor::parallel_in_pool(std::sync::Arc::new(
rayon::ThreadPoolBuilder::new()
.num_threads(3)
.build()
.unwrap(),
))
.with_default_static_chunking(),
]
}
#[cfg(not(feature = "multi-threaded"))]
{
vec![Executor::serial(), Executor::new()]
}
}
#[test]
fn nearest_one_batch_matches_single_point_queries() {
let (tree, queries) = tree_and_queries(2048, 257);
for executor in executors() {
let batch = tree
.query_batch(&queries)
.nearest_one::<SquaredEuclidean<f64>>()
.with_executor(&executor)
.execute();
assert_eq!(batch.len(), queries.len());
for (index, query) in queries.iter().enumerate() {
let expected = tree
.query(query)
.nearest_one::<SquaredEuclidean<f64>>()
.execute();
assert_eq!(batch[index].item, expected.item, "index {index}");
assert_eq!(batch[index].distance, expected.distance, "index {index}");
}
}
}
#[test]
fn nearest_n_batch_matches_single_point_queries() {
let (tree, queries) = tree_and_queries(2048, 129);
let max_qty = NonZeroUsize::new(7).unwrap();
for executor in executors() {
let batch = tree
.query_batch(&queries)
.nearest_n::<SquaredEuclidean<f64>>(max_qty)
.with_executor(&executor)
.execute();
assert_eq!(batch.len(), queries.len());
assert_eq!(batch.total_len(), queries.len() * max_qty.get());
for (index, query) in queries.iter().enumerate() {
let expected = tree
.query(query)
.nearest_n::<SquaredEuclidean<f64>>(max_qty)
.execute();
let actual = batch.get(index).unwrap();
assert_eq!(actual.len(), expected.len(), "index {index}");
for (actual, expected) in actual.iter().zip(expected.iter()) {
assert_eq!(actual.item, expected.item, "index {index}");
assert_eq!(actual.distance, expected.distance, "index {index}");
}
}
}
}
#[test]
fn nearest_n_within_batch_matches_single_point_queries() {
let (tree, queries) = tree_and_queries(2048, 65);
let max_qty = NonZeroUsize::new(5).unwrap();
let radius = 0.05;
let batch = tree
.query_batch(&queries)
.nearest_n::<SquaredEuclidean<f64>>(max_qty)
.within::<SquaredEuclidean<f64>>(radius)
.execute();
for (index, query) in queries.iter().enumerate() {
let expected = tree
.query(query)
.nearest_n::<SquaredEuclidean<f64>>(max_qty)
.within::<SquaredEuclidean<f64>>(radius)
.execute();
let actual = &batch[index];
assert_eq!(actual.len(), expected.len(), "index {index}");
for (actual, expected) in actual.iter().zip(expected.iter()) {
assert_eq!(actual.item, expected.item, "index {index}");
assert_eq!(actual.distance, expected.distance, "index {index}");
}
}
}
#[test]
fn batch_queries_work_against_a_mutable_tree() {
let mut rng = StdRng::seed_from_u64(RNG_SEED);
let mut tree: KdTree<f64, u32, Eytzinger, VecOfArrays<f64, u32, K, BUCKET>, K, BUCKET> =
KdTree::default();
for item in 0..512u32 {
tree.add(&rng.random::<[f64; K]>(), item).unwrap();
}
let queries: Vec<[f64; K]> = (0..64).map(|_| rng.random()).collect();
let batch = tree
.query_batch(&queries)
.nearest_one::<SquaredEuclidean<f64>>()
.execute();
for (index, query) in queries.iter().enumerate() {
let expected = tree
.query(query)
.nearest_one::<SquaredEuclidean<f64>>()
.execute();
assert_eq!(batch[index].item, expected.item, "index {index}");
}
}
#[test]
fn within_batch_matches_single_point_queries() {
let (tree, queries) = tree_and_queries(2048, 65);
let radius = 0.05;
let batch = tree
.query_batch(&queries)
.within::<SquaredEuclidean<f64>>(radius)
.execute();
assert_eq!(batch.len(), queries.len());
let mut flat_len = 0;
for (index, query) in queries.iter().enumerate() {
let expected = tree
.query(query)
.within::<SquaredEuclidean<f64>>(radius)
.execute();
let actual = &batch[index];
assert_eq!(actual.len(), expected.len(), "index {index}");
flat_len += expected.len();
for (actual, expected) in actual.iter().zip(expected.iter()) {
assert_eq!(actual.item, expected.item, "index {index}");
}
}
assert_eq!(batch.total_len(), flat_len);
assert_eq!(batch.into_flat_vec().len(), flat_len);
}
#[test]
fn best_n_within_batch_matches_single_point_queries() {
let (tree, queries) = tree_and_queries(2048, 33);
let radius = 0.05;
let max_qty = NonZeroUsize::new(5).unwrap();
let batch = tree
.query_batch(&queries)
.best_n_within::<SquaredEuclidean<f64>>(radius, max_qty)
.execute();
for (index, query) in queries.iter().enumerate() {
let expected = tree
.query(query)
.best_n_within::<SquaredEuclidean<f64>>(radius, max_qty)
.execute()
.into_sorted_vec();
let actual = &batch[index];
assert_eq!(actual.len(), expected.len(), "index {index}");
for (actual, expected) in actual.iter().zip(expected.iter()) {
assert_eq!(actual.item, expected.item, "index {index}");
assert_eq!(actual.distance, expected.distance, "index {index}");
}
}
}
#[test]
fn projections_and_modifiers_carry_through_to_batch() {
let (tree, queries) = tree_and_queries(1024, 17);
let max_qty = NonZeroUsize::new(3).unwrap();
let batch = tree
.query_batch(&queries)
.nearest_n::<Manhattan<f64>>(max_qty)
.with_points()
.without_items()
.execute();
for (index, query) in queries.iter().enumerate() {
let expected = tree
.query(query)
.nearest_n::<Manhattan<f64>>(max_qty)
.with_points()
.without_items()
.execute();
for (actual, expected) in batch[index].iter().zip(expected.iter()) {
assert_eq!(actual.point, expected.point, "index {index}");
assert_eq!(actual.distance, expected.distance, "index {index}");
assert_eq!(actual.item, (), "index {index}");
}
}
}
#[test]
fn unsorted_and_exclusive_boundaries_carry_through_to_batch() {
let (tree, queries) = tree_and_queries(1024, 17);
let radius = 0.05;
let batch = tree
.query_batch(&queries)
.within::<SquaredEuclidean<f64>>(radius)
.unsorted()
.exclusive_boundaries()
.with_result_capacity(8)
.execute();
for (index, query) in queries.iter().enumerate() {
let expected = tree
.query(query)
.within::<SquaredEuclidean<f64>>(radius)
.unsorted()
.exclusive_boundaries()
.with_result_capacity(8)
.execute();
assert_eq!(batch[index].len(), expected.len(), "index {index}");
}
}
#[test]
fn approx_nearest_one_carries_through_to_batch() {
let (tree, queries) = tree_and_queries(1024, 17);
let batch = tree
.query_batch(&queries)
.nearest_one::<SquaredEuclidean<f64>>()
.approx()
.execute();
for (index, query) in queries.iter().enumerate() {
let expected = tree
.query(query)
.nearest_one::<SquaredEuclidean<f64>>()
.approx()
.execute();
assert_eq!(batch[index].item, expected.item, "index {index}");
}
}
#[test]
fn periodic_boundary_conditions_carry_through_to_batch() {
let (tree, queries) = tree_and_queries(1024, 17);
let box_size = [1.0f64; K];
let batch = tree
.query_batch(&queries)
.periodic_boundary_condition(&box_size)
.nearest_one::<SquaredEuclidean<f64>>()
.execute();
for (index, query) in queries.iter().enumerate() {
let expected = tree
.query(query)
.periodic_boundary_condition(&box_size)
.nearest_one::<SquaredEuclidean<f64>>()
.execute();
assert_eq!(batch[index].item, expected.item, "index {index}");
assert_eq!(batch[index].distance, expected.distance, "index {index}");
}
}
#[test]
fn for_each_visits_every_query_exactly_once_with_its_own_index() {
let (tree, queries) = tree_and_queries(2048, 513);
for executor in executors() {
let seen: Mutex<Vec<Option<u32>>> = Mutex::new(vec![None; queries.len()]);
tree.query_batch(&queries)
.nearest_one::<SquaredEuclidean<f64>>()
.with_executor(&executor)
.for_each(|index, result| {
let mut seen = seen.lock().unwrap();
assert!(seen[index].is_none(), "index {index} visited twice");
seen[index] = Some(result.item);
});
let seen = seen.into_inner().unwrap();
for (index, query) in queries.iter().enumerate() {
let expected = tree
.query(query)
.nearest_one::<SquaredEuclidean<f64>>()
.execute();
assert_eq!(seen[index], Some(expected.item), "index {index}");
}
}
}
#[test]
fn empty_batch_yields_empty_results() {
let (tree, _) = tree_and_queries(256, 0);
let queries: [[f64; K]; 0] = [];
let nearest_one = tree
.query_batch(&queries)
.nearest_one::<SquaredEuclidean<f64>>()
.execute();
assert!(nearest_one.is_empty());
assert_eq!(nearest_one.len(), 0);
assert!(nearest_one.into_vec().is_empty());
let nearest_n = tree
.query_batch(&queries)
.nearest_n::<SquaredEuclidean<f64>>(NonZeroUsize::new(4).unwrap())
.execute();
assert!(nearest_n.is_empty());
assert_eq!(nearest_n.total_len(), 0);
assert_eq!(nearest_n.iter().count(), 0);
tree.query_batch(&queries)
.nearest_one::<SquaredEuclidean<f64>>()
.for_each(|_, _| panic!("visitor called for an empty batch"));
}
#[test]
fn single_query_batch_is_supported() {
let entries = [
(10_u32, [0.0, 0.0]),
(11_u32, [1.0, 1.0]),
(12_u32, [0.2, 0.1]),
];
let tree = SmallTree::new_from_entries(&entries).unwrap();
let queries = [[0.9, 0.9]];
let results = tree
.query_batch(&queries)
.nearest_one::<SquaredEuclidean<f64>>()
.execute();
assert_eq!(results.len(), 1);
assert_eq!(results[0].item, 11);
}
#[test]
fn builder_reports_batch_size_before_execution() {
let (tree, queries) = tree_and_queries(256, 9);
let builder = tree.query_batch(&queries);
assert_eq!(builder.len(), 9);
assert!(!builder.is_empty());
let empty: [[f64; K]; 0] = [];
assert!(tree.query_batch(&empty).is_empty());
}
#[cfg(feature = "multi-threaded")]
#[test]
fn scheduling_hints_do_not_change_results() {
let (tree, queries) = tree_and_queries(1024, 200);
let baseline = tree
.query_batch(&queries)
.nearest_one::<SquaredEuclidean<f64>>()
.with_executor(&Executor::serial())
.execute();
for executor in executors() {
let hinted = tree
.query_batch(&queries)
.nearest_one::<SquaredEuclidean<f64>>()
.with_executor(&executor)
.execute();
assert_eq!(
hinted.as_slice(),
baseline.as_slice(),
"executor {executor:?}"
);
}
}
#[cfg(feature = "multi-threaded")]
#[test]
fn static_chunking_indexes_ragged_batches_correctly() {
for query_count in [1usize, 2, 3, 5, 17, 63, 64, 65, 257] {
let (tree, queries) = tree_and_queries(512, query_count);
let baseline = tree
.query_batch(&queries)
.nearest_one::<SquaredEuclidean<f64>>()
.with_executor(&Executor::serial())
.execute();
for multiplier in [1usize, 3, 4, 16] {
let executor = Executor::parallel()
.with_static_chunk_thread_multiplier(NonZeroUsize::new(multiplier).unwrap());
let chunked = tree
.query_batch(&queries)
.nearest_one::<SquaredEuclidean<f64>>()
.with_executor(&executor)
.execute();
assert_eq!(
chunked.as_slice(),
baseline.as_slice(),
"{query_count} queries, multiplier {multiplier}"
);
}
}
}
#[cfg(feature = "multi-threaded")]
#[test]
fn static_chunking_for_each_reports_correct_indices() {
use std::sync::Mutex;
for query_count in [1usize, 5, 17, 63, 65, 257] {
let (tree, queries) = tree_and_queries(512, query_count);
let expected: Vec<_> = queries
.iter()
.map(|query| {
tree.query(query)
.nearest_one::<SquaredEuclidean<f64>>()
.execute()
})
.collect();
let seen = Mutex::new(vec![None; query_count]);
tree.query_batch(&queries)
.nearest_one::<SquaredEuclidean<f64>>()
.with_executor(&Executor::parallel().with_default_static_chunking())
.for_each(|index, result| {
seen.lock().unwrap()[index] = Some(result);
});
let seen = seen.into_inner().unwrap();
for (index, (actual, expected)) in seen.iter().zip(expected.iter()).enumerate() {
assert_eq!(
actual.as_ref(),
Some(expected),
"{query_count} queries, index {index}"
);
}
}
}
#[cfg(feature = "multi-threaded")]
#[test]
fn serial_fallback_threshold_selects_the_calling_thread() {
let (tree, queries) = tree_and_queries(1024, 64);
for threshold in [0usize, 32, 64, 65, usize::MAX] {
let executor = Executor::parallel().with_serial_fallback_threshold(threshold);
assert_eq!(
executor.is_parallel(queries.len()),
queries.len() >= threshold
);
let results = tree
.query_batch(&queries)
.nearest_one::<SquaredEuclidean<f64>>()
.with_executor(&executor)
.execute();
assert_eq!(results.len(), queries.len());
}
}
#[cfg(feature = "multi-threaded")]
#[test]
fn caller_supplied_pool_bounds_the_thread_count() {
use std::collections::HashSet;
use std::sync::Mutex;
let (tree, queries) = tree_and_queries(4096, 512);
let pool = std::sync::Arc::new(
rayon::ThreadPoolBuilder::new()
.num_threads(2)
.build()
.unwrap(),
);
let threads = Mutex::new(HashSet::new());
tree.query_batch(&queries)
.nearest_one::<SquaredEuclidean<f64>>()
.with_executor(&Executor::parallel_in_pool(std::sync::Arc::clone(&pool)))
.for_each(|_index, _result| {
threads
.lock()
.unwrap()
.insert(rayon::current_thread_index());
});
let threads = threads.into_inner().unwrap();
assert!(
threads
.iter()
.all(|index| index.is_none_or(|index| index < 2)),
"work escaped the caller-supplied 2-thread pool: {threads:?}"
);
}
#[test]
fn results_containers_expose_owned_and_borrowed_views() {
let (tree, queries) = tree_and_queries(512, 5);
let results = tree
.query_batch(&queries)
.nearest_one::<SquaredEuclidean<f64>>()
.execute();
let borrowed: Vec<u32> = results.iter().map(|result| result.item).collect();
let by_ref: Vec<u32> = (&results).into_iter().map(|result| result.item).collect();
let owned: Vec<u32> = results.into_iter().map(|result| result.item).collect();
assert_eq!(borrowed, owned);
assert_eq!(by_ref, owned);
let groups = tree
.query_batch(&queries)
.nearest_n::<SquaredEuclidean<f64>>(NonZeroUsize::new(2).unwrap())
.execute();
let group_lens: Vec<usize> = groups.iter().map(<[_]>::len).collect();
let by_ref_lens: Vec<usize> = (&groups).into_iter().map(<[_]>::len).collect();
assert_eq!(group_lens, vec![2; queries.len()]);
assert_eq!(by_ref_lens, group_lens);
assert_eq!(groups.into_nested_vec().len(), queries.len());
}
}
#[cfg(all(test, feature = "multi-threaded"))]
mod parallel_tests {
use std::collections::HashSet;
use std::sync::Mutex;
use std::thread::ThreadId;
use crate::batch::Executor;
use crate::dist::SquaredEuclidean;
use super::tests::tree_and_queries;
#[test]
fn parallel_executor_uses_the_rayon_pool() {
if rayon::current_num_threads() < 2 {
return;
}
let (tree, queries) = tree_and_queries(4096, 8192);
let threads: Mutex<HashSet<ThreadId>> = Mutex::new(HashSet::new());
tree.query_batch(&queries)
.nearest_one::<SquaredEuclidean<f64>>()
.with_executor(&Executor::parallel())
.for_each(|_, _| {
threads.lock().unwrap().insert(std::thread::current().id());
});
assert!(
threads.into_inner().unwrap().len() > 1,
"expected work to reach more than one thread"
);
}
}