1use crate::error::{InterpolateError, InterpolateResult};
7use scirs2_core::parallel_ops::*;
8use std::collections::BinaryHeap;
9
10#[derive(Debug)]
16enum KdNode {
17 Leaf { point_indices: Vec<usize> },
19 Split {
21 axis: usize,
22 split_val: f64,
23 left: Box<KdNode>,
24 right: Box<KdNode>,
25 },
26}
27
28#[derive(PartialEq)]
37struct HeapEntry(f64, usize);
38
39impl Eq for HeapEntry {}
40
41impl PartialOrd for HeapEntry {
42 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
43 Some(self.cmp(other))
44 }
45}
46
47impl Ord for HeapEntry {
48 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
49 self.0
52 .partial_cmp(&other.0)
53 .unwrap_or(std::cmp::Ordering::Equal)
54 .then(other.1.cmp(&self.1))
55 }
56}
57
58#[derive(Debug, Clone)]
64pub struct KdQueryResult {
65 pub indices: Vec<usize>,
67 pub distances_sq: Vec<f64>,
69}
70
71pub struct GpuKdTree {
105 points: Vec<Vec<f64>>,
107 dim: usize,
109 root: Option<KdNode>,
111 leaf_size: usize,
113}
114
115impl GpuKdTree {
116 pub fn new(points: Vec<Vec<f64>>) -> InterpolateResult<Self> {
125 Self::with_leaf_size(points, 16)
126 }
127
128 pub fn with_leaf_size(points: Vec<Vec<f64>>, leaf_size: usize) -> InterpolateResult<Self> {
133 let leaf_size = leaf_size.max(1);
134
135 if points.is_empty() {
136 return Ok(Self {
137 points: Vec::new(),
138 dim: 0,
139 root: None,
140 leaf_size,
141 });
142 }
143
144 let dim = points[0].len();
145 for (i, p) in points.iter().enumerate() {
146 if p.len() != dim {
147 return Err(InterpolateError::InvalidInput {
148 message: format!("Point {i} has dimension {} but expected {dim}", p.len()),
149 });
150 }
151 }
152
153 let indices: Vec<usize> = (0..points.len()).collect();
154 let root = Some(build_node(&points, indices, dim, leaf_size));
155
156 Ok(Self {
157 points,
158 dim,
159 root,
160 leaf_size,
161 })
162 }
163
164 pub fn knn(&self, query: &[f64], k: usize) -> InterpolateResult<KdQueryResult> {
173 if query.len() != self.dim {
174 return Err(InterpolateError::InvalidInput {
175 message: format!(
176 "Query has dimension {} but tree has dimension {}",
177 query.len(),
178 self.dim
179 ),
180 });
181 }
182
183 if self.points.is_empty() || self.root.is_none() {
184 return Ok(KdQueryResult {
185 indices: Vec::new(),
186 distances_sq: Vec::new(),
187 });
188 }
189
190 let k_effective = k.min(self.points.len());
191 let mut heap: BinaryHeap<HeapEntry> = BinaryHeap::with_capacity(k_effective + 1);
192
193 if let Some(root) = &self.root {
194 search_knn(root, &self.points, query, k_effective, &mut heap);
195 }
196
197 let mut results: Vec<(f64, usize)> = heap.into_iter().map(|e| (e.0, e.1)).collect();
199 results.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
200
201 Ok(KdQueryResult {
202 indices: results.iter().map(|(_, i)| *i).collect(),
203 distances_sq: results.iter().map(|(d, _)| *d).collect(),
204 })
205 }
206
207 pub fn knn_batch(
213 &self,
214 queries: &[Vec<f64>],
215 k: usize,
216 ) -> InterpolateResult<Vec<KdQueryResult>> {
217 queries.into_par_iter().map(|q| self.knn(q, k)).collect()
218 }
219
220 pub fn n_points(&self) -> usize {
226 self.points.len()
227 }
228
229 pub fn dim(&self) -> usize {
231 self.dim
232 }
233}
234
235fn build_node(points: &[Vec<f64>], indices: Vec<usize>, dim: usize, leaf_size: usize) -> KdNode {
240 if indices.len() <= leaf_size {
241 return KdNode::Leaf {
242 point_indices: indices,
243 };
244 }
245
246 let axis = (0..dim)
248 .max_by(|&a, &b| {
249 let va = variance_along(points, &indices, a);
250 let vb = variance_along(points, &indices, b);
251 va.partial_cmp(&vb).unwrap_or(std::cmp::Ordering::Equal)
252 })
253 .unwrap_or(0);
254
255 let mut sorted = indices;
257 sorted.sort_unstable_by(|&i, &j| {
258 points[i][axis]
259 .partial_cmp(&points[j][axis])
260 .unwrap_or(std::cmp::Ordering::Equal)
261 });
262
263 let mid = sorted.len() / 2;
264 let split_val = points[sorted[mid]][axis];
265
266 let right_indices = sorted.split_off(mid);
267 let left_indices = sorted;
268
269 KdNode::Split {
270 axis,
271 split_val,
272 left: Box::new(build_node(points, left_indices, dim, leaf_size)),
273 right: Box::new(build_node(points, right_indices, dim, leaf_size)),
274 }
275}
276
277fn variance_along(points: &[Vec<f64>], indices: &[usize], axis: usize) -> f64 {
278 let n = indices.len() as f64;
279 if n < 2.0 {
280 return 0.0;
281 }
282 let mean = indices.iter().map(|&i| points[i][axis]).sum::<f64>() / n;
283 indices
284 .iter()
285 .map(|&i| (points[i][axis] - mean).powi(2))
286 .sum::<f64>()
287 / n
288}
289
290fn dist_sq(a: &[f64], b: &[f64]) -> f64 {
295 a.iter().zip(b.iter()).map(|(x, y)| (x - y).powi(2)).sum()
296}
297
298fn search_knn(
299 node: &KdNode,
300 points: &[Vec<f64>],
301 query: &[f64],
302 k: usize,
303 heap: &mut BinaryHeap<HeapEntry>,
304) {
305 match node {
306 KdNode::Leaf { point_indices } => {
307 for &idx in point_indices {
308 let d = dist_sq(query, &points[idx]);
309 maybe_push(heap, d, idx, k);
310 }
311 }
312 KdNode::Split {
313 axis,
314 split_val,
315 left,
316 right,
317 } => {
318 let diff = query[*axis] - split_val;
319 let (near, far) = if diff <= 0.0 {
320 (left.as_ref(), right.as_ref())
321 } else {
322 (right.as_ref(), left.as_ref())
323 };
324
325 search_knn(near, points, query, k, heap);
326
327 let worst_sq = heap.peek().map(|e| e.0).unwrap_or(f64::INFINITY);
329 if diff * diff < worst_sq || heap.len() < k {
330 search_knn(far, points, query, k, heap);
331 }
332 }
333 }
334}
335
336fn maybe_push(heap: &mut BinaryHeap<HeapEntry>, d: f64, idx: usize, k: usize) {
338 if heap.len() < k {
339 heap.push(HeapEntry(d, idx));
340 } else if let Some(top) = heap.peek() {
341 if d < top.0 {
342 heap.pop();
343 heap.push(HeapEntry(d, idx));
344 }
345 }
346}
347
348#[cfg(test)]
353mod tests {
354 use super::*;
355
356 fn brute_force_knn(points: &[Vec<f64>], query: &[f64], k: usize) -> Vec<usize> {
358 let mut dists: Vec<(f64, usize)> = points
359 .iter()
360 .enumerate()
361 .map(|(i, p)| {
362 let d = p.iter().zip(query).map(|(a, b)| (a - b).powi(2)).sum();
363 (d, i)
364 })
365 .collect();
366 dists.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
367 dists[..k.min(dists.len())]
368 .iter()
369 .map(|(_, i)| *i)
370 .collect()
371 }
372
373 #[test]
374 fn test_kdtree_empty_returns_empty() {
375 let tree = GpuKdTree::new(vec![]).expect("build empty");
376 let res = tree.knn(&[], 1).expect("query empty");
377 assert!(res.indices.is_empty());
378 assert!(res.distances_sq.is_empty());
379 }
380
381 #[test]
382 fn test_kdtree_single_point() {
383 let pts = vec![vec![3.0_f64, 4.0]];
384 let tree = GpuKdTree::new(pts).expect("build single");
385 let res = tree.knn(&[0.0, 0.0], 1).expect("query");
386 assert_eq!(res.indices.len(), 1);
387 assert_eq!(res.indices[0], 0);
388 let expected_d = 25.0_f64;
390 assert!((res.distances_sq[0] - expected_d).abs() < 1e-12);
391 }
392
393 #[test]
394 fn test_kdtree_1d_finds_nearest() {
395 let pts: Vec<Vec<f64>> = vec![vec![0.0], vec![1.0], vec![2.0], vec![3.0], vec![4.0]];
396 let tree = GpuKdTree::new(pts.clone()).expect("build 1d");
397 let res = tree.knn(&[2.5], 1).expect("query 1d");
398 assert!(
400 res.indices[0] == 2 || res.indices[0] == 3,
401 "expected index 2 or 3, got {}",
402 res.indices[0]
403 );
404 assert!((res.distances_sq[0] - 0.25).abs() < 1e-12);
405 }
406
407 #[test]
408 fn test_kdtree_2d_knn_k3() {
409 let pts: Vec<Vec<f64>> = (0..3)
412 .flat_map(|r: i32| (0..3).map(move |c: i32| vec![c as f64, r as f64]))
413 .collect();
414 let tree = GpuKdTree::new(pts.clone()).expect("build 2d grid");
415
416 let res = tree.knn(&[1.0, 1.0], 3).expect("knn 3 at center");
418 assert_eq!(res.indices.len(), 3);
419 assert_eq!(res.indices[0], 4);
421 assert!(
423 (res.distances_sq[1] - 1.0).abs() < 1e-12 || (res.distances_sq[2] - 1.0).abs() < 1e-12,
424 "distances: {:?}",
425 res.distances_sq
426 );
427 }
428
429 #[test]
430 fn test_kdtree_knn_batch_matches_brute_force() {
431 use scirs2_core::random::{rngs::StdRng, RngExt, SeedableRng};
432 let mut rng = StdRng::seed_from_u64(42);
433
434 let n = 50_usize;
435 let dim = 3_usize;
436 let pts: Vec<Vec<f64>> = (0..n)
437 .map(|_| (0..dim).map(|_| rng.random::<f64>()).collect())
438 .collect();
439
440 let tree = GpuKdTree::new(pts.clone()).expect("build 3d");
441
442 let queries: Vec<Vec<f64>> = (0..20)
443 .map(|_| (0..dim).map(|_| rng.random::<f64>()).collect())
444 .collect();
445
446 let k = 5;
447 let batch = tree.knn_batch(&queries, k).expect("batch knn");
448 assert_eq!(batch.len(), queries.len());
449
450 for (q_idx, (res, q)) in batch.iter().zip(queries.iter()).enumerate() {
451 let expected = brute_force_knn(&pts, q, k);
452 let mut got = res.indices.clone();
454 let mut exp = expected.clone();
455 got.sort_unstable();
456 exp.sort_unstable();
457 assert_eq!(got, exp, "query {q_idx}: tree={got:?} brute={exp:?}");
458 }
459 }
460
461 #[test]
462 fn test_kdtree_dimension_mismatch_errors() {
463 let pts = vec![vec![1.0_f64, 2.0], vec![3.0, 4.0]];
464 let tree = GpuKdTree::new(pts).expect("build");
465 let err = tree.knn(&[0.0], 1);
466 assert!(err.is_err(), "should error on dimension mismatch");
467 }
468
469 #[test]
470 fn test_kdtree_dimension_mismatch_on_build() {
471 let pts = vec![vec![1.0_f64, 2.0], vec![3.0, 4.0, 5.0]];
472 let err = GpuKdTree::new(pts);
473 assert!(err.is_err(), "should error when points have different dims");
474 }
475
476 #[test]
477 fn test_kdtree_high_dim_correct() {
478 use scirs2_core::random::{rngs::StdRng, RngExt, SeedableRng};
479 let mut rng = StdRng::seed_from_u64(99);
480
481 let n = 100_usize;
482 let dim = 10_usize;
483 let pts: Vec<Vec<f64>> = (0..n)
484 .map(|_| (0..dim).map(|_| rng.random::<f64>()).collect())
485 .collect();
486
487 let tree = GpuKdTree::new(pts.clone()).expect("build 10d");
488 let q: Vec<f64> = (0..dim).map(|_| rng.random::<f64>()).collect();
489 let k = 7;
490
491 let tree_res = tree.knn(&q, k).expect("knn 10d");
492 let brute_res = brute_force_knn(&pts, &q, k);
493
494 let mut got = tree_res.indices.clone();
495 let mut exp = brute_res;
496 got.sort_unstable();
497 exp.sort_unstable();
498 assert_eq!(got, exp, "10-D: tree={got:?} brute={exp:?}");
499 }
500
501 #[test]
502 fn test_kdtree_k_larger_than_n() {
503 let pts = vec![vec![0.0_f64], vec![1.0], vec![2.0]];
504 let tree = GpuKdTree::new(pts).expect("build");
505 let res = tree.knn(&[1.5], 100).expect("k > n");
506 assert_eq!(res.indices.len(), 3);
508 }
509
510 #[test]
511 fn test_kdtree_distances_are_sorted_ascending() {
512 use scirs2_core::random::{rngs::StdRng, RngExt, SeedableRng};
513 let mut rng = StdRng::seed_from_u64(7);
514
515 let pts: Vec<Vec<f64>> = (0..30)
516 .map(|_| vec![rng.random::<f64>(), rng.random::<f64>()])
517 .collect();
518 let tree = GpuKdTree::new(pts).expect("build");
519 let q = vec![0.5, 0.5];
520 let res = tree.knn(&q, 10).expect("knn 10");
521 for w in res.distances_sq.windows(2) {
522 assert!(w[0] <= w[1], "distances not sorted: {:?}", res.distances_sq);
523 }
524 }
525}