scirs2_spatial/
ml_optimization.rs

1//! Machine learning-based spatial optimization
2//!
3//! This module implements advanced machine learning techniques to automatically optimize
4//! spatial algorithms, including neural network-based parameter tuning, reinforcement
5//! learning for algorithm selection, and adaptive optimization strategies that learn
6//! from data patterns and computational environments.
7//!
8//! # Features
9//!
10//! - **Neural Algorithm Optimization**: Deep learning models that optimize algorithm parameters
11//! - **Reinforcement Learning**: RL agents that learn optimal algorithm selection strategies
12//! - **Meta-learning**: Learn to learn new spatial patterns quickly
13//! - **AutoML for Spatial Computing**: Automated machine learning for spatial problems
14//! - **Bayesian Optimization**: Gaussian process-based hyperparameter optimization
15//! - **Ensemble Methods**: Combine multiple algorithms intelligently
16//! - **Online Learning**: Adapt to changing data distributions in real-time
17//! - **Transfer Learning**: Apply knowledge from related spatial domains
18//!
19//! # Applications
20//!
21//! - **Automatic hyperparameter tuning** for clustering algorithms
22//! - **Dynamic algorithm selection** based on data characteristics
23//! - **Learned distance metrics** optimized for specific tasks
24//! - **Adaptive spatial data structures** that restructure based on access patterns
25//! - **Predictive preprocessing** that optimizes data layout for better performance
26//!
27//! # Examples
28//!
29//! ```ignore
30//! use scirs2_spatial::ml_optimization::{NeuralSpatialOptimizer, ReinforcementLearningSelector};
31//! use scirs2_core::ndarray::array;
32//!
33//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
34//! // Neural network-based spatial optimizer
35//! let optimizer_builder = NeuralSpatialOptimizer::new()
36//!     .with_network_architecture([64, 128, 64, 32])
37//!     .with_learning_rate(0.001)
38//!     .with_adaptive_learning(true);
39//! let mut optimizer = optimizer_builder;
40//!
41//! let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
42//! let optimized_params = optimizer.optimize_clustering_parameters(&points.view())?;
43//! println!("Optimized k-means parameters: {:?}", optimized_params);
44//!
45//! // Reinforcement learning algorithm selector
46//! let rl_builder = ReinforcementLearningSelector::new()
47//!     .with_epsilon_greedy(0.1)
48//!     .with_experience_replay(true)
49//!     .with_target_network(true);
50//! let mut rl_selector = rl_builder;
51//!
52//! let selected_algorithm = rl_selector.select_best_algorithm(&points.view())?;
53//! println!("RL selected algorithm: {:?}", selected_algorithm);
54//! # Ok(())
55//! # }
56//! ```
57
58use crate::error::{SpatialError, SpatialResult};
59use scirs2_core::ndarray::{Array1, Array2, ArrayView2, Axis};
60use scirs2_core::random::Rng;
61use statrs::statistics::Statistics;
62use std::collections::{HashMap, VecDeque};
63use std::f64::consts::PI;
64
65/// Neural network layer for spatial optimization
66#[derive(Debug, Clone)]
67pub struct NeuralLayer {
68    /// Layer weights
69    pub weights: Array2<f64>,
70    /// Layer biases
71    pub biases: Array1<f64>,
72    /// Activation function
73    pub activation: ActivationFunction,
74}
75
76/// Activation functions for neural networks
77#[derive(Debug, Clone, Copy, PartialEq)]
78pub enum ActivationFunction {
79    /// Rectified Linear Unit
80    ReLU,
81    /// Leaky ReLU
82    LeakyReLU(f64),
83    /// Sigmoid activation
84    Sigmoid,
85    /// Hyperbolic tangent
86    Tanh,
87    /// Swish activation (x * sigmoid(x))
88    Swish,
89    /// GELU activation
90    GELU,
91}
92
93impl ActivationFunction {
94    /// Apply activation function to input
95    pub fn apply(&self, x: f64) -> f64 {
96        match self {
97            ActivationFunction::ReLU => x.max(0.0),
98            ActivationFunction::LeakyReLU(alpha) => {
99                if x > 0.0 {
100                    x
101                } else {
102                    alpha * x
103                }
104            }
105            ActivationFunction::Sigmoid => 1.0 / (1.0 + (-x).exp()),
106            ActivationFunction::Tanh => x.tanh(),
107            ActivationFunction::Swish => x * (1.0 / (1.0 + (-x).exp())),
108            ActivationFunction::GELU => {
109                0.5 * x * (1.0 + ((2.0_f64 / PI).sqrt() * (x + 0.044715 * x.powi(3))).tanh())
110            }
111        }
112    }
113
114    /// Compute derivative of activation function
115    pub fn derivative(&self, x: f64) -> f64 {
116        match self {
117            ActivationFunction::ReLU => {
118                if x > 0.0 {
119                    1.0
120                } else {
121                    0.0
122                }
123            }
124            ActivationFunction::LeakyReLU(alpha) => {
125                if x > 0.0 {
126                    1.0
127                } else {
128                    *alpha
129                }
130            }
131            ActivationFunction::Sigmoid => {
132                let sigmoid_x = self.apply(x);
133                sigmoid_x * (1.0 - sigmoid_x)
134            }
135            ActivationFunction::Tanh => 1.0 - x.tanh().powi(2),
136            ActivationFunction::Swish => {
137                let sigmoid_x = 1.0 / (1.0 + (-x).exp());
138                sigmoid_x + x * sigmoid_x * (1.0 - sigmoid_x)
139            }
140            ActivationFunction::GELU => {
141                let sqrt_2_pi = (2.0_f64 / PI).sqrt();
142                let tanh_input = sqrt_2_pi * (x + 0.044715 * x.powi(3));
143                let tanh_val = tanh_input.tanh();
144                0.5 * (1.0 + tanh_val)
145                    + 0.5
146                        * x
147                        * (1.0 - tanh_val.powi(2))
148                        * sqrt_2_pi
149                        * (1.0 + 3.0 * 0.044715 * x.powi(2))
150            }
151        }
152    }
153}
154
155/// Neural spatial optimizer
156#[derive(Debug, Clone)]
157pub struct NeuralSpatialOptimizer {
158    /// Neural network layers
159    layers: Vec<NeuralLayer>,
160    /// Learning rate
161    learning_rate: f64,
162    /// Adaptive learning rate
163    adaptive_learning: bool,
164    /// Experience buffer for training
165    experience_buffer: VecDeque<(Array1<f64>, Array1<f64>)>,
166    /// Training iterations
167    training_iterations: usize,
168    /// Loss history
169    loss_history: Vec<f64>,
170}
171
172impl Default for NeuralSpatialOptimizer {
173    fn default() -> Self {
174        Self::new()
175    }
176}
177
178impl NeuralSpatialOptimizer {
179    /// Create new neural spatial optimizer
180    pub fn new() -> Self {
181        Self {
182            layers: Vec::new(),
183            learning_rate: 0.001,
184            adaptive_learning: false,
185            experience_buffer: VecDeque::new(),
186            training_iterations: 0,
187            loss_history: Vec::new(),
188        }
189    }
190
191    /// Configure network architecture
192    pub fn with_network_architecture(mut self, layersizes: impl AsRef<[usize]>) -> Self {
193        let sizes = layersizes.as_ref();
194        self.layers.clear();
195
196        for i in 0..sizes.len() - 1 {
197            let input_size = sizes[i];
198            let output_size = sizes[i + 1];
199
200            // Xavier/Glorot initialization
201            let scale = (2.0_f64 / (input_size + output_size) as f64).sqrt();
202            let weights = Array2::from_shape_fn((output_size, input_size), |_| {
203                (scirs2_core::random::random::<f64>() - 0.5) * 2.0 * scale
204            });
205            let biases = Array1::zeros(output_size);
206
207            let activation = if i == sizes.len() - 2 {
208                ActivationFunction::Sigmoid // Output layer
209            } else {
210                ActivationFunction::ReLU // Hidden layers
211            };
212
213            self.layers.push(NeuralLayer {
214                weights,
215                biases,
216                activation,
217            });
218        }
219
220        self
221    }
222
223    /// Set network architecture in place (for use when already borrowed mutably)
224    pub fn set_network_architecture(&mut self, layersizes: impl AsRef<[usize]>) {
225        let sizes = layersizes.as_ref();
226        self.layers.clear();
227
228        for i in 0..sizes.len() - 1 {
229            let input_size = sizes[i];
230            let output_size = sizes[i + 1];
231
232            // Xavier/Glorot initialization
233            let scale = (2.0_f64 / (input_size + output_size) as f64).sqrt();
234            let weights = Array2::from_shape_fn((output_size, input_size), |_| {
235                (scirs2_core::random::random::<f64>() - 0.5) * 2.0 * scale
236            });
237            let biases = Array1::zeros(output_size);
238
239            let activation = if i == sizes.len() - 2 {
240                ActivationFunction::Sigmoid // Output layer
241            } else {
242                ActivationFunction::ReLU // Hidden layers
243            };
244
245            self.layers.push(NeuralLayer {
246                weights,
247                biases,
248                activation,
249            });
250        }
251    }
252
253    /// Configure learning rate
254    pub fn with_learning_rate(&mut self, lr: f64) -> &mut Self {
255        self.learning_rate = lr;
256        self
257    }
258
259    /// Enable adaptive learning
260    pub fn with_adaptive_learning(&mut self, enabled: bool) -> &mut Self {
261        self.adaptive_learning = enabled;
262        self
263    }
264
265    /// Optimize clustering parameters using neural network
266    pub fn optimize_clustering_parameters(
267        &mut self,
268        points: &ArrayView2<'_, f64>,
269    ) -> SpatialResult<ClusteringParameters> {
270        // Extract features from spatial data
271        let features = self.extract_spatial_features(points)?;
272
273        // If network is empty, initialize with default architecture
274        if self.layers.is_empty() {
275            let feature_size = features.len();
276            self.set_network_architecture([feature_size, 64, 32, 16, 8]);
277        }
278
279        // Forward pass to get optimal parameters
280        let output = self.forward_pass(&features)?;
281
282        // Convert network output to clustering parameters
283        let params = self.decode_clustering_parameters(&output)?;
284
285        // Evaluate parameters and update network if we have ground truth
286        if let Some(qualityscore) = self.evaluate_clustering_quality(points, &params)? {
287            self.update_network(&features, qualityscore)?;
288        }
289
290        Ok(params)
291    }
292
293    /// Extract spatial features from data
294    fn extract_spatial_features(
295        &self,
296        n_points: &ArrayView2<'_, f64>,
297    ) -> SpatialResult<Array1<f64>> {
298        let (num_points, n_dims) = n_points.dim();
299        let mut features = Vec::new();
300
301        // Basic statistics
302        features.push(num_points as f64);
303        features.push(n_dims as f64);
304
305        // Data distribution features
306        for dim in 0..n_dims {
307            let column = n_points.column(dim);
308            let mean = column.mean();
309            let std = (column.mapv(|x| (x - mean).powi(2)).mean()).sqrt();
310            let min_val = column.fold(f64::INFINITY, |a, &b| a.min(b));
311            let max_val = column.fold(f64::NEG_INFINITY, |a, &b| a.max(b));
312
313            features.push(mean);
314            features.push(std);
315            features.push(max_val - min_val); // Range
316            features.push(if std > 0.0 {
317                (max_val - min_val) / std
318            } else {
319                0.0
320            }); // Coefficient of variation
321        }
322
323        // Pairwise distance statistics
324        let mut distances = Vec::new();
325        for i in 0..num_points.min(100) {
326            // Sample for efficiency
327            for j in (i + 1)..num_points.min(100) {
328                let dist: f64 = n_points
329                    .row(i)
330                    .iter()
331                    .zip(n_points.row(j).iter())
332                    .map(|(&a, &b)| (a - b).powi(2))
333                    .sum::<f64>()
334                    .sqrt();
335                distances.push(dist);
336            }
337        }
338
339        if !distances.is_empty() {
340            let mean_dist = distances.iter().sum::<f64>() / distances.len() as f64;
341            let dist_std = {
342                let variance = distances
343                    .iter()
344                    .map(|&d| (d - mean_dist).powi(2))
345                    .sum::<f64>()
346                    / distances.len() as f64;
347                variance.sqrt()
348            };
349            features.push(mean_dist);
350            features.push(dist_std);
351            features.push(distances.iter().fold(f64::INFINITY, |a, &b| a.min(b))); // Min distance
352            features.push(distances.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b)));
353        // Max distance
354        } else {
355            features.extend_from_slice(&[0.0, 0.0, 0.0, 0.0]);
356        }
357
358        // Density estimation
359        let density = NeuralSpatialOptimizer::estimate_local_density(n_points)?;
360        features.push(density);
361
362        // Clustering tendency (Hopkins statistic approximation)
363        let hopkins = self.estimate_clustering_tendency(n_points)?;
364        features.push(hopkins);
365
366        Ok(Array1::from(features))
367    }
368
369    /// Estimate local density of the data
370    fn estimate_local_density(points: &ArrayView2<'_, f64>) -> SpatialResult<f64> {
371        let (n_points, n_dims) = points.dim();
372
373        if n_points < 2 {
374            return Ok(0.0);
375        }
376
377        // Sample n_points for efficiency
378        let sample_size = n_points.min(50);
379        let mut total_inverse_distance = 0.0;
380        let mut count = 0;
381
382        for i in 0..sample_size {
383            let mut nearest_distance = f64::INFINITY;
384
385            for j in 0..n_points {
386                if i != j {
387                    let dist: f64 = points
388                        .row(i)
389                        .iter()
390                        .zip(points.row(j).iter())
391                        .map(|(&a, &b)| (a - b).powi(2))
392                        .sum::<f64>()
393                        .sqrt();
394
395                    if dist < nearest_distance {
396                        nearest_distance = dist;
397                    }
398                }
399            }
400
401            if nearest_distance > 0.0 && nearest_distance < f64::INFINITY {
402                total_inverse_distance += 1.0 / nearest_distance;
403                count += 1;
404            }
405        }
406
407        Ok(if count > 0 {
408            total_inverse_distance / count as f64
409        } else {
410            0.0
411        })
412    }
413
414    /// Estimate clustering tendency (Hopkins-like statistic)
415    fn estimate_clustering_tendency(&self, points: &ArrayView2<'_, f64>) -> SpatialResult<f64> {
416        let (n_points, n_dims) = points.dim();
417
418        if n_points < 10 {
419            return Ok(0.5); // Neutral value
420        }
421
422        // Sample a subset of n_points
423        let sample_size = n_points.min(20);
424        let mut real_distances = Vec::new();
425        let mut random_distances = Vec::new();
426
427        // Calculate distances to nearest neighbors for real n_points
428        for i in 0..sample_size {
429            let mut min_dist = f64::INFINITY;
430            for j in 0..n_points {
431                if i != j {
432                    let dist: f64 = points
433                        .row(i)
434                        .iter()
435                        .zip(points.row(j).iter())
436                        .map(|(&a, &b)| (a - b).powi(2))
437                        .sum::<f64>()
438                        .sqrt();
439                    min_dist = min_dist.min(dist);
440                }
441            }
442            real_distances.push(min_dist);
443        }
444
445        // Generate random n_points and calculate distances
446        let bounds = self.get_data_bounds(points);
447        for _ in 0..sample_size {
448            let random_point: Array1<f64> = Array1::from_shape_fn(n_dims, |i| {
449                scirs2_core::random::random::<f64>() * (bounds[i].1 - bounds[i].0) + bounds[i].0
450            });
451
452            let mut min_dist = f64::INFINITY;
453            for j in 0..n_points {
454                let dist: f64 = random_point
455                    .iter()
456                    .zip(points.row(j).iter())
457                    .map(|(&a, &b)| (a - b).powi(2))
458                    .sum::<f64>()
459                    .sqrt();
460                min_dist = min_dist.min(dist);
461            }
462            random_distances.push(min_dist);
463        }
464
465        // Calculate Hopkins-like statistic
466        let sum_random: f64 = random_distances.iter().sum();
467        let sum_real: f64 = real_distances.iter().sum();
468        let hopkins = sum_random / (sum_random + sum_real);
469
470        Ok(hopkins)
471    }
472
473    /// Get data bounds for each dimension
474    fn get_data_bounds(&self, points: &ArrayView2<'_, f64>) -> Vec<(f64, f64)> {
475        let (_, n_dims) = points.dim();
476        let mut bounds = Vec::new();
477
478        for dim in 0..n_dims {
479            let column = points.column(dim);
480            let min_val = column.fold(f64::INFINITY, |a, &b| a.min(b));
481            let max_val = column.fold(f64::NEG_INFINITY, |a, &b| a.max(b));
482            bounds.push((min_val, max_val));
483        }
484
485        bounds
486    }
487
488    /// Forward pass through neural network
489    fn forward_pass(&self, input: &Array1<f64>) -> SpatialResult<Array1<f64>> {
490        let mut current = input.clone();
491
492        for layer in &self.layers {
493            // Linear transformation: y = Wx + b
494            let linear_output = layer.weights.dot(&current) + &layer.biases;
495
496            // Apply activation function
497            current = linear_output.mapv(|x| layer.activation.apply(x));
498        }
499
500        Ok(current)
501    }
502
503    /// Decode neural network output to clustering parameters
504    fn decode_clustering_parameters(
505        &self,
506        output: &Array1<f64>,
507    ) -> SpatialResult<ClusteringParameters> {
508        if output.len() < 8 {
509            return Err(SpatialError::InvalidInput(
510                "Insufficient network output for parameter decoding".to_string(),
511            ));
512        }
513
514        Ok(ClusteringParameters {
515            num_clusters: ((output[0] * 20.0) as usize).clamp(1, 20), // 1-20 clusters
516            max_iterations: ((output[1] * 500.0) as usize).clamp(10, 500), // 10-500 iterations
517            tolerance: output[2] * 1e-3,                              // 0 to 1e-3 tolerance
518            init_method: if output[3] > 0.5 {
519                InitMethod::KMeansPlusPlus
520            } else {
521                InitMethod::Random
522            },
523            distance_metric: NeuralSpatialOptimizer::decode_distance_metric(output[4]),
524            regularization: output[5] * 0.1, // 0 to 0.1 regularization
525            early_stopping: output[6] > 0.5,
526            adaptive_parameters: output[7] > 0.5,
527        })
528    }
529
530    /// Decode distance metric from neural output
531    fn decode_distance_metric(value: f64) -> DistanceMetric {
532        match (value * 4.0) as usize {
533            0 => DistanceMetric::Euclidean,
534            1 => DistanceMetric::Manhattan,
535            2 => DistanceMetric::Cosine,
536            3 => DistanceMetric::Minkowski(2.0),
537            _ => DistanceMetric::Euclidean,
538        }
539    }
540
541    /// Evaluate clustering quality
542    fn evaluate_clustering_quality(
543        &self,
544        points: &ArrayView2<'_, f64>,
545        params: &ClusteringParameters,
546    ) -> SpatialResult<Option<f64>> {
547        // Run clustering with given parameters
548        let clustering_result = self.run_clustering(points, params)?;
549
550        // Calculate quality metrics
551        let silhouette_score = self.calculate_silhouette_score(points, &clustering_result)?;
552        let inertia = NeuralSpatialOptimizer::calculate_inertia(points, &clustering_result)?;
553        let calinski_harabasz =
554            self.calculate_calinski_harabasz_score(points, &clustering_result)?;
555
556        // Combine metrics into single quality score
557        let qualityscore =
558            0.5 * silhouette_score + 0.3 * (1.0 / (1.0 + inertia)) + 0.2 * calinski_harabasz;
559
560        Ok(Some(qualityscore))
561    }
562
563    /// Run clustering with given parameters
564    fn run_clustering(
565        &self,
566        points: &ArrayView2<'_, f64>,
567        params: &ClusteringParameters,
568    ) -> SpatialResult<ClusteringResult> {
569        // Simplified k-means implementation
570        let (n_points, n_dims) = points.dim();
571        let mut centroids = Array2::zeros((params.num_clusters, n_dims));
572        let mut assignments = Array1::zeros(n_points);
573
574        // Initialize centroids
575        match params.init_method {
576            InitMethod::Random => {
577                for i in 0..params.num_clusters {
578                    for j in 0..n_dims {
579                        centroids[[i, j]] = scirs2_core::random::random::<f64>();
580                    }
581                }
582            }
583            InitMethod::KMeansPlusPlus => {
584                // Simplified k-means++ initialization
585                let mut rng = scirs2_core::random::rng();
586                let mut selected = Vec::new();
587                for _ in 0..params.num_clusters {
588                    let idx = rng.gen_range(0..n_points);
589                    selected.push(idx);
590                }
591
592                for (i, &idx) in selected.iter().enumerate() {
593                    centroids.row_mut(i).assign(&points.row(idx));
594                }
595            }
596        }
597
598        // Run k-means iterations
599        for _ in 0..params.max_iterations {
600            let mut changed = false;
601
602            // Assignment step
603            for i in 0..n_points {
604                let mut best_cluster = 0;
605                let mut best_distance = f64::INFINITY;
606
607                for j in 0..params.num_clusters {
608                    let distance = NeuralSpatialOptimizer::calculate_distance(
609                        &points.row(i).to_owned(),
610                        &centroids.row(j).to_owned(),
611                        &params.distance_metric,
612                    );
613
614                    if distance < best_distance {
615                        best_distance = distance;
616                        best_cluster = j;
617                    }
618                }
619
620                if assignments[i] != best_cluster {
621                    assignments[i] = best_cluster;
622                    changed = true;
623                }
624            }
625
626            if !changed {
627                break;
628            }
629
630            // Update centroids
631            let mut cluster_counts = vec![0; params.num_clusters];
632            let mut new_centroids = Array2::zeros((params.num_clusters, n_dims));
633
634            for i in 0..n_points {
635                let cluster = assignments[i];
636                cluster_counts[cluster] += 1;
637                for j in 0..n_dims {
638                    new_centroids[[cluster, j]] += points[[i, j]];
639                }
640            }
641
642            for i in 0..params.num_clusters {
643                if cluster_counts[i] > 0 {
644                    for j in 0..n_dims {
645                        new_centroids[[i, j]] /= cluster_counts[i] as f64;
646                    }
647                }
648            }
649
650            centroids = new_centroids;
651        }
652
653        let inertia = self.calculate_inertia_direct(points, &centroids, &assignments)?;
654
655        Ok(ClusteringResult {
656            centroids,
657            assignments,
658            inertia,
659        })
660    }
661
662    /// Calculate distance between two points
663    fn calculate_distance(a: &Array1<f64>, b: &Array1<f64>, metric: &DistanceMetric) -> f64 {
664        match metric {
665            DistanceMetric::Euclidean => a
666                .iter()
667                .zip(b.iter())
668                .map(|(&x, &y)| (x - y).powi(2))
669                .sum::<f64>()
670                .sqrt(),
671            DistanceMetric::Manhattan => a.iter().zip(b.iter()).map(|(&x, &y)| (x - y).abs()).sum(),
672            DistanceMetric::Cosine => {
673                let dot_product: f64 = a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum();
674                let norm_a = a.iter().map(|&x| x.powi(2)).sum::<f64>().sqrt();
675                let norm_b = b.iter().map(|&x| x.powi(2)).sum::<f64>().sqrt();
676                1.0 - (dot_product / (norm_a * norm_b))
677            }
678            DistanceMetric::Minkowski(p) => a
679                .iter()
680                .zip(b.iter())
681                .map(|(&x, &y)| (x - y).abs().powf(*p))
682                .sum::<f64>()
683                .powf(1.0 / p),
684        }
685    }
686
687    /// Calculate silhouette score
688    fn calculate_silhouette_score(
689        &self,
690        points: &ArrayView2<'_, f64>,
691        result: &ClusteringResult,
692    ) -> SpatialResult<f64> {
693        let (n_points, n_dims) = points.dim();
694        let mut silhouette_scores = Vec::new();
695
696        for i in 0..n_points {
697            let cluster_i = result.assignments[i];
698
699            // Calculate average distance to points in same cluster
700            let mut intra_cluster_distances = Vec::new();
701            for j in 0..n_points {
702                if i != j && result.assignments[j] == cluster_i {
703                    let dist = NeuralSpatialOptimizer::calculate_distance(
704                        &points.row(i).to_owned(),
705                        &points.row(j).to_owned(),
706                        &DistanceMetric::Euclidean,
707                    );
708                    intra_cluster_distances.push(dist);
709                }
710            }
711
712            let a = if intra_cluster_distances.is_empty() {
713                0.0
714            } else {
715                intra_cluster_distances.iter().sum::<f64>() / intra_cluster_distances.len() as f64
716            };
717
718            // Calculate minimum average distance to points in other clusters
719            let mut min_inter_cluster_distance = f64::INFINITY;
720            for cluster in 0..result.centroids.nrows() {
721                if cluster != cluster_i {
722                    let mut inter_cluster_distances = Vec::new();
723                    for j in 0..n_points {
724                        if result.assignments[j] == cluster {
725                            let dist = NeuralSpatialOptimizer::calculate_distance(
726                                &points.row(i).to_owned(),
727                                &points.row(j).to_owned(),
728                                &DistanceMetric::Euclidean,
729                            );
730                            inter_cluster_distances.push(dist);
731                        }
732                    }
733
734                    if !inter_cluster_distances.is_empty() {
735                        let avg_dist = inter_cluster_distances.iter().sum::<f64>()
736                            / inter_cluster_distances.len() as f64;
737                        min_inter_cluster_distance = min_inter_cluster_distance.min(avg_dist);
738                    }
739                }
740            }
741
742            let b = min_inter_cluster_distance;
743
744            let silhouette = if a < b {
745                1.0 - (a / b)
746            } else if a > b {
747                (b / a) - 1.0
748            } else {
749                0.0
750            };
751
752            silhouette_scores.push(silhouette);
753        }
754
755        Ok(silhouette_scores.iter().sum::<f64>() / silhouette_scores.len() as f64)
756    }
757
758    /// Calculate inertia (WCSS)
759    fn calculate_inertia(
760        self_points: &ArrayView2<'_, f64>,
761        result: &ClusteringResult,
762    ) -> SpatialResult<f64> {
763        Ok(result.inertia)
764    }
765
766    /// Calculate inertia directly
767    fn calculate_inertia_direct(
768        &self,
769        points: &ArrayView2<'_, f64>,
770        centroids: &Array2<f64>,
771        assignments: &Array1<usize>,
772    ) -> SpatialResult<f64> {
773        let (n_points, n_dims) = points.dim();
774        let mut inertia = 0.0;
775
776        for i in 0..n_points {
777            let cluster = assignments[i];
778            let distance = NeuralSpatialOptimizer::calculate_distance(
779                &points.row(i).to_owned(),
780                &centroids.row(cluster).to_owned(),
781                &DistanceMetric::Euclidean,
782            );
783            inertia += distance.powi(2);
784        }
785
786        Ok(inertia)
787    }
788
789    /// Calculate Calinski-Harabasz score
790    fn calculate_calinski_harabasz_score(
791        &self,
792        points: &ArrayView2<'_, f64>,
793        result: &ClusteringResult,
794    ) -> SpatialResult<f64> {
795        let (n_points, n_dims) = points.dim();
796        let k = result.centroids.nrows();
797
798        if k <= 1 || n_points <= k {
799            return Ok(0.0);
800        }
801
802        // Calculate overall centroid
803        let overall_centroid: Array1<f64> = points.mean_axis(Axis(0)).unwrap();
804
805        // Calculate between-cluster sum of squares
806        let mut between_ss = 0.0;
807        for i in 0..k {
808            let cluster_size = result.assignments.iter().filter(|&&x| x == i).count() as f64;
809            if cluster_size > 0.0 {
810                let distance_to_overall = NeuralSpatialOptimizer::calculate_distance(
811                    &result.centroids.row(i).to_owned(),
812                    &overall_centroid,
813                    &DistanceMetric::Euclidean,
814                );
815                between_ss += cluster_size * distance_to_overall.powi(2);
816            }
817        }
818
819        // Calculate within-cluster sum of squares
820        let within_ss = result.inertia;
821
822        // Calinski-Harabasz index
823        let ch_score = (between_ss / (k - 1) as f64) / (within_ss / (n_points - k) as f64);
824        Ok(ch_score)
825    }
826
827    /// Update neural network based on performance feedback
828    fn update_network(&mut self, _input: &Array1<f64>, qualityscore: f64) -> SpatialResult<()> {
829        // Store experience for training
830        let target = Array1::from(vec![qualityscore; 8]); // Simplified target
831        self.experience_buffer.push_back((_input.clone(), target));
832
833        // Limit buffer size
834        if self.experience_buffer.len() > 1000 {
835            self.experience_buffer.pop_front();
836        }
837
838        // Train network if we have enough experience
839        if self.experience_buffer.len() >= 32 {
840            self.train_network_batch()?;
841        }
842
843        Ok(())
844    }
845
846    /// Train neural network on a batch of experiences
847    fn train_network_batch(&mut self) -> SpatialResult<()> {
848        let batch_size = 32.min(self.experience_buffer.len());
849
850        for _ in 0..batch_size {
851            if let Some((input, target)) = self.experience_buffer.pop_front() {
852                self.train_single_example(&input, &target)?;
853            }
854        }
855
856        self.training_iterations += 1;
857
858        // Adaptive learning rate
859        if self.adaptive_learning && self.training_iterations.is_multiple_of(100) {
860            self.learning_rate *= 0.95; // Decay learning rate
861        }
862
863        Ok(())
864    }
865
866    /// Train on single example using backpropagation
867    fn train_single_example(
868        &mut self,
869        input: &Array1<f64>,
870        target: &Array1<f64>,
871    ) -> SpatialResult<()> {
872        // Forward pass
873        let mut activations = vec![input.clone()];
874        let mut current = input.clone();
875
876        for layer in &self.layers {
877            let linear_output = layer.weights.dot(&current) + &layer.biases;
878            current = linear_output.mapv(|x| layer.activation.apply(x));
879            activations.push(current.clone());
880        }
881
882        // Calculate loss
883        let output = &activations[activations.len() - 1];
884        let loss: f64 = target
885            .iter()
886            .zip(output.iter())
887            .map(|(&t, &o)| (t - o).powi(2))
888            .sum::<f64>()
889            / target.len() as f64;
890
891        self.loss_history.push(loss);
892
893        // Backward pass
894        let mut delta = output - target;
895
896        for (i, layer) in self.layers.iter_mut().enumerate().rev() {
897            let layer_input = &activations[i];
898            let _layer_output = &activations[i + 1];
899
900            // Compute gradients
901            let weight_gradients = delta
902                .clone()
903                .insert_axis(Axis(1))
904                .dot(&layer_input.clone().insert_axis(Axis(0)));
905            let bias_gradients = delta.clone();
906
907            // Update weights and biases
908            layer.weights = &layer.weights - self.learning_rate * &weight_gradients;
909            layer.biases = &layer.biases - self.learning_rate * &bias_gradients;
910
911            // Compute delta for next layer
912            if i > 0 {
913                let prev_layer_output = &activations[i];
914                delta = layer.weights.t().dot(&delta);
915
916                // Apply derivative of activation function
917                for (j, &output_val) in prev_layer_output.iter().enumerate() {
918                    delta[j] *= layer.activation.derivative(output_val);
919                }
920            }
921        }
922
923        Ok(())
924    }
925}
926
927/// Clustering parameters optimized by neural network
928#[derive(Debug, Clone)]
929pub struct ClusteringParameters {
930    pub num_clusters: usize,
931    pub max_iterations: usize,
932    pub tolerance: f64,
933    pub init_method: InitMethod,
934    pub distance_metric: DistanceMetric,
935    pub regularization: f64,
936    pub early_stopping: bool,
937    pub adaptive_parameters: bool,
938}
939
940/// Initialization methods for clustering
941#[derive(Debug, Clone, Copy, PartialEq)]
942pub enum InitMethod {
943    Random,
944    KMeansPlusPlus,
945}
946
947/// Distance metrics for clustering
948#[derive(Debug, Clone, PartialEq)]
949pub enum DistanceMetric {
950    Euclidean,
951    Manhattan,
952    Cosine,
953    Minkowski(f64),
954}
955
956/// Clustering result
957#[derive(Debug, Clone)]
958pub struct ClusteringResult {
959    pub centroids: Array2<f64>,
960    pub assignments: Array1<usize>,
961    pub inertia: f64,
962}
963
964/// Reinforcement learning algorithm selector
965#[allow(dead_code)]
966#[derive(Debug, Clone)]
967pub struct ReinforcementLearningSelector {
968    /// Q-table for algorithm selection
969    q_table: HashMap<StateAction, f64>,
970    /// Epsilon for epsilon-greedy exploration
971    epsilon: f64,
972    /// Learning rate
973    learning_rate: f64,
974    /// Discount factor
975    gamma: f64,
976    /// Experience replay buffer
977    experience_buffer: VecDeque<Experience>,
978    /// Target network (for DQN)
979    target_q_table: Option<HashMap<StateAction, f64>>,
980    /// Episode count
981    episodes: usize,
982}
983
984/// State-action pair for Q-learning
985#[derive(Debug, Clone, Hash, PartialEq, Eq)]
986pub struct StateAction {
987    pub state: DataState,
988    pub action: SpatialAlgorithm,
989}
990
991/// Data state representation
992#[derive(Debug, Clone, Hash, PartialEq, Eq)]
993pub struct DataState {
994    pub size_category: SizeCategory,
995    pub dimensionality_category: DimensionalityCategory,
996    pub density_category: DensityCategory,
997    pub clustering_tendency_category: ClusteringTendencyCategory,
998}
999
1000/// Data size categories
1001#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1002pub enum SizeCategory {
1003    Small,  // < 1000 points
1004    Medium, // 1000 - 10000 points
1005    Large,  // > 10000 points
1006}
1007
1008/// Dimensionality categories
1009#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1010pub enum DimensionalityCategory {
1011    Low,    // < 5 dimensions
1012    Medium, // 5 - 20 dimensions
1013    High,   // > 20 dimensions
1014}
1015
1016/// Density categories
1017#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1018pub enum DensityCategory {
1019    Low,
1020    Medium,
1021    High,
1022}
1023
1024/// Clustering tendency categories
1025#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1026pub enum ClusteringTendencyCategory {
1027    Random,
1028    Structured,
1029    HighlyStructured,
1030}
1031
1032/// Available spatial algorithms
1033#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1034pub enum SpatialAlgorithm {
1035    KMeans,
1036    DBScan,
1037    HierarchicalClustering,
1038    GaussianMixture,
1039    SpectralClustering,
1040    KDTree,
1041    BallTree,
1042}
1043
1044/// Experience tuple for reinforcement learning
1045#[derive(Debug, Clone)]
1046pub struct Experience {
1047    pub state: DataState,
1048    pub action: SpatialAlgorithm,
1049    pub reward: f64,
1050    pub next_state: DataState,
1051    pub done: bool,
1052}
1053
1054impl Default for ReinforcementLearningSelector {
1055    fn default() -> Self {
1056        Self::new()
1057    }
1058}
1059
1060impl ReinforcementLearningSelector {
1061    /// Create new reinforcement learning selector
1062    pub fn new() -> Self {
1063        Self {
1064            q_table: HashMap::new(),
1065            epsilon: 0.1,
1066            learning_rate: 0.1,
1067            gamma: 0.9,
1068            experience_buffer: VecDeque::new(),
1069            target_q_table: None,
1070            episodes: 0,
1071        }
1072    }
1073
1074    /// Configure epsilon for exploration
1075    pub fn with_epsilon_greedy(&mut self, epsilon: f64) -> &mut Self {
1076        self.epsilon = epsilon;
1077        self
1078    }
1079
1080    /// Enable experience replay
1081    pub fn with_experience_replay(&mut self, enabled: bool) -> &mut Self {
1082        if enabled {
1083            self.experience_buffer = VecDeque::new();
1084        }
1085        self
1086    }
1087
1088    /// Enable target network
1089    pub fn with_target_network(&mut self, enabled: bool) -> &mut Self {
1090        if enabled {
1091            self.target_q_table = Some(HashMap::new());
1092        }
1093        self
1094    }
1095
1096    /// Select best algorithm for given data
1097    pub fn select_best_algorithm(
1098        &mut self,
1099        points: &ArrayView2<'_, f64>,
1100    ) -> SpatialResult<SpatialAlgorithm> {
1101        let state = self.analyze_data_state(points)?;
1102
1103        // Epsilon-greedy action selection
1104        if scirs2_core::random::random::<f64>() < self.epsilon {
1105            // Explore: random action
1106            Ok(self.random_algorithm())
1107        } else {
1108            // Exploit: best known action
1109            Ok(self.best_algorithm_for_state(&state))
1110        }
1111    }
1112
1113    /// Analyze data to determine state
1114    fn analyze_data_state(&self, points: &ArrayView2<'_, f64>) -> SpatialResult<DataState> {
1115        let (n_points, n_dims) = points.dim();
1116
1117        let size_category = match n_points {
1118            0..=999 => SizeCategory::Small,
1119            1000..=9999 => SizeCategory::Medium,
1120            _ => SizeCategory::Large,
1121        };
1122
1123        let dimensionality_category = match n_dims {
1124            0..=4 => DimensionalityCategory::Low,
1125            5..=20 => DimensionalityCategory::Medium,
1126            _ => DimensionalityCategory::High,
1127        };
1128
1129        // Estimate density
1130        let density = self.estimate_density(points)?;
1131        let density_category = if density < 0.3 {
1132            DensityCategory::Low
1133        } else if density < 0.7 {
1134            DensityCategory::Medium
1135        } else {
1136            DensityCategory::High
1137        };
1138
1139        // Estimate clustering tendency
1140        let hopkins = self.estimate_hopkins_statistic(points)?;
1141        let clustering_tendency_category = if hopkins < 0.3 {
1142            ClusteringTendencyCategory::HighlyStructured
1143        } else if hopkins < 0.7 {
1144            ClusteringTendencyCategory::Structured
1145        } else {
1146            ClusteringTendencyCategory::Random
1147        };
1148
1149        Ok(DataState {
1150            size_category,
1151            dimensionality_category,
1152            density_category,
1153            clustering_tendency_category,
1154        })
1155    }
1156
1157    /// Estimate data density
1158    fn estimate_density(&self, points: &ArrayView2<'_, f64>) -> SpatialResult<f64> {
1159        let (n_points, n_dims) = points.dim();
1160
1161        if n_points < 2 {
1162            return Ok(0.0);
1163        }
1164
1165        let sample_size = n_points.min(100);
1166        let mut total_inverse_distance = 0.0;
1167        let mut count = 0;
1168
1169        for i in 0..sample_size {
1170            let mut nearest_distance = f64::INFINITY;
1171
1172            for j in 0..n_points {
1173                if i != j {
1174                    let dist: f64 = points
1175                        .row(i)
1176                        .iter()
1177                        .zip(points.row(j).iter())
1178                        .map(|(&a, &b)| (a - b).powi(2))
1179                        .sum::<f64>()
1180                        .sqrt();
1181
1182                    if dist < nearest_distance {
1183                        nearest_distance = dist;
1184                    }
1185                }
1186            }
1187
1188            if nearest_distance > 0.0 && nearest_distance.is_finite() {
1189                total_inverse_distance += 1.0 / nearest_distance;
1190                count += 1;
1191            }
1192        }
1193
1194        Ok(if count > 0 {
1195            total_inverse_distance / count as f64 / 10.0
1196        } else {
1197            0.0
1198        })
1199    }
1200
1201    /// Estimate Hopkins statistic
1202    fn estimate_hopkins_statistic(&self, points: &ArrayView2<'_, f64>) -> SpatialResult<f64> {
1203        let (n_points, n_dims) = points.dim();
1204
1205        if n_points < 10 {
1206            return Ok(0.5);
1207        }
1208
1209        let sample_size = n_points.min(20);
1210        let mut real_distances = Vec::new();
1211        let mut random_distances = Vec::new();
1212
1213        // Real point distances
1214        for i in 0..sample_size {
1215            let mut min_dist = f64::INFINITY;
1216            for j in 0..n_points {
1217                if i != j {
1218                    let dist: f64 = points
1219                        .row(i)
1220                        .iter()
1221                        .zip(points.row(j).iter())
1222                        .map(|(&a, &b)| (a - b).powi(2))
1223                        .sum::<f64>()
1224                        .sqrt();
1225                    min_dist = min_dist.min(dist);
1226                }
1227            }
1228            real_distances.push(min_dist);
1229        }
1230
1231        // Random point distances
1232        let bounds = self.get_data_bounds(points);
1233        for _ in 0..sample_size {
1234            let random_point: Array1<f64> = Array1::from_shape_fn(n_dims, |i| {
1235                scirs2_core::random::random::<f64>() * (bounds[i].1 - bounds[i].0) + bounds[i].0
1236            });
1237
1238            let mut min_dist = f64::INFINITY;
1239            for j in 0..n_points {
1240                let dist: f64 = random_point
1241                    .iter()
1242                    .zip(points.row(j).iter())
1243                    .map(|(&a, &b)| (a - b).powi(2))
1244                    .sum::<f64>()
1245                    .sqrt();
1246                min_dist = min_dist.min(dist);
1247            }
1248            random_distances.push(min_dist);
1249        }
1250
1251        let sum_random: f64 = random_distances.iter().sum();
1252        let sum_real: f64 = real_distances.iter().sum();
1253        let hopkins = sum_random / (sum_random + sum_real);
1254
1255        Ok(hopkins)
1256    }
1257
1258    /// Get data bounds
1259    fn get_data_bounds(&self, points: &ArrayView2<'_, f64>) -> Vec<(f64, f64)> {
1260        let (_, n_dims) = points.dim();
1261        let mut bounds = Vec::new();
1262
1263        for dim in 0..n_dims {
1264            let column = points.column(dim);
1265            let min_val = column.fold(f64::INFINITY, |a, &b| a.min(b));
1266            let max_val = column.fold(f64::NEG_INFINITY, |a, &b| a.max(b));
1267            bounds.push((min_val, max_val));
1268        }
1269
1270        bounds
1271    }
1272
1273    /// Select random algorithm for exploration
1274    fn random_algorithm(&self) -> SpatialAlgorithm {
1275        let algorithms = [
1276            SpatialAlgorithm::KMeans,
1277            SpatialAlgorithm::DBScan,
1278            SpatialAlgorithm::HierarchicalClustering,
1279            SpatialAlgorithm::GaussianMixture,
1280            SpatialAlgorithm::SpectralClustering,
1281            SpatialAlgorithm::KDTree,
1282            SpatialAlgorithm::BallTree,
1283        ];
1284
1285        let index = (scirs2_core::random::random::<f64>() * algorithms.len() as f64) as usize;
1286        algorithms[index.min(algorithms.len() - 1)].clone()
1287    }
1288
1289    /// Find best algorithm for given state
1290    fn best_algorithm_for_state(&self, state: &DataState) -> SpatialAlgorithm {
1291        let algorithms = [
1292            SpatialAlgorithm::KMeans,
1293            SpatialAlgorithm::DBScan,
1294            SpatialAlgorithm::HierarchicalClustering,
1295            SpatialAlgorithm::GaussianMixture,
1296            SpatialAlgorithm::SpectralClustering,
1297            SpatialAlgorithm::KDTree,
1298            SpatialAlgorithm::BallTree,
1299        ];
1300
1301        let mut best_algorithm = SpatialAlgorithm::KMeans;
1302        let mut best_q_value = f64::NEG_INFINITY;
1303
1304        for algorithm in &algorithms {
1305            let state_action = StateAction {
1306                state: state.clone(),
1307                action: algorithm.clone(),
1308            };
1309
1310            let q_value = self.q_table.get(&state_action).unwrap_or(&0.0);
1311            if *q_value > best_q_value {
1312                best_q_value = *q_value;
1313                best_algorithm = algorithm.clone();
1314            }
1315        }
1316
1317        best_algorithm
1318    }
1319
1320    /// Update Q-values based on experience
1321    pub fn update_q_values(&mut self, experience: Experience) -> SpatialResult<()> {
1322        let state_action = StateAction {
1323            state: experience.state.clone(),
1324            action: experience.action.clone(),
1325        };
1326
1327        let current_q = *self.q_table.get(&state_action).unwrap_or(&0.0);
1328
1329        // Find maximum Q-value for next state
1330        let max_next_q = if experience.done {
1331            0.0
1332        } else {
1333            self.max_q_value_for_state(&experience.next_state)
1334        };
1335
1336        // Q-learning update rule
1337        let new_q = current_q
1338            + self.learning_rate * (experience.reward + self.gamma * max_next_q - current_q);
1339
1340        self.q_table.insert(state_action, new_q);
1341
1342        // Store experience for replay
1343        self.experience_buffer.push_back(experience);
1344        if self.experience_buffer.len() > 10000 {
1345            self.experience_buffer.pop_front();
1346        }
1347
1348        // Perform experience replay
1349        if self.experience_buffer.len() >= 32 {
1350            self.replay_experience()?;
1351        }
1352
1353        Ok(())
1354    }
1355
1356    /// Find maximum Q-value for given state
1357    fn max_q_value_for_state(&self, state: &DataState) -> f64 {
1358        let algorithms = [
1359            SpatialAlgorithm::KMeans,
1360            SpatialAlgorithm::DBScan,
1361            SpatialAlgorithm::HierarchicalClustering,
1362            SpatialAlgorithm::GaussianMixture,
1363            SpatialAlgorithm::SpectralClustering,
1364            SpatialAlgorithm::KDTree,
1365            SpatialAlgorithm::BallTree,
1366        ];
1367
1368        algorithms
1369            .iter()
1370            .map(|algorithm| {
1371                let state_action = StateAction {
1372                    state: state.clone(),
1373                    action: algorithm.clone(),
1374                };
1375                *self.q_table.get(&state_action).unwrap_or(&0.0)
1376            })
1377            .fold(f64::NEG_INFINITY, |a, b| a.max(b))
1378    }
1379
1380    /// Perform experience replay training
1381    fn replay_experience(&mut self) -> SpatialResult<()> {
1382        let batch_size = 32.min(self.experience_buffer.len());
1383
1384        for _ in 0..batch_size {
1385            if let Some(experience) = self.experience_buffer.pop_front() {
1386                let state_action = StateAction {
1387                    state: experience.state.clone(),
1388                    action: experience.action.clone(),
1389                };
1390
1391                let current_q = *self.q_table.get(&state_action).unwrap_or(&0.0);
1392                let max_next_q = if experience.done {
1393                    0.0
1394                } else {
1395                    self.max_q_value_for_state(&experience.next_state)
1396                };
1397
1398                let new_q = current_q
1399                    + self.learning_rate
1400                        * (experience.reward + self.gamma * max_next_q - current_q);
1401
1402                self.q_table.insert(state_action, new_q);
1403            }
1404        }
1405
1406        Ok(())
1407    }
1408}
1409
1410#[cfg(test)]
1411mod tests {
1412    use super::*;
1413    use scirs2_core::ndarray::array;
1414
1415    #[test]
1416    fn test_activation_functions() {
1417        assert_eq!(ActivationFunction::ReLU.apply(1.0), 1.0);
1418        assert_eq!(ActivationFunction::ReLU.apply(-1.0), 0.0);
1419
1420        let sigmoid_result = ActivationFunction::Sigmoid.apply(0.0);
1421        assert!((sigmoid_result - 0.5).abs() < 1e-10);
1422    }
1423
1424    #[test]
1425    fn test_neural_spatial_optimizer_creation() {
1426        let mut optimizer =
1427            NeuralSpatialOptimizer::new().with_network_architecture([10, 64, 32, 8]);
1428        optimizer.with_learning_rate(0.001);
1429        optimizer.with_adaptive_learning(true);
1430
1431        assert_eq!(optimizer.layers.len(), 3);
1432        assert_eq!(optimizer.learning_rate, 0.001);
1433        assert!(optimizer.adaptive_learning);
1434    }
1435
1436    #[test]
1437    fn test_clustering_parameters() {
1438        let params = ClusteringParameters {
1439            num_clusters: 3,
1440            max_iterations: 100,
1441            tolerance: 1e-6,
1442            init_method: InitMethod::KMeansPlusPlus,
1443            distance_metric: DistanceMetric::Euclidean,
1444            regularization: 0.01,
1445            early_stopping: true,
1446            adaptive_parameters: false,
1447        };
1448
1449        assert_eq!(params.num_clusters, 3);
1450        assert_eq!(params.init_method, InitMethod::KMeansPlusPlus);
1451    }
1452
1453    #[test]
1454    fn test_reinforcement_learning_selector() {
1455        let mut selector = ReinforcementLearningSelector::new();
1456        selector.with_epsilon_greedy(0.1);
1457        selector.with_experience_replay(true);
1458
1459        let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
1460        let algorithm = selector.select_best_algorithm(&points.view());
1461
1462        assert!(algorithm.is_ok());
1463    }
1464
1465    #[test]
1466    fn test_data_state_analysis() {
1467        let selector = ReinforcementLearningSelector::new();
1468        let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]];
1469
1470        let state = selector.analyze_data_state(&points.view());
1471        assert!(state.is_ok());
1472
1473        let data_state = state.unwrap();
1474        assert_eq!(data_state.size_category, SizeCategory::Small);
1475        assert_eq!(
1476            data_state.dimensionality_category,
1477            DimensionalityCategory::Low
1478        );
1479    }
1480
1481    #[test]
1482    fn test_neural_optimizer_feature_extraction() {
1483        let optimizer = NeuralSpatialOptimizer::new();
1484        let points = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
1485
1486        let features = optimizer.extract_spatial_features(&points.view());
1487        assert!(features.is_ok());
1488
1489        let feature_vector = features.unwrap();
1490        assert!(!feature_vector.is_empty());
1491    }
1492}