1use crate::error::{SpatialError, SpatialResult};
43use crate::memory_pool::DistancePool;
44use scirs2_core::ndarray::{Array2, ArrayView1, ArrayView2};
45use scirs2_core::parallel_ops::*;
46use scirs2_core::simd_ops::{PlatformCapabilities, SimdUnifiedOps};
47use std::cmp::Ordering;
48use std::collections::BinaryHeap;
49use std::sync::Arc;
50
51#[derive(Debug, Clone)]
53pub struct KDTreeConfig {
54 pub cache_aware_layout: bool,
56 pub vectorized_search: bool,
58 pub numa_aware: bool,
60 pub leaf_size: usize,
62 pub cache_line_size: usize,
64 pub parallel_construction: bool,
66 pub parallel_threshold: usize,
68 pub use_memory_pools: bool,
70 pub enable_prefetching: bool,
72}
73
74impl Default for KDTreeConfig {
75 fn default() -> Self {
76 Self::new()
77 }
78}
79
80impl KDTreeConfig {
81 pub fn new() -> Self {
83 Self {
84 cache_aware_layout: true,
85 vectorized_search: true,
86 numa_aware: true,
87 leaf_size: 32, cache_line_size: 64, parallel_construction: true,
90 parallel_threshold: 1000,
91 use_memory_pools: true,
92 enable_prefetching: true,
93 }
94 }
95
96 pub fn with_cache_aware_layout(mut self, enabled: bool) -> Self {
98 self.cache_aware_layout = enabled;
99 self
100 }
101
102 pub fn with_vectorized_search(mut self, enabled: bool) -> Self {
104 self.vectorized_search = enabled;
105 self
106 }
107
108 pub fn with_numa_aware(mut self, enabled: bool) -> Self {
110 self.numa_aware = enabled;
111 self
112 }
113
114 pub fn with_leaf_size(mut self, leafsize: usize) -> Self {
116 self.leaf_size = leafsize;
117 self
118 }
119
120 pub fn with_parallel_construction(&mut self, enabled: bool, threshold: usize) -> &mut Self {
122 self.parallel_construction = enabled;
123 self.parallel_threshold = threshold;
124 self
125 }
126
127 pub fn with_memory_pools(mut self, enabled: bool) -> Self {
129 self.use_memory_pools = enabled;
130 self
131 }
132}
133
134pub struct AdvancedKDTree {
136 nodes: Vec<AdvancedKDNode>,
138 points: Array2<f64>,
140 config: KDTreeConfig,
142 root_index: Option<usize>,
144 stats: TreeStatistics,
146 #[allow(dead_code)]
148 memory_pool: Arc<DistancePool>,
149}
150
151#[derive(Debug, Clone)]
153pub struct AdvancedKDNode {
154 point_index: u32,
156 splitting_dimension: u8,
158 node_info: NodeInfo,
160 bounding_box: Option<BoundingBox>,
162}
163
164#[derive(Debug, Clone)]
166pub struct NodeInfo {
167 left_child: u32,
169 right_child: u32,
171 is_leaf: bool,
173 #[allow(dead_code)]
175 subtree_size: u32,
176}
177
178#[derive(Debug, Clone)]
180pub struct BoundingBox {
181 min_coords: [f64; 8], max_coords: [f64; 8],
185 dimensions: usize,
187}
188
189impl BoundingBox {
190 fn new(dimensions: usize) -> Self {
191 assert!(dimensions <= 8, "BoundingBox supports up to 8 dimensions");
192 Self {
193 min_coords: [f64::INFINITY; 8],
194 max_coords: [f64::NEG_INFINITY; 8],
195 dimensions,
196 }
197 }
198
199 fn update_with_point(&mut self, point: &ArrayView1<f64>) {
200 for (i, &coord) in point.iter().enumerate().take(self.dimensions) {
201 self.min_coords[i] = self.min_coords[i].min(coord);
202 self.max_coords[i] = self.max_coords[i].max(coord);
203 }
204 }
205
206 #[allow(dead_code)]
207 fn contains_point(&self, point: &ArrayView1<f64>) -> bool {
208 for i in 0..self.dimensions {
209 if point[i] < self.min_coords[i] || point[i] > self.max_coords[i] {
210 return false;
211 }
212 }
213 true
214 }
215
216 fn distance_to_point(&self, point: &ArrayView1<f64>) -> f64 {
217 let mut distance_sq = 0.0;
218 for i in 0..self.dimensions {
219 let coord = point[i];
220 if coord < self.min_coords[i] {
221 let diff = self.min_coords[i] - coord;
222 distance_sq += diff * diff;
223 } else if coord > self.max_coords[i] {
224 let diff = coord - self.max_coords[i];
225 distance_sq += diff * diff;
226 }
227 }
228 distance_sq.sqrt()
229 }
230}
231
232#[derive(Debug, Clone, Default)]
234pub struct TreeStatistics {
235 pub node_count: usize,
237 pub depth: usize,
239 pub construction_time_ms: f64,
241 pub memory_usage_bytes: usize,
243 pub estimated_cache_misses: usize,
245 pub simd_operations: usize,
247}
248
249impl AdvancedKDTree {
250 pub fn new(points: &ArrayView2<'_, f64>, config: KDTreeConfig) -> SpatialResult<Self> {
252 let start_time = std::time::Instant::now();
253
254 if points.is_empty() {
255 return Ok(Self {
256 nodes: Vec::new(),
257 points: Array2::zeros((0, 0)),
258 config,
259 root_index: None,
260 stats: TreeStatistics::default(),
261 memory_pool: Arc::new(DistancePool::new(1000)),
262 });
263 }
264
265 let n_points = points.nrows();
267 let n_dims = points.ncols();
268
269 if n_points > 10_000_000 {
270 return Err(SpatialError::ValueError(format!(
271 "Dataset too large: {n_points} points. Advanced KD-Tree supports up to 10M points"
272 )));
273 }
274
275 if n_dims > 50 {
276 return Err(SpatialError::ValueError(format!(
277 "Dimension too high: {n_dims}. Advanced KD-Tree is efficient up to 50 dimensions"
278 )));
279 }
280
281 for (i, row) in points.outer_iter().enumerate() {
283 for (j, &coord) in row.iter().enumerate() {
284 if !coord.is_finite() {
285 return Err(SpatialError::ValueError(format!(
286 "Point {i} has invalid coordinate {coord} at dimension {j}"
287 )));
288 }
289 }
290 }
291
292 let points_copy = points.to_owned();
294
295 let memory_pool = if config.use_memory_pools {
297 Arc::new(DistancePool::new(1000)) } else {
300 Arc::new(DistancePool::new(1000))
301 };
302
303 let estimated_nodes = n_points.next_power_of_two();
305 let mut nodes = Vec::with_capacity(estimated_nodes);
306
307 let mut indices: Vec<usize> = (0..n_points).collect();
309 let root_index = if config.parallel_construction && n_points >= config.parallel_threshold {
310 Self::build_tree_parallel(&points_copy, &mut indices, &mut nodes, 0, &config)?
311 } else {
312 Self::build_tree_sequential(&points_copy, &mut indices, &mut nodes, 0, &config)?
313 };
314
315 let construction_time = start_time.elapsed().as_secs_f64() * 1000.0;
316
317 let stats = TreeStatistics {
319 node_count: nodes.len(),
320 depth: Self::calculate_depth(&nodes, root_index),
321 construction_time_ms: construction_time,
322 memory_usage_bytes: Self::calculate_memory_usage(&nodes, &points_copy),
323 estimated_cache_misses: Self::estimate_cache_misses(&nodes, &config),
324 simd_operations: 0,
325 };
326
327 Ok(Self {
328 nodes,
329 points: points_copy,
330 config,
331 root_index,
332 stats,
333 memory_pool,
334 })
335 }
336
337 fn build_tree_sequential(
339 points: &Array2<f64>,
340 indices: &mut [usize],
341 nodes: &mut Vec<AdvancedKDNode>,
342 depth: usize,
343 config: &KDTreeConfig,
344 ) -> SpatialResult<Option<usize>> {
345 if indices.is_empty() {
346 return Ok(None);
347 }
348
349 let n_dims = points.ncols();
350 let splitting_dimension = depth % n_dims;
351
352 let bounding_box = if config.cache_aware_layout {
354 let mut bbox = BoundingBox::new(n_dims.min(8));
355 for &idx in indices.iter() {
356 bbox.update_with_point(&points.row(idx));
357 }
358 Some(bbox)
359 } else {
360 None
361 };
362
363 if indices.len() <= config.leaf_size {
365 let node_index = nodes.len();
366 nodes.push(AdvancedKDNode {
367 point_index: indices[0] as u32,
368 splitting_dimension: splitting_dimension as u8,
369 node_info: NodeInfo {
370 left_child: u32::MAX,
371 right_child: u32::MAX,
372 is_leaf: true,
373 subtree_size: indices.len() as u32,
374 },
375 bounding_box,
376 });
377 return Ok(Some(node_index));
378 }
379
380 let median_idx = Self::find_median_optimized(points, indices, splitting_dimension);
382
383 let (left_indices, right_indices) = indices.split_at_mut(median_idx);
385 let right_indices = &mut right_indices[1..]; let left_child =
389 Self::build_tree_sequential(points, left_indices, nodes, depth + 1, config)?;
390 let right_child =
391 Self::build_tree_sequential(points, right_indices, nodes, depth + 1, config)?;
392
393 let node_index = nodes.len();
395 nodes.push(AdvancedKDNode {
396 point_index: indices[median_idx] as u32,
397 splitting_dimension: splitting_dimension as u8,
398 node_info: NodeInfo {
399 left_child: left_child.map(|v| v as u32).unwrap_or(u32::MAX),
400 right_child: right_child.map(|v| v as u32).unwrap_or(u32::MAX),
401 is_leaf: false,
402 subtree_size: indices.len() as u32,
403 },
404 bounding_box,
405 });
406
407 Ok(Some(node_index))
408 }
409
410 fn build_tree_parallel(
412 points: &Array2<f64>,
413 indices: &mut [usize],
414 nodes: &mut Vec<AdvancedKDNode>,
415 depth: usize,
416 config: &KDTreeConfig,
417 ) -> SpatialResult<Option<usize>> {
418 Self::build_tree_sequential(points, indices, nodes, depth, config)
421 }
422
423 fn find_median_optimized(
425 points: &Array2<f64>,
426 indices: &mut [usize],
427 dimension: usize,
428 ) -> usize {
429 indices.sort_unstable_by(|&a, &b| {
431 let coord_a = points[[a, dimension]];
432 let coord_b = points[[b, dimension]];
433 coord_a.partial_cmp(&coord_b).unwrap_or(Ordering::Equal)
434 });
435
436 indices.len() / 2
437 }
438
439 pub fn knn_search_advanced(
441 &self,
442 query: &ArrayView1<f64>,
443 k: usize,
444 ) -> SpatialResult<(Vec<usize>, Vec<f64>)> {
445 if k == 0 {
446 return Ok((Vec::new(), Vec::new()));
447 }
448
449 if query.len() != self.points.ncols() {
450 return Err(SpatialError::ValueError(format!(
451 "Query dimension ({}) must match tree dimension ({})",
452 query.len(),
453 self.points.ncols()
454 )));
455 }
456
457 if k > self.points.nrows() {
458 return Err(SpatialError::ValueError(format!(
459 "k ({k}) cannot be larger than number of points ({})",
460 self.points.nrows()
461 )));
462 }
463
464 if self.root_index.is_none() {
465 return Ok((Vec::new(), Vec::new()));
466 }
467
468 let mut heap = BinaryHeap::with_capacity(k + 1);
470
471 self.search_knn_advanced(
473 self.root_index.expect("Operation failed"),
474 query,
475 k,
476 &mut heap,
477 );
478
479 let mut results: Vec<(usize, f64)> = heap
481 .into_sorted_vec()
482 .into_iter()
483 .map(|item| (item.index, item.distance))
484 .collect();
485
486 results.truncate(k);
487
488 let indices: Vec<usize> = results.iter().map(|(idx, _)| *idx).collect();
489 let distances: Vec<f64> = results.iter().map(|(_, dist)| *dist).collect();
490
491 Ok((indices, distances))
492 }
493
494 fn search_knn_advanced(
496 &self,
497 node_index: usize,
498 query: &ArrayView1<f64>,
499 k: usize,
500 heap: &mut BinaryHeap<KNNItem>,
501 ) {
502 let node = &self.nodes[node_index];
503
504 let point = self.points.row(node.point_index as usize);
506 let distance = if self.config.vectorized_search {
507 self.distance_simd(query, &point)
508 } else {
509 self.distance_scalar(query, &point)
510 };
511
512 if heap.len() < k {
514 heap.push(KNNItem {
515 distance,
516 index: node.point_index as usize,
517 });
518 } else if let Some(top) = heap.peek() {
519 if distance < top.distance {
520 heap.pop();
521 heap.push(KNNItem {
522 distance,
523 index: node.point_index as usize,
524 });
525 }
526 }
527
528 if let Some(ref bbox) = node.bounding_box {
530 if heap.len() == k {
531 if let Some(top) = heap.peek() {
532 if bbox.distance_to_point(query) > top.distance {
533 return; }
535 }
536 }
537 }
538
539 if !node.node_info.is_leaf {
541 let query_coord = query[node.splitting_dimension as usize];
542 let split_coord = point[node.splitting_dimension as usize];
543
544 let (first_child, second_child) = if query_coord < split_coord {
545 (node.node_info.left_child, node.node_info.right_child)
546 } else {
547 (node.node_info.right_child, node.node_info.left_child)
548 };
549
550 if first_child != u32::MAX {
552 self.search_knn_advanced(first_child as usize, query, k, heap);
553 }
554
555 let dimension_distance = (query_coord - split_coord).abs();
557 let should_search_other = heap.len() < k
558 || heap
559 .peek()
560 .is_none_or(|top| dimension_distance < top.distance);
561
562 if should_search_other && second_child != u32::MAX {
563 self.search_knn_advanced(second_child as usize, query, k, heap);
564 }
565 }
566 }
567
568 fn distance_simd(&self, a: &ArrayView1<f64>, b: &ArrayView1<f64>) -> f64 {
570 if PlatformCapabilities::detect().simd_available {
571 let diff = f64::simd_sub(a, b);
573 let squared = f64::simd_mul(&diff.view(), &diff.view());
574 f64::simd_sum(&squared.view()).sqrt()
575 } else {
576 self.distance_scalar(a, b)
577 }
578 }
579
580 fn distance_scalar(&self, a: &ArrayView1<f64>, b: &ArrayView1<f64>) -> f64 {
582 a.iter()
583 .zip(b.iter())
584 .map(|(x, y)| (x - y).powi(2))
585 .sum::<f64>()
586 .sqrt()
587 }
588
589 pub fn batch_knn_search(
591 &self,
592 queries: &ArrayView2<'_, f64>,
593 k: usize,
594 ) -> SpatialResult<(Array2<usize>, Array2<f64>)> {
595 let n_queries = queries.nrows();
596 let mut indices = Array2::zeros((n_queries, k));
597 let mut distances = Array2::zeros((n_queries, k));
598
599 if self.config.parallel_construction && n_queries >= 100 {
601 indices
602 .outer_iter_mut()
603 .zip(distances.outer_iter_mut())
604 .zip(queries.outer_iter())
605 .enumerate()
606 .par_bridge()
607 .try_for_each(
608 |(_i, ((mut idx_row, mut dist_row), query))| -> SpatialResult<()> {
609 let (query_indices, query_distances) =
610 self.knn_search_advanced(&query, k)?;
611
612 for (j, &idx) in query_indices.iter().enumerate().take(k) {
613 idx_row[j] = idx;
614 }
615 for (j, &dist) in query_distances.iter().enumerate().take(k) {
616 dist_row[j] = dist;
617 }
618 Ok(())
619 },
620 )?;
621 } else {
622 for (i, query) in queries.outer_iter().enumerate() {
624 let (query_indices, query_distances) = self.knn_search_advanced(&query, k)?;
625
626 for (j, &idx) in query_indices.iter().enumerate().take(k) {
627 indices[[i, j]] = idx;
628 }
629 for (j, &dist) in query_distances.iter().enumerate().take(k) {
630 distances[[i, j]] = dist;
631 }
632 }
633 }
634
635 Ok((indices, distances))
636 }
637
638 pub fn range_search(
640 &self,
641 query: &ArrayView1<f64>,
642 radius: f64,
643 ) -> SpatialResult<Vec<(usize, f64)>> {
644 if query.len() != self.points.ncols() {
645 return Err(SpatialError::ValueError(
646 "Query dimension must match tree dimension".to_string(),
647 ));
648 }
649
650 if self.root_index.is_none() {
651 return Ok(Vec::new());
652 }
653
654 let mut result = Vec::new();
655 self.search_range_advanced(
656 self.root_index.expect("Operation failed"),
657 query,
658 radius,
659 &mut result,
660 );
661 Ok(result)
662 }
663
664 fn search_range_advanced(
666 &self,
667 node_index: usize,
668 query: &ArrayView1<f64>,
669 radius: f64,
670 result: &mut Vec<(usize, f64)>,
671 ) {
672 let node = &self.nodes[node_index];
673 let point = self.points.row(node.point_index as usize);
674
675 let distance = if self.config.vectorized_search {
677 self.distance_simd(query, &point)
678 } else {
679 self.distance_scalar(query, &point)
680 };
681
682 if distance <= radius {
683 result.push((node.point_index as usize, distance));
684 }
685
686 if let Some(ref bbox) = node.bounding_box {
688 if bbox.distance_to_point(query) > radius {
689 return; }
691 }
692
693 if !node.node_info.is_leaf {
695 let query_coord = query[node.splitting_dimension as usize];
696 let split_coord = point[node.splitting_dimension as usize];
697
698 if node.node_info.left_child != u32::MAX && query_coord - radius <= split_coord {
700 self.search_range_advanced(
701 node.node_info.left_child as usize,
702 query,
703 radius,
704 result,
705 );
706 }
707
708 if node.node_info.right_child != u32::MAX && query_coord + radius >= split_coord {
710 self.search_range_advanced(
711 node.node_info.right_child as usize,
712 query,
713 radius,
714 result,
715 );
716 }
717 }
718 }
719
720 pub fn statistics(&self) -> &TreeStatistics {
722 &self.stats
723 }
724
725 pub fn config(&self) -> &KDTreeConfig {
727 &self.config
728 }
729
730 fn calculate_depth(_nodes: &[AdvancedKDNode], rootindex: Option<usize>) -> usize {
732 if let Some(root) = rootindex {
733 Self::calculate_depth_recursive(_nodes, root, 0)
734 } else {
735 0
736 }
737 }
738
739 fn calculate_depth_recursive(
740 nodes: &[AdvancedKDNode],
741 node_index: usize,
742 current_depth: usize,
743 ) -> usize {
744 let node = &nodes[node_index];
745 if node.node_info.is_leaf {
746 current_depth + 1
747 } else {
748 let left_depth = if node.node_info.left_child != u32::MAX {
749 Self::calculate_depth_recursive(
750 nodes,
751 node.node_info.left_child as usize,
752 current_depth + 1,
753 )
754 } else {
755 current_depth
756 };
757 let right_depth = if node.node_info.right_child != u32::MAX {
758 Self::calculate_depth_recursive(
759 nodes,
760 node.node_info.right_child as usize,
761 current_depth + 1,
762 )
763 } else {
764 current_depth
765 };
766 left_depth.max(right_depth)
767 }
768 }
769
770 fn calculate_memory_usage(nodes: &[AdvancedKDNode], points: &Array2<f64>) -> usize {
771 let _node_size = std::mem::size_of::<AdvancedKDNode>();
772 let point_size = points.len() * std::mem::size_of::<f64>();
773 std::mem::size_of_val(nodes) + point_size
774 }
775
776 fn estimate_cache_misses(nodes: &[AdvancedKDNode], config: &KDTreeConfig) -> usize {
777 let cache_lines_per_level = nodes.len() / config.cache_line_size.max(1);
779 cache_lines_per_level * 2 }
781}
782
783#[derive(Debug, Clone)]
785struct KNNItem {
786 distance: f64,
787 index: usize,
788}
789
790impl PartialEq for KNNItem {
791 fn eq(&self, other: &Self) -> bool {
792 self.distance == other.distance
793 }
794}
795
796impl Eq for KNNItem {}
797
798impl PartialOrd for KNNItem {
799 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
800 Some(self.cmp(other))
801 }
802}
803
804impl Ord for KNNItem {
805 fn cmp(&self, other: &Self) -> Ordering {
806 self.distance
808 .partial_cmp(&other.distance)
809 .unwrap_or(Ordering::Equal)
810 }
811}
812
813#[cfg(test)]
814mod tests {
815 use super::{AdvancedKDTree, BoundingBox, KDTreeConfig};
816 #[allow(unused_imports)]
817 use approx::assert_relative_eq;
818 use scirs2_core::ndarray::array;
819
820 #[test]
821 fn test_advanced_kdtree_creation() {
822 let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
823 let config = KDTreeConfig::new();
824
825 let kdtree = AdvancedKDTree::new(&points.view(), config);
826 assert!(kdtree.is_ok());
827
828 let kdtree = kdtree.expect("Operation failed");
829 assert_eq!(kdtree.points.nrows(), 4);
830 assert_eq!(kdtree.points.ncols(), 2);
831 }
832
833 #[test]
834 fn test_advanced_knn_search() {
835 let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0], [0.5, 0.5]];
836 let config = KDTreeConfig::new()
837 .with_vectorized_search(true)
838 .with_cache_aware_layout(true)
839 .with_leaf_size(1); let kdtree = AdvancedKDTree::new(&points.view(), config).expect("Operation failed");
842 let query = array![0.6, 0.6];
843
844 let (indices, distances) = kdtree
845 .knn_search_advanced(&query.view(), 2)
846 .expect("Operation failed");
847
848 assert_eq!(indices.len(), 2);
849 assert_eq!(distances.len(), 2);
850
851 assert_eq!(indices[0], 4);
853 assert!(distances[0] < distances[1]);
854 }
855
856 #[test]
857 fn test_advanced_range_search() {
858 let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0], [0.5, 0.5]];
859 let config = KDTreeConfig::new();
860
861 let kdtree = AdvancedKDTree::new(&points.view(), config).expect("Operation failed");
862 let query = array![0.5, 0.5];
863
864 let results = kdtree
865 .range_search(&query.view(), 0.8)
866 .expect("Operation failed");
867
868 assert!(!results.is_empty());
870
871 for (_, distance) in results {
873 assert!(distance <= 0.8);
874 }
875 }
876
877 #[test]
878 fn test_batch_knn_search() {
879 let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
880 let queries = array![[0.1, 0.1], [0.9, 0.9]];
881 let config = KDTreeConfig::new().with_leaf_size(1);
883
884 let kdtree = AdvancedKDTree::new(&points.view(), config).expect("Operation failed");
885 let (indices, distances) = kdtree
886 .batch_knn_search(&queries.view(), 2)
887 .expect("Operation failed");
888
889 assert_eq!(indices.dim(), (2, 2));
890 assert_eq!(distances.dim(), (2, 2));
891
892 assert_eq!(indices[[0, 0]], 0);
894 assert_eq!(indices[[1, 0]], 3);
896 }
897
898 #[test]
899 fn test_bounding_box() {
900 let mut bbox = BoundingBox::new(2);
901 let point1 = array![1.0, 2.0];
902 let point2 = array![3.0, 4.0];
903
904 bbox.update_with_point(&point1.view());
905 bbox.update_with_point(&point2.view());
906
907 assert_eq!(bbox.min_coords[0], 1.0);
908 assert_eq!(bbox.max_coords[0], 3.0);
909 assert_eq!(bbox.min_coords[1], 2.0);
910 assert_eq!(bbox.max_coords[1], 4.0);
911
912 let inside_point = array![2.0, 3.0];
914 assert!(bbox.contains_point(&inside_point.view()));
915
916 let outside_point = array![5.0, 6.0];
917 assert!(!bbox.contains_point(&outside_point.view()));
918 }
919
920 #[test]
921 fn test_tree_statistics() {
922 let points = array![
923 [0.0, 0.0],
924 [1.0, 0.0],
925 [0.0, 1.0],
926 [1.0, 1.0],
927 [2.0, 2.0],
928 [3.0, 3.0],
929 [4.0, 4.0],
930 [5.0, 5.0]
931 ];
932 let config = KDTreeConfig::new();
933
934 let kdtree = AdvancedKDTree::new(&points.view(), config).expect("Operation failed");
935 let stats = kdtree.statistics();
936
937 assert!(stats.node_count > 0);
938 assert!(stats.depth > 0);
939 assert!(stats.construction_time_ms >= 0.0);
940 assert!(stats.memory_usage_bytes > 0);
941 }
942}