use crate::types::DistanceCalculationMetric;
use ndarray::{Array2, ArrayView1, ArrayView2};
use std::cmp::Ordering;
use std::collections::BinaryHeap;
const NONE: usize = usize::MAX;
#[derive(Debug, Clone)]
struct KdNode {
point_idx: usize,
axis: usize,
split: f64,
left: usize,
right: usize,
}
#[derive(Debug, Clone, Copy)]
struct Neighbor {
cmp_dist: f64,
idx: usize,
}
impl PartialEq for Neighbor {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for Neighbor {}
impl PartialOrd for Neighbor {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Neighbor {
fn cmp(&self, other: &Self) -> Ordering {
self.cmp_dist
.total_cmp(&other.cmp_dist)
.then(self.idx.cmp(&other.idx))
}
}
#[derive(Debug, Clone)]
pub struct KdTree {
metric: DistanceCalculationMetric,
points: Array2<f64>,
nodes: Vec<KdNode>,
root: usize,
}
impl KdTree {
pub fn build(points: ArrayView2<f64>, metric: DistanceCalculationMetric) -> Self {
let points = points.to_owned();
let n = points.nrows();
let mut nodes: Vec<KdNode> = Vec::with_capacity(n);
let mut indices: Vec<usize> = (0..n).collect();
let root = Self::build_recursive(&points, &mut indices, &mut nodes);
KdTree {
metric,
points,
nodes,
root,
}
}
fn build_recursive(
points: &Array2<f64>,
indices: &mut [usize],
nodes: &mut Vec<KdNode>,
) -> usize {
if indices.is_empty() {
return NONE;
}
let n_features = points.ncols();
let mut axis = 0;
let mut best_spread = f64::NEG_INFINITY;
for d in 0..n_features {
let mut lo = f64::INFINITY;
let mut hi = f64::NEG_INFINITY;
for &idx in indices.iter() {
let v = points[[idx, d]];
if v < lo {
lo = v;
}
if v > hi {
hi = v;
}
}
let spread = hi - lo;
if spread > best_spread {
best_spread = spread;
axis = d;
}
}
let mid = indices.len() / 2;
indices.select_nth_unstable_by(mid, |&a, &b| {
points[[a, axis]]
.partial_cmp(&points[[b, axis]])
.unwrap_or(Ordering::Equal)
});
let median_idx = indices[mid];
let split = points[[median_idx, axis]];
let node_index = nodes.len();
nodes.push(KdNode {
point_idx: median_idx,
axis,
split,
left: NONE,
right: NONE,
});
let (left_slice, rest) = indices.split_at_mut(mid);
let right_slice = &mut rest[1..]; let left = Self::build_recursive(points, left_slice, nodes);
let right = Self::build_recursive(points, right_slice, nodes);
nodes[node_index].left = left;
nodes[node_index].right = right;
node_index
}
pub(super) fn radius_neighbors(&self, query: ArrayView1<f64>, radius: f64) -> Vec<usize> {
let radius_cmp = self.metric.comparable_scalar(radius);
let mut out = Vec::new();
self.radius_recurse(self.root, query, radius_cmp, &mut out);
out.sort_unstable();
out
}
fn radius_recurse(
&self,
node: usize,
query: ArrayView1<f64>,
radius_cmp: f64,
out: &mut Vec<usize>,
) {
if node == NONE {
return;
}
let node = &self.nodes[node];
let d_cmp = self
.metric
.comparable_distance(query, self.points.row(node.point_idx));
if d_cmp <= radius_cmp {
out.push(node.point_idx);
}
let delta = query[node.axis] - node.split;
let (near, far) = if delta <= 0.0 {
(node.left, node.right)
} else {
(node.right, node.left)
};
self.radius_recurse(near, query, radius_cmp, out);
if self.metric.comparable_scalar(delta.abs()) <= radius_cmp {
self.radius_recurse(far, query, radius_cmp, out);
}
}
pub fn k_nearest(&self, query: ArrayView1<f64>, k: usize) -> Vec<(usize, f64)> {
let mut heap: BinaryHeap<Neighbor> = BinaryHeap::with_capacity(k.min(self.nodes.len()));
if k > 0 {
self.knn_recurse(self.root, query, k, &mut heap);
}
let mut found: Vec<Neighbor> = heap.into_vec();
found.sort_unstable();
found.into_iter().map(|n| (n.idx, n.cmp_dist)).collect()
}
fn knn_recurse(
&self,
node: usize,
query: ArrayView1<f64>,
k: usize,
heap: &mut BinaryHeap<Neighbor>,
) {
if node == NONE {
return;
}
let node = &self.nodes[node];
let candidate = Neighbor {
cmp_dist: self
.metric
.comparable_distance(query, self.points.row(node.point_idx)),
idx: node.point_idx,
};
if heap.len() < k {
heap.push(candidate);
} else if candidate < *heap.peek().unwrap() {
heap.pop();
heap.push(candidate);
}
let delta = query[node.axis] - node.split;
let (near, far) = if delta <= 0.0 {
(node.left, node.right)
} else {
(node.right, node.left)
};
self.knn_recurse(near, query, k, heap);
let worst = if heap.len() < k {
f64::INFINITY
} else {
heap.peek().unwrap().cmp_dist
};
if self.metric.comparable_scalar(delta.abs()) <= worst {
self.knn_recurse(far, query, k, heap);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::Array2;
use ndarray_rand::RandomExt;
use ndarray_rand::rand::SeedableRng;
use ndarray_rand::rand::rngs::StdRng;
use ndarray_rand::rand_distr::Uniform;
fn brute_radius(
points: &Array2<f64>,
query: ArrayView1<f64>,
radius: f64,
metric: DistanceCalculationMetric,
) -> Vec<usize> {
let radius_cmp = metric.comparable_scalar(radius);
(0..points.nrows())
.filter(|&i| metric.comparable_distance(query, points.row(i)) <= radius_cmp)
.collect()
}
fn brute_knn(
points: &Array2<f64>,
query: ArrayView1<f64>,
k: usize,
metric: DistanceCalculationMetric,
) -> Vec<(usize, f64)> {
let mut all: Vec<Neighbor> = (0..points.nrows())
.map(|i| Neighbor {
cmp_dist: metric.comparable_distance(query, points.row(i)),
idx: i,
})
.collect();
all.sort_unstable();
all.into_iter()
.take(k)
.map(|n| (n.idx, n.cmp_dist))
.collect()
}
fn metrics() -> Vec<DistanceCalculationMetric> {
vec![
DistanceCalculationMetric::Euclidean,
DistanceCalculationMetric::Manhattan,
DistanceCalculationMetric::Minkowski(3.0),
]
}
#[test]
fn radius_search_matches_brute_force() {
for (seed, &n) in [11_u64, 23, 37, 41]
.iter()
.zip([1_usize, 5, 30, 200].iter())
{
for &d in &[1_usize, 2, 3, 5] {
let mut rng = StdRng::seed_from_u64(seed * 100 + d as u64);
let points =
Array2::random_using((n, d), Uniform::new(-5.0, 5.0).unwrap(), &mut rng);
for metric in metrics() {
let tree = KdTree::build(points.view(), metric);
for qi in 0..n.min(6) {
for &radius in &[0.0, 0.5, 2.0, 6.0, 20.0] {
let q = points.row(qi);
let mut got = tree.radius_neighbors(q, radius);
got.sort_unstable();
let expected = brute_radius(&points, q, radius, metric);
assert_eq!(
got, expected,
"radius mismatch: metric={metric:?} n={n} d={d} qi={qi} r={radius}"
);
}
}
}
}
}
}
#[test]
fn knn_matches_brute_force() {
for (seed, &n) in [7_u64, 19, 53].iter().zip([1_usize, 12, 150].iter()) {
for &d in &[1_usize, 2, 4] {
let mut rng = StdRng::seed_from_u64(seed * 50 + d as u64);
let points =
Array2::random_using((n, d), Uniform::new(-3.0, 3.0).unwrap(), &mut rng);
for metric in metrics() {
let tree = KdTree::build(points.view(), metric);
let q =
Array2::random_using((1, d), Uniform::new(-3.0, 3.0).unwrap(), &mut rng);
let q = q.row(0);
for &k in &[1_usize, 3, 7, 200] {
let got = tree.k_nearest(q, k);
let expected = brute_knn(&points, q, k, metric);
let got_idx: Vec<usize> = got.iter().map(|&(i, _)| i).collect();
let exp_idx: Vec<usize> = expected.iter().map(|&(i, _)| i).collect();
assert_eq!(
got_idx, exp_idx,
"knn index mismatch: metric={metric:?} n={n} d={d} k={k}"
);
for (&(_, gc), &(_, ec)) in got.iter().zip(expected.iter()) {
assert!((gc - ec).abs() < 1e-12, "knn distance mismatch");
}
}
}
}
}
}
#[test]
fn handles_duplicate_points_deterministically() {
let points = Array2::from_shape_vec(
(6, 2),
vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0],
)
.unwrap();
let metric = DistanceCalculationMetric::Euclidean;
let tree = KdTree::build(points.view(), metric);
let q = points.row(0);
let mut got_r = tree.radius_neighbors(q, 1.5);
got_r.sort_unstable();
assert_eq!(got_r, brute_radius(&points, q, 1.5, metric));
for k in 1..=points.nrows() {
let got: Vec<usize> = tree.k_nearest(q, k).into_iter().map(|(i, _)| i).collect();
let exp: Vec<usize> = brute_knn(&points, q, k, metric)
.into_iter()
.map(|(i, _)| i)
.collect();
assert_eq!(got, exp, "duplicate-point knn mismatch at k={k}");
}
}
#[test]
fn single_point_tree() {
let points = Array2::from_shape_vec((1, 3), vec![1.0, 2.0, 3.0]).unwrap();
let metric = DistanceCalculationMetric::Euclidean;
let tree = KdTree::build(points.view(), metric);
assert_eq!(tree.radius_neighbors(points.row(0), 0.0), vec![0]);
assert_eq!(tree.k_nearest(points.row(0), 5), vec![(0, 0.0)]);
let far = ndarray::array![10.0, 10.0, 10.0];
assert!(tree.radius_neighbors(far.view(), 1.0).is_empty());
}
}