Skip to main content

scirs2_interpolate/spatial/
optimized_search.rs

1//! Optimized spatial search algorithms with enhanced performance features
2//!
3//! This module provides advanced spatial search optimizations including:
4//! - SIMD-accelerated distance computations (via scirs2-core)
5//! - Cache-friendly memory layouts
6//! - Adaptive search strategies
7//! - Batch query processing
8//! - Multi-threaded search operations
9//!
10//! All SIMD operations are delegated to scirs2-core's unified SIMD abstraction layer
11//! in compliance with the project-wide SIMD policy.
12
13use 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
25/// Enhanced spatial search interface with multiple optimization strategies
26pub trait OptimizedSpatialSearch<F: Float> {
27    /// Perform batch k-nearest neighbor search for multiple queries
28    fn batch_k_nearest_neighbors(
29        &self,
30        queries: &ArrayView2<F>,
31        k: usize,
32    ) -> InterpolateResult<Vec<Vec<(usize, F)>>>;
33
34    /// Perform parallel k-nearest neighbor search
35    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    /// Adaptive k-nearest neighbor search that adjusts strategy based on query characteristics
43    fn adaptive_k_nearest_neighbors(
44        &self,
45        query: &[F],
46        k: usize,
47    ) -> InterpolateResult<Vec<(usize, F)>>;
48
49    /// Range search with multiple radii for the same query point
50    fn multi_radius_search(
51        &self,
52        query: &[F],
53        radii: &[F],
54    ) -> InterpolateResult<Vec<Vec<(usize, F)>>>;
55}
56
57/// SIMD-accelerated distance computation utilities
58pub struct SimdDistanceOps;
59
60impl SimdDistanceOps {
61    /// Compute squared Euclidean distance using SIMD operations when available
62    #[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    /// Enhanced batch distance computation with SIMD optimization for better memory access patterns
83    #[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                // Process in chunks for better cache utilization
103                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                        // Use SIMD-optimized distance calculation
112                        let distance = if dim >= 8 {
113                            // For higher dimensions, use vectorized operations
114                            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                            // Fallback for lower dimensions
119                            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                // Non-SIMD fallback
130                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    /// SIMD-optimized parallel batch processing for very large datasets
147    #[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        // Process queries sequentially for now
159        (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    /// Compute squared Euclidean distance without SIMD
168    #[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    /// Batch compute distances from multiple points to a single query
185    #[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    /// Batch compute distances without SIMD
200    #[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/// Cache-friendly kNN search with distance precomputation
216#[allow(dead_code)]
217pub struct CacheFriendlyKNN<F: Float> {
218    /// Maximum number of distances to cache
219    cache_size: usize,
220    /// Phantom data for type parameter
221    _phantom: std::marker::PhantomData<F>,
222}
223
224impl<F: Float + FromPrimitive> CacheFriendlyKNN<F> {
225    /// Create a new cache-friendly kNN searcher
226    pub fn new(cachesize: usize) -> Self {
227        Self {
228            cache_size: cachesize,
229            _phantom: std::marker::PhantomData,
230        }
231    }
232
233    /// Find k nearest neighbors with caching strategy
234    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        // Use adaptive strategy for small k
244        if k <= 10 {
245            searcher.adaptive_k_nearest_neighbors(query, k)
246        } else {
247            // For larger k, use standard search
248            // This is a placeholder - actual implementation would depend on the searcher
249            searcher.adaptive_k_nearest_neighbors(query, k)
250        }
251    }
252}
253
254/// Parallel batch query processor
255#[cfg(feature = "parallel")]
256pub struct ParallelQueryProcessor<F: Float> {
257    /// Number of worker threads
258    num_workers: usize,
259    /// Phantom data for type parameter
260    _phantom: std::marker::PhantomData<F>,
261}
262
263#[cfg(feature = "parallel")]
264impl<F: Float + FromPrimitive + Send + Sync> ParallelQueryProcessor<F> {
265    /// Create a new parallel query processor
266    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    /// Process queries in parallel
276    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
289/// Default implementation of OptimizedSpatialSearch for KdTree
290impl<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        // Fallback to sequential processing
338        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        // For now, just use the standard k-nearest neighbors
347        // A more sophisticated implementation could choose different strategies
348        // based on k, dimension, and data characteristics
349        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
364/// Default implementation of OptimizedSpatialSearch for BallTree
365impl<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        // Fallback to sequential processing
413        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); // (1-0)^2 + (2-0)^2 = 5
459        assert_eq!(distances[1], 25.0); // (3-0)^2 + (4-0)^2 = 25
460        assert_eq!(distances[2], 61.0); // (5-0)^2 + (6-0)^2 = 61
461    }
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    /// Recursively invokes `SimdDistanceOps::squared_euclidean_distance` (the SIMD-enabled
477    /// path) at real Rust call-stack recursion depth (not a loop). Exists purely to
478    /// stress-test the `#[inline(never)]` mitigation applied to the underlying SIMD leaf
479    /// kernels in scirs2-core (`simd/distances.rs`): the originally hypothesized failure
480    /// mode was that a deeply recursive caller (e.g. KdTree/BallTree descent) duplicates
481    /// the kernels' wide `__m256`/`__m256d` stack frames at every recursion level.
482    #[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    /// STRESS TEST: deep, real recursion (not a token 3-level test) calling the
496    /// SIMD-enabled `squared_euclidean_distance` at every level, run inside a thread with
497    /// an explicit, bounded stack. Positively confirms no stack overflow occurs with SIMD
498    /// enabled — this is the regression guard for the `#[inline(never)]` precautionary
499    /// mitigation on `simd_distance_squared_euclidean_f32/f64`.
500    #[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; // 64 MiB: explicit, deterministic budget
506
507        // f64 path
508        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        // f32 path
525        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    /// STRESS TEST: large-scale (>=10,000 points), realistic-depth KdTree/BallTree
543    /// build+query combined with direct `SimdDistanceOps` batch calls, complementing the
544    /// deep-recursion test above with a large, real-world-shaped workload.
545    ///
546    /// NOTE (KdTree correctness, out of scope here): while developing this test,
547    /// `KdTree::k_nearest_neighbors` was found to return a non-minimal nearest-neighbor
548    /// distance at this scale. Root cause (confirmed by inspection of
549    /// `spatial/kdtree.rs::build_subtree`, the `n_points <= self.leaf_size` branch): a
550    /// leaf node stores only `indices[0]` — the other up to `leaf_size - 1` points in
551    /// that partition are never inserted into the tree and can never be returned by any
552    /// query. Every pre-existing KdTree test uses <= 5 points (below the default
553    /// `leaf_size` of 10), so all of them take the `linear_k_nearest_neighbors` fallback
554    /// and never exercise `build_subtree`'s recursive path, which is presumably why this
555    /// has gone uncaught. This is a real, separate correctness bug outside this SIMD-
556    /// surfacing item's file list (`kdtree.rs` is not touched here) and is flagged for a
557    /// dedicated follow-up rather than fixed inline. `BallTree` does not share this bug —
558    /// its leaf nodes retain all member indices (`BallNode.indices: Vec<usize>`) and its
559    /// `search_k_nearest` iterates all of them — so only `BallTree`'s answer is
560    /// cross-checked against the brute-force SIMD minimum below. `KdTree` is still built
561    /// and queried here to confirm it does not crash/overflow the stack at this scale,
562    /// which is what this test is actually chartered to prove.
563    #[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        // Deterministic LCG-based point generation (matches the project's established
572        // reproducible-PRNG idiom elsewhere in this crate; avoids a `rand` dev-dependency).
573        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        // Build both tree types at realistic depth (~log2(12_000) ~= 14 levels).
585        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            // Exercise the real recursive tree descent at realistic depth for BOTH trees
592            // (this "does it crash/overflow" check is what this test is chartered to
593            // prove; see the KdTree correctness note on the test above for why only
594            // BallTree's *answer* is cross-checked below).
595            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            // Exercise SimdDistanceOps::squared_euclidean_distance directly against every
605            // point at this scale (batch_distances_to_query delegates to it per-row).
606            let distances = SimdDistanceOps::batch_distances_to_query(&points.view(), query_slice);
607            assert_eq!(distances.len(), N_POINTS);
608
609            // Cross-check: BallTree's best (sqrt'd Euclidean) neighbor distance, squared,
610            // should match the minimum of the directly SIMD-computed squared distances.
611            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}