1use crate::error::{InterpolateError, InterpolateResult};
14use crate::spatial::{BallTree, KdTree};
15use scirs2_core::ndarray::{ArrayView2, Axis};
16
17#[cfg(feature = "simd")]
18use scirs2_core::ndarray::{Array1, ArrayView1};
19use scirs2_core::numeric::{Float, FromPrimitive};
20use std::fmt::Debug;
21
22#[cfg(feature = "simd")]
23use scirs2_core::simd_ops::SimdUnifiedOps;
24
25pub trait OptimizedSpatialSearch<F: Float> {
27 fn batch_k_nearest_neighbors(
29 &self,
30 queries: &ArrayView2<F>,
31 k: usize,
32 ) -> InterpolateResult<Vec<Vec<(usize, F)>>>;
33
34 fn parallel_k_nearest_neighbors(
36 &self,
37 queries: &ArrayView2<F>,
38 k: usize,
39 workers: Option<usize>,
40 ) -> InterpolateResult<Vec<Vec<(usize, F)>>>;
41
42 fn adaptive_k_nearest_neighbors(
44 &self,
45 query: &[F],
46 k: usize,
47 ) -> InterpolateResult<Vec<(usize, F)>>;
48
49 fn multi_radius_search(
51 &self,
52 query: &[F],
53 radii: &[F],
54 ) -> InterpolateResult<Vec<Vec<(usize, F)>>>;
55}
56
57pub struct SimdDistanceOps;
59
60impl SimdDistanceOps {
61 #[cfg(feature = "simd")]
63 pub fn squared_euclidean_distance<F>(a: &[F], b: &[F]) -> F
64 where
65 F: Float + FromPrimitive + SimdUnifiedOps,
66 {
67 assert_eq!(a.len(), b.len(), "Vectors must have the same dimension");
68
69 if F::simd_available() {
70 F::simd_distance_squared_euclidean(&ArrayView1::from(a), &ArrayView1::from(b))
71 } else {
72 a.iter()
73 .zip(b.iter())
74 .map(|(&x, &y)| {
75 let diff = x - y;
76 diff * diff
77 })
78 .fold(F::zero(), |acc, x| acc + x)
79 }
80 }
81
82 #[cfg(feature = "simd")]
84 pub fn enhanced_batch_distances<F>(
85 points: &ArrayView2<F>,
86 queries: &ArrayView2<F>,
87 ) -> Vec<Vec<F>>
88 where
89 F: Float + FromPrimitive + SimdUnifiedOps + Debug,
90 {
91 let n_queries = queries.nrows();
92 let n_points = points.nrows();
93 let dim = points.ncols();
94
95 let mut results = Vec::with_capacity(n_queries);
96
97 for query_idx in 0..n_queries {
98 let query = queries.row(query_idx);
99 let mut distances = Vec::with_capacity(n_points);
100
101 if F::simd_available() && dim >= 4 && n_points >= 8 {
102 const CHUNK_SIZE: usize = 16;
104
105 for chunk_start in (0..n_points).step_by(CHUNK_SIZE) {
106 let chunk_end = (chunk_start + CHUNK_SIZE).min(n_points);
107
108 for point_idx in chunk_start..chunk_end {
109 let point = points.row(point_idx);
110
111 let distance = if dim >= 8 {
113 let diff = F::simd_sub(&point, &query);
115 let squared = F::simd_mul(&diff.view(), &diff.view());
116 F::simd_sum(&squared.view())
117 } else {
118 Self::squared_euclidean_distance(
120 point.as_slice().expect("Operation failed"),
121 query.as_slice().expect("Operation failed"),
122 )
123 };
124
125 distances.push(distance);
126 }
127 }
128 } else {
129 for point_idx in 0..n_points {
131 let point = points.row(point_idx);
132 let distance = Self::squared_euclidean_distance(
133 point.as_slice().expect("Operation failed"),
134 query.as_slice().expect("Operation failed"),
135 );
136 distances.push(distance);
137 }
138 }
139
140 results.push(distances);
141 }
142
143 results
144 }
145
146 #[cfg(all(feature = "simd", feature = "parallel"))]
148 pub fn parallel_enhanced_batch_distances<F>(
149 points: &ArrayView2<F>,
150 queries: &ArrayView2<F>,
151 _num_threads: Option<usize>,
152 ) -> Vec<Vec<F>>
153 where
154 F: Float + FromPrimitive + SimdUnifiedOps + Debug + Send + Sync,
155 {
156 let n_queries = queries.nrows();
157
158 (0..n_queries)
160 .map(|query_idx| {
161 let query = queries.row(query_idx);
162 Self::batch_distances_to_query(points, query.as_slice().expect("Operation failed"))
163 })
164 .collect()
165 }
166
167 #[cfg(not(feature = "simd"))]
169 pub fn squared_euclidean_distance<F>(a: &[F], b: &[F]) -> F
170 where
171 F: Float + FromPrimitive,
172 {
173 assert_eq!(a.len(), b.len(), "Vectors must have the same dimension");
174
175 a.iter()
176 .zip(b.iter())
177 .map(|(&x, &y)| {
178 let diff = x - y;
179 diff * diff
180 })
181 .fold(F::zero(), |acc, x| acc + x)
182 }
183
184 #[cfg(feature = "simd")]
186 pub fn batch_distances_to_query<F>(points: &ArrayView2<F>, query: &[F]) -> Vec<F>
187 where
188 F: Float + FromPrimitive + SimdUnifiedOps,
189 {
190 points
191 .axis_iter(Axis(0))
192 .map(|point| {
193 let point_slice = point.as_slice().expect("Operation failed");
194 Self::squared_euclidean_distance(point_slice, query)
195 })
196 .collect()
197 }
198
199 #[cfg(not(feature = "simd"))]
201 pub fn batch_distances_to_query<F>(points: &ArrayView2<F>, query: &[F]) -> Vec<F>
202 where
203 F: Float + FromPrimitive,
204 {
205 points
206 .axis_iter(Axis(0))
207 .map(|point| {
208 let point_slice = point.as_slice().expect("Operation failed");
209 Self::squared_euclidean_distance(point_slice, query)
210 })
211 .collect()
212 }
213}
214
215#[allow(dead_code)]
217pub struct CacheFriendlyKNN<F: Float> {
218 cache_size: usize,
220 _phantom: std::marker::PhantomData<F>,
222}
223
224impl<F: Float + FromPrimitive> CacheFriendlyKNN<F> {
225 pub fn new(cachesize: usize) -> Self {
227 Self {
228 cache_size: cachesize,
229 _phantom: std::marker::PhantomData,
230 }
231 }
232
233 pub fn find_k_nearest<S>(
235 &self,
236 searcher: &S,
237 query: &[F],
238 k: usize,
239 ) -> InterpolateResult<Vec<(usize, F)>>
240 where
241 S: OptimizedSpatialSearch<F>,
242 {
243 if k <= 10 {
245 searcher.adaptive_k_nearest_neighbors(query, k)
246 } else {
247 searcher.adaptive_k_nearest_neighbors(query, k)
250 }
251 }
252}
253
254#[cfg(feature = "parallel")]
256pub struct ParallelQueryProcessor<F: Float> {
257 num_workers: usize,
259 _phantom: std::marker::PhantomData<F>,
261}
262
263#[cfg(feature = "parallel")]
264impl<F: Float + FromPrimitive + Send + Sync> ParallelQueryProcessor<F> {
265 pub fn new(num_workers: Option<usize>) -> Self {
267 use scirs2_core::parallel_ops::num_threads;
268
269 Self {
270 num_workers: num_workers.unwrap_or_else(num_threads),
271 _phantom: std::marker::PhantomData,
272 }
273 }
274
275 pub fn process_queries<S>(
277 &self,
278 searcher: &S,
279 queries: &ArrayView2<F>,
280 k: usize,
281 ) -> InterpolateResult<Vec<Vec<(usize, F)>>>
282 where
283 S: OptimizedSpatialSearch<F> + Sync,
284 {
285 searcher.parallel_k_nearest_neighbors(queries, k, Some(self.num_workers))
286 }
287}
288
289impl<F> OptimizedSpatialSearch<F> for KdTree<F>
291where
292 F: Float + FromPrimitive + Debug + Send + Sync + ordered_float::FloatCore,
293{
294 fn batch_k_nearest_neighbors(
295 &self,
296 queries: &ArrayView2<F>,
297 k: usize,
298 ) -> InterpolateResult<Vec<Vec<(usize, F)>>> {
299 queries
300 .axis_iter(Axis(0))
301 .map(|query| {
302 let query_slice = query.as_slice().expect("Operation failed");
303 self.k_nearest_neighbors(query_slice, k)
304 })
305 .collect()
306 }
307
308 #[cfg(feature = "parallel")]
309 fn parallel_k_nearest_neighbors(
310 &self,
311 queries: &ArrayView2<F>,
312 k: usize,
313 workers: Option<usize>,
314 ) -> InterpolateResult<Vec<Vec<(usize, F)>>> {
315 use scirs2_core::parallel_ops::*;
316
317 let queries_vec: Vec<_> = queries.axis_iter(Axis(0)).collect();
318
319 par_scope(|_| {
320 queries_vec
321 .into_par_iter()
322 .map(|query| {
323 let query_slice = query.as_slice().expect("Operation failed");
324 self.k_nearest_neighbors(query_slice, k)
325 })
326 .collect::<Result<Vec<_>, InterpolateError>>()
327 })
328 }
329
330 #[cfg(not(feature = "parallel"))]
331 fn parallel_k_nearest_neighbors(
332 &self,
333 queries: &ArrayView2<F>,
334 k: usize,
335 workers: Option<usize>,
336 ) -> InterpolateResult<Vec<Vec<(usize, F)>>> {
337 self.batch_k_nearest_neighbors(queries, k)
339 }
340
341 fn adaptive_k_nearest_neighbors(
342 &self,
343 query: &[F],
344 k: usize,
345 ) -> InterpolateResult<Vec<(usize, F)>> {
346 self.k_nearest_neighbors(query, k)
350 }
351
352 fn multi_radius_search(
353 &self,
354 query: &[F],
355 radii: &[F],
356 ) -> InterpolateResult<Vec<Vec<(usize, F)>>> {
357 radii
358 .iter()
359 .map(|&radius| self.radius_neighbors(query, radius))
360 .collect()
361 }
362}
363
364impl<F> OptimizedSpatialSearch<F> for BallTree<F>
366where
367 F: Float + FromPrimitive + Debug + Send + Sync + ordered_float::FloatCore,
368{
369 fn batch_k_nearest_neighbors(
370 &self,
371 queries: &ArrayView2<F>,
372 k: usize,
373 ) -> InterpolateResult<Vec<Vec<(usize, F)>>> {
374 queries
375 .axis_iter(Axis(0))
376 .map(|query| {
377 let query_slice = query.as_slice().expect("Operation failed");
378 self.k_nearest_neighbors(query_slice, k)
379 })
380 .collect()
381 }
382
383 #[cfg(feature = "parallel")]
384 fn parallel_k_nearest_neighbors(
385 &self,
386 queries: &ArrayView2<F>,
387 k: usize,
388 workers: Option<usize>,
389 ) -> InterpolateResult<Vec<Vec<(usize, F)>>> {
390 use scirs2_core::parallel_ops::*;
391
392 let queries_vec: Vec<_> = queries.axis_iter(Axis(0)).collect();
393
394 par_scope(|_| {
395 queries_vec
396 .into_par_iter()
397 .map(|query| {
398 let query_slice = query.as_slice().expect("Operation failed");
399 self.k_nearest_neighbors(query_slice, k)
400 })
401 .collect::<Result<Vec<_>, InterpolateError>>()
402 })
403 }
404
405 #[cfg(not(feature = "parallel"))]
406 fn parallel_k_nearest_neighbors(
407 &self,
408 queries: &ArrayView2<F>,
409 k: usize,
410 workers: Option<usize>,
411 ) -> InterpolateResult<Vec<Vec<(usize, F)>>> {
412 self.batch_k_nearest_neighbors(queries, k)
414 }
415
416 fn adaptive_k_nearest_neighbors(
417 &self,
418 query: &[F],
419 k: usize,
420 ) -> InterpolateResult<Vec<(usize, F)>> {
421 self.k_nearest_neighbors(query, k)
422 }
423
424 fn multi_radius_search(
425 &self,
426 query: &[F],
427 radii: &[F],
428 ) -> InterpolateResult<Vec<Vec<(usize, F)>>> {
429 radii
430 .iter()
431 .map(|&radius| self.radius_neighbors(query, radius))
432 .collect()
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use scirs2_core::ndarray::array;
440
441 #[test]
442 fn test_simd_distance_ops() {
443 let a = vec![1.0, 2.0, 3.0, 4.0];
444 let b = vec![2.0, 3.0, 4.0, 5.0];
445
446 let distance = SimdDistanceOps::squared_euclidean_distance(&a, &b);
447 assert_eq!(distance, 4.0);
448 }
449
450 #[test]
451 fn test_batch_distances() {
452 let points = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
453 let query = vec![0.0, 0.0];
454
455 let distances = SimdDistanceOps::batch_distances_to_query(&points.view(), &query);
456
457 assert_eq!(distances.len(), 3);
458 assert_eq!(distances[0], 5.0); assert_eq!(distances[1], 25.0); assert_eq!(distances[2], 61.0); }
462
463 #[test]
464 fn test_cache_friendly_knn() {
465 let knn = CacheFriendlyKNN::<f64>::new(1000);
466 assert_eq!(knn.cache_size, 1000);
467 }
468
469 #[cfg(feature = "parallel")]
470 #[test]
471 fn test_parallel_query_processor() {
472 let processor = ParallelQueryProcessor::<f64>::new(Some(4));
473 assert_eq!(processor.num_workers, 4);
474 }
475
476 #[cfg(feature = "simd")]
483 fn recursive_squared_distance_probe<F>(depth: usize, a: &[F], b: &[F], acc: F) -> F
484 where
485 F: Float + FromPrimitive + SimdUnifiedOps,
486 {
487 let d = SimdDistanceOps::squared_euclidean_distance(a, b);
488 if depth == 0 {
489 acc + d
490 } else {
491 recursive_squared_distance_probe(depth - 1, a, b, acc + d)
492 }
493 }
494
495 #[cfg(feature = "simd")]
501 #[test]
502 fn test_squared_euclidean_distance_deep_recursion_stress() {
503 const DEPTH: usize = 100_000;
504 const DIM: usize = 64;
505 const STACK_SIZE: usize = 64 * 1024 * 1024; let a64: Vec<f64> = (0..DIM).map(|i| i as f64).collect();
509 let b64: Vec<f64> = (0..DIM).map(|i| i as f64 + 1.0).collect();
510 let handle64 = std::thread::Builder::new()
511 .name("sq-euclid-recursion-stress-f64".to_string())
512 .stack_size(STACK_SIZE)
513 .spawn(move || recursive_squared_distance_probe(DEPTH, &a64, &b64, 0.0f64))
514 .expect("failed to spawn f64 stress-test thread");
515 let total64 = handle64.join().expect(
516 "deep recursive squared_euclidean_distance (f64, SIMD-enabled) overflowed the stack",
517 );
518 let expected64 = (DEPTH as f64 + 1.0) * (DIM as f64);
519 assert!(
520 (total64 - expected64).abs() < 1e-6,
521 "f64 stress result mismatch: got {total64}, expected {expected64}"
522 );
523
524 let a32: Vec<f32> = (0..DIM).map(|i| i as f32).collect();
526 let b32: Vec<f32> = (0..DIM).map(|i| i as f32 + 1.0).collect();
527 let handle32 = std::thread::Builder::new()
528 .name("sq-euclid-recursion-stress-f32".to_string())
529 .stack_size(STACK_SIZE)
530 .spawn(move || recursive_squared_distance_probe(DEPTH, &a32, &b32, 0.0f32))
531 .expect("failed to spawn f32 stress-test thread");
532 let total32 = handle32.join().expect(
533 "deep recursive squared_euclidean_distance (f32, SIMD-enabled) overflowed the stack",
534 );
535 let expected32 = (DEPTH as f32 + 1.0) * (DIM as f32);
536 assert!(
537 (total32 - expected32).abs() < 1e-3,
538 "f32 stress result mismatch: got {total32}, expected {expected32}"
539 );
540 }
541
542 #[cfg(feature = "simd")]
564 #[test]
565 fn test_squared_euclidean_distance_large_kdtree_balltree_stress() {
566 use scirs2_core::ndarray::Array2;
567
568 const N_POINTS: usize = 12_000;
569 const DIM: usize = 8;
570
571 let mut state: u64 = 0x2545_F491_4F6C_DD1D;
574 let mut next_f64 = || -> f64 {
575 state = state
576 .wrapping_mul(6_364_136_223_846_793_005)
577 .wrapping_add(1_442_695_040_888_963_407);
578 ((state >> 11) as f64) / ((1u64 << 53) as f64)
579 };
580
581 let points = Array2::from_shape_fn((N_POINTS, DIM), |_| next_f64() * 100.0);
582 let query_points = Array2::from_shape_fn((32, DIM), |_| next_f64() * 100.0);
583
584 let kdtree = KdTree::new(points.clone()).expect("KdTree build should succeed");
586 let balltree = BallTree::new(points.clone()).expect("BallTree build should succeed");
587
588 for query in query_points.axis_iter(Axis(0)) {
589 let query_slice = query.as_slice().expect("contiguous query row");
590
591 let kd_neighbors = kdtree
596 .k_nearest_neighbors(query_slice, 10)
597 .expect("KdTree k-NN should succeed");
598 let ball_neighbors = balltree
599 .k_nearest_neighbors(query_slice, 10)
600 .expect("BallTree k-NN should succeed");
601 assert_eq!(kd_neighbors.len(), 10);
602 assert_eq!(ball_neighbors.len(), 10);
603
604 let distances = SimdDistanceOps::batch_distances_to_query(&points.view(), query_slice);
607 assert_eq!(distances.len(), N_POINTS);
608
609 let min_direct = distances.iter().cloned().fold(f64::INFINITY, f64::min);
612 let ball_best_dist_sq = ball_neighbors[0].1.powi(2);
613 assert!(
614 (ball_best_dist_sq - min_direct).abs() < 1e-6,
615 "BallTree best squared dist {ball_best_dist_sq} should match direct SIMD min {min_direct}"
616 );
617 }
618 }
619}