Skip to main content

sklears_decomposition/
manifold.rs

1//! Manifold Learning algorithms for non-linear dimensionality reduction
2//!
3//! This module provides various manifold learning techniques:
4//! - Locally Linear Embedding (LLE)
5//! - Isomap (Isometric Mapping)
6//! - Laplacian Eigenmaps
7//! - t-Distributed Stochastic Neighbor Embedding (t-SNE)
8//! - Uniform Manifold Approximation and Projection (UMAP)
9
10use scirs2_core::ndarray::{Array1, Array2, Axis};
11use scirs2_core::random::rngs::StdRng;
12use scirs2_core::random::{rng as make_rng, RngExt, SeedableRng};
13use scirs2_linalg::compat::{ArrayLinalgExt, UPLO};
14#[cfg(feature = "serde")]
15use serde::{Deserialize, Serialize};
16use sklears_core::{
17    error::{Result, SklearsError},
18    traits::{Fit, Transform, Untrained},
19};
20
21/// Type alias for manifold learning method results
22/// Returns: (embedding, optional_weights, optional_distances, iterations)
23type ManifoldResult = Result<(Array2<f64>, Option<Array2<f64>>, Option<Array2<f64>>, usize)>;
24
25/// Manifold learning algorithm variants
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
28pub enum ManifoldAlgorithm {
29    /// Locally Linear Embedding
30    #[default]
31    LLE,
32    /// Isomap (Isometric Mapping)
33    Isomap,
34    /// Laplacian Eigenmaps
35    LaplacianEigenmaps,
36    /// t-Distributed Stochastic Neighbor Embedding
37    TSNE,
38    /// Uniform Manifold Approximation and Projection
39    UMAP,
40}
41
42/// Distance metrics for manifold learning
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
45pub enum DistanceMetric {
46    /// Euclidean distance
47    #[default]
48    Euclidean,
49    /// Manhattan distance
50    Manhattan,
51    /// Cosine distance
52    Cosine,
53}
54
55/// Manifold Learning transformer
56#[derive(Debug, Clone)]
57pub struct ManifoldLearning<State = Untrained> {
58    /// Algorithm to use
59    pub algorithm: ManifoldAlgorithm,
60    /// Target dimensionality
61    pub n_components: usize,
62    /// Number of neighbors for local methods
63    pub n_neighbors: usize,
64    /// Distance metric
65    pub metric: DistanceMetric,
66    /// Maximum number of iterations (for iterative methods)
67    pub max_iter: usize,
68    /// Learning rate (for gradient-based methods)
69    pub learning_rate: f64,
70    /// Perplexity (for t-SNE)
71    pub perplexity: f64,
72    /// Random state for reproducibility
73    pub random_state: Option<u64>,
74    /// Early exaggeration factor (for t-SNE)
75    pub early_exaggeration: f64,
76    /// Minimum distance (for UMAP)
77    pub min_dist: f64,
78    /// Spread (for UMAP)
79    pub spread: f64,
80
81    /// Trained state
82    state: State,
83}
84
85/// Trained manifold learning state
86#[derive(Debug, Clone)]
87#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
88pub struct TrainedManifoldLearning {
89    pub embedding: Array2<f64>,
90    pub training_data: Array2<f64>,
91    pub neighbor_graph: Option<Array2<f64>>,
92    pub distance_matrix: Option<Array2<f64>>,
93    pub algorithm: ManifoldAlgorithm,
94    pub metric: DistanceMetric,
95    pub n_features_in: usize,
96    pub n_components: usize,
97    pub n_iter: usize,
98}
99
100impl ManifoldLearning<Untrained> {
101    /// Create a new manifold learning transformer
102    pub fn new(algorithm: ManifoldAlgorithm, n_components: usize) -> Self {
103        Self {
104            algorithm,
105            n_components,
106            n_neighbors: 5,
107            metric: DistanceMetric::Euclidean,
108            max_iter: 1000,
109            learning_rate: 200.0,
110            perplexity: 30.0,
111            random_state: None,
112            early_exaggeration: 12.0,
113            min_dist: 0.1,
114            spread: 1.0,
115            state: Untrained,
116        }
117    }
118
119    /// Set number of neighbors
120    pub fn n_neighbors(mut self, n_neighbors: usize) -> Self {
121        self.n_neighbors = n_neighbors;
122        self
123    }
124
125    /// Set distance metric
126    pub fn metric(mut self, metric: DistanceMetric) -> Self {
127        self.metric = metric;
128        self
129    }
130
131    /// Set maximum iterations
132    pub fn max_iter(mut self, max_iter: usize) -> Self {
133        self.max_iter = max_iter;
134        self
135    }
136
137    /// Set learning rate
138    pub fn learning_rate(mut self, learning_rate: f64) -> Self {
139        self.learning_rate = learning_rate;
140        self
141    }
142
143    /// Set perplexity (for t-SNE)
144    pub fn perplexity(mut self, perplexity: f64) -> Self {
145        self.perplexity = perplexity;
146        self
147    }
148
149    /// Set random state
150    pub fn random_state(mut self, random_state: u64) -> Self {
151        self.random_state = Some(random_state);
152        self
153    }
154
155    /// Set early exaggeration (for t-SNE)
156    pub fn early_exaggeration(mut self, early_exaggeration: f64) -> Self {
157        self.early_exaggeration = early_exaggeration;
158        self
159    }
160
161    /// Set minimum distance (for UMAP)
162    pub fn min_dist(mut self, min_dist: f64) -> Self {
163        self.min_dist = min_dist;
164        self
165    }
166
167    /// Set spread (for UMAP)
168    pub fn spread(mut self, spread: f64) -> Self {
169        self.spread = spread;
170        self
171    }
172}
173
174impl Fit<Array2<f64>, ()> for ManifoldLearning<Untrained> {
175    type Fitted = ManifoldLearning<TrainedManifoldLearning>;
176
177    fn fit(self, x: &Array2<f64>, _y: &()) -> Result<Self::Fitted> {
178        let (n_samples, n_features) = x.dim();
179
180        if n_samples < self.n_components {
181            return Err(SklearsError::InvalidInput(
182                "Number of samples must be greater than n_components".to_string(),
183            ));
184        }
185
186        if self.n_neighbors >= n_samples {
187            return Err(SklearsError::InvalidInput(
188                "n_neighbors must be less than number of samples".to_string(),
189            ));
190        }
191
192        let (embedding, neighbor_graph, distance_matrix, n_iter) = match self.algorithm {
193            ManifoldAlgorithm::LLE => self.locally_linear_embedding(x)?,
194            ManifoldAlgorithm::Isomap => self.isomap(x)?,
195            ManifoldAlgorithm::LaplacianEigenmaps => self.laplacian_eigenmaps(x)?,
196            ManifoldAlgorithm::TSNE => self.tsne(x)?,
197            ManifoldAlgorithm::UMAP => self.umap(x)?,
198        };
199
200        Ok(ManifoldLearning {
201            algorithm: self.algorithm,
202            n_components: self.n_components,
203            n_neighbors: self.n_neighbors,
204            metric: self.metric,
205            max_iter: self.max_iter,
206            learning_rate: self.learning_rate,
207            perplexity: self.perplexity,
208            random_state: self.random_state,
209            early_exaggeration: self.early_exaggeration,
210            min_dist: self.min_dist,
211            spread: self.spread,
212            state: TrainedManifoldLearning {
213                embedding,
214                training_data: x.clone(),
215                neighbor_graph,
216                distance_matrix,
217                algorithm: self.algorithm,
218                metric: self.metric,
219                n_features_in: n_features,
220                n_components: self.n_components,
221                n_iter,
222            },
223        })
224    }
225}
226
227impl Transform<Array2<f64>, Array2<f64>> for ManifoldLearning<TrainedManifoldLearning> {
228    fn transform(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
229        let (_n_samples, n_features) = x.dim();
230
231        if n_features != self.state.n_features_in {
232            return Err(SklearsError::FeatureMismatch {
233                expected: self.state.n_features_in,
234                actual: n_features,
235            });
236        }
237
238        // Out-of-sample extension (simplified approach)
239        match self.state.algorithm {
240            ManifoldAlgorithm::LLE => self.lle_transform(x),
241            ManifoldAlgorithm::Isomap => self.isomap_transform(x),
242            ManifoldAlgorithm::LaplacianEigenmaps => self.laplacian_transform(x),
243            ManifoldAlgorithm::TSNE => {
244                // t-SNE doesn't have a natural out-of-sample extension
245                Err(SklearsError::InvalidInput(
246                    "t-SNE does not support out-of-sample transformation".to_string(),
247                ))
248            }
249            ManifoldAlgorithm::UMAP => self.umap_transform(x),
250        }
251    }
252}
253
254impl ManifoldLearning<Untrained> {
255    /// Locally Linear Embedding (LLE)
256    fn locally_linear_embedding(&self, x: &Array2<f64>) -> ManifoldResult {
257        let (_n_samples, _) = x.dim();
258
259        // Step 1: Find k-nearest neighbors
260        let neighbors = self.find_knn(x, self.n_neighbors)?;
261
262        // Step 2: Compute reconstruction weights
263        let weights = self.compute_lle_weights(x, &neighbors)?;
264
265        // Step 3: Find low-dimensional embedding
266        let embedding = self.compute_lle_embedding(&weights)?;
267
268        Ok((embedding, Some(weights), None, 1))
269    }
270
271    /// Isomap algorithm
272    fn isomap(&self, x: &Array2<f64>) -> ManifoldResult {
273        let (_n_samples, _) = x.dim();
274
275        // Step 1: Build neighborhood graph
276        let neighbors = self.find_knn(x, self.n_neighbors)?;
277        let distance_matrix = self.compute_distance_matrix(x)?;
278
279        // Step 2: Compute geodesic distances using Floyd-Warshall
280        let geodesic_distances = self.compute_geodesic_distances(&distance_matrix, &neighbors)?;
281
282        // Step 3: Apply classical MDS
283        let embedding = self.classical_mds(&geodesic_distances)?;
284
285        Ok((embedding, None, Some(geodesic_distances), 1))
286    }
287
288    /// Laplacian Eigenmaps
289    fn laplacian_eigenmaps(&self, x: &Array2<f64>) -> ManifoldResult {
290        let (_n_samples, _) = x.dim();
291
292        // Step 1: Build neighborhood graph
293        let neighbors = self.find_knn(x, self.n_neighbors)?;
294
295        // Step 2: Compute weight matrix (using heat kernel)
296        let weight_matrix = self.compute_laplacian_weights(x, &neighbors)?;
297
298        // Step 3: Compute Laplacian and solve eigenvalue problem
299        let embedding = self.solve_laplacian_eigenproblem(&weight_matrix)?;
300
301        Ok((embedding, Some(weight_matrix), None, 1))
302    }
303
304    /// t-SNE algorithm (simplified version)
305    fn tsne(&self, x: &Array2<f64>) -> ManifoldResult {
306        let (n_samples, _) = x.dim();
307
308        // Initialize random number generator with optional seeding for reproducibility
309        let mut rng: StdRng = match self.random_state {
310            Some(seed) => StdRng::seed_from_u64(seed),
311            None => StdRng::from_rng(&mut make_rng()),
312        };
313
314        // Step 1: Compute pairwise similarities in high-dimensional space
315        let p_matrix = self.compute_tsne_similarities(x)?;
316
317        // Step 2: Initialize low-dimensional embedding randomly
318        let mut embedding = Array2::zeros((n_samples, self.n_components));
319        for i in 0..n_samples {
320            for j in 0..self.n_components {
321                embedding[[i, j]] = rng.random::<f64>() * 1e-4;
322            }
323        }
324
325        // Step 3: Optimize embedding using gradient descent
326        let n_iter = self.optimize_tsne_embedding(&mut embedding, &p_matrix)?;
327
328        Ok((embedding, Some(p_matrix), None, n_iter))
329    }
330
331    /// UMAP algorithm (simplified version)
332    fn umap(&self, x: &Array2<f64>) -> ManifoldResult {
333        let (n_samples, _) = x.dim();
334
335        // Initialize random number generator with optional seeding for reproducibility
336        let mut rng: StdRng = match self.random_state {
337            Some(seed) => StdRng::seed_from_u64(seed),
338            None => StdRng::from_rng(&mut make_rng()),
339        };
340
341        // Step 1: Build fuzzy topological representation
342        let neighbors = self.find_knn(x, self.n_neighbors)?;
343        let fuzzy_graph = self.build_fuzzy_simplicial_set(x, &neighbors)?;
344
345        // Step 2: Initialize low-dimensional embedding
346        let mut embedding = Array2::zeros((n_samples, self.n_components));
347        for i in 0..n_samples {
348            for j in 0..self.n_components {
349                embedding[[i, j]] = (rng.random::<f64>() - 0.5) * 20.0;
350            }
351        }
352
353        // Step 3: Optimize embedding
354        let n_iter = self.optimize_umap_embedding(&mut embedding, &fuzzy_graph)?;
355
356        Ok((embedding, Some(fuzzy_graph), None, n_iter))
357    }
358
359    /// Find k-nearest neighbors
360    fn find_knn(&self, x: &Array2<f64>, k: usize) -> Result<Array2<usize>> {
361        let (n_samples, _) = x.dim();
362        let mut neighbors = Array2::zeros((n_samples, k));
363
364        for i in 0..n_samples {
365            let mut distances: Vec<(f64, usize)> = Vec::new();
366
367            for j in 0..n_samples {
368                if i != j {
369                    let distance = self.compute_distance(&x.row(i), &x.row(j));
370                    distances.push((distance, j));
371                }
372            }
373
374            distances.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
375
376            for (idx, &(_, neighbor_idx)) in distances.iter().take(k).enumerate() {
377                neighbors[[i, idx]] = neighbor_idx;
378            }
379        }
380
381        Ok(neighbors)
382    }
383
384    /// Compute distance between two points
385    fn compute_distance(
386        &self,
387        a: &scirs2_core::ndarray::ArrayView1<f64>,
388        b: &scirs2_core::ndarray::ArrayView1<f64>,
389    ) -> f64 {
390        match self.metric {
391            DistanceMetric::Euclidean => a
392                .iter()
393                .zip(b.iter())
394                .map(|(&x, &y)| (x - y) * (x - y))
395                .sum::<f64>()
396                .sqrt(),
397            DistanceMetric::Manhattan => a
398                .iter()
399                .zip(b.iter())
400                .map(|(&x, &y)| (x - y).abs())
401                .sum::<f64>(),
402            DistanceMetric::Cosine => {
403                let dot_product = a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum::<f64>();
404                let norm_a = a.iter().map(|&x| x * x).sum::<f64>().sqrt();
405                let norm_b = b.iter().map(|&x| x * x).sum::<f64>().sqrt();
406
407                if norm_a > 1e-12 && norm_b > 1e-12 {
408                    1.0 - (dot_product / (norm_a * norm_b))
409                } else {
410                    0.0
411                }
412            }
413        }
414    }
415
416    /// Compute distance matrix
417    fn compute_distance_matrix(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
418        let (n_samples, _) = x.dim();
419        let mut distance_matrix = Array2::zeros((n_samples, n_samples));
420
421        for i in 0..n_samples {
422            for j in i + 1..n_samples {
423                let distance = self.compute_distance(&x.row(i), &x.row(j));
424                distance_matrix[[i, j]] = distance;
425                distance_matrix[[j, i]] = distance;
426            }
427        }
428
429        Ok(distance_matrix)
430    }
431
432    /// Compute LLE reconstruction weights
433    fn compute_lle_weights(
434        &self,
435        x: &Array2<f64>,
436        neighbors: &Array2<usize>,
437    ) -> Result<Array2<f64>> {
438        let (n_samples, n_features) = x.dim();
439        let k = neighbors.ncols();
440        let mut weights = Array2::zeros((n_samples, n_samples));
441
442        for i in 0..n_samples {
443            // Build local covariance matrix
444            let mut local_cov = Array2::zeros((k, k));
445
446            for a in 0..k {
447                for b in 0..k {
448                    let neighbor_a = neighbors[[i, a]];
449                    let neighbor_b = neighbors[[i, b]];
450
451                    let mut cov = 0.0;
452                    for d in 0..n_features {
453                        let diff_a = x[[neighbor_a, d]] - x[[i, d]];
454                        let diff_b = x[[neighbor_b, d]] - x[[i, d]];
455                        cov += diff_a * diff_b;
456                    }
457                    local_cov[[a, b]] = cov;
458                }
459            }
460
461            // Add regularization
462            for j in 0..k {
463                local_cov[[j, j]] += 1e-3;
464            }
465
466            // Solve for weights (least squares solution)
467            let ones = Array1::ones(k);
468            let weights_local = self.solve_linear_system(&local_cov, &ones)?;
469
470            // Normalize weights
471            let weight_sum = weights_local.sum();
472            if weight_sum > 1e-12 {
473                for (idx, &neighbor_idx) in neighbors.row(i).iter().enumerate() {
474                    weights[[i, neighbor_idx]] = weights_local[idx] / weight_sum;
475                }
476            }
477        }
478
479        Ok(weights)
480    }
481
482    /// Solve linear system Ax = b
483    fn solve_linear_system(&self, a: &Array2<f64>, b: &Array1<f64>) -> Result<Array1<f64>> {
484        // Use scirs2-linalg solve method
485        let solution = a.solve(b).map_err(|e| {
486            SklearsError::NumericalError(format!("Failed to solve linear system: {}", e))
487        })?;
488
489        Ok(solution)
490    }
491
492    /// Compute LLE embedding using eigendecomposition
493    fn compute_lle_embedding(&self, weights: &Array2<f64>) -> Result<Array2<f64>> {
494        let n_samples = weights.nrows();
495
496        // Compute M = (I - W)^T (I - W)
497        let identity = Array2::eye(n_samples);
498        let i_minus_w = &identity - weights;
499        let m_matrix = i_minus_w.t().dot(&i_minus_w);
500
501        // Find smallest eigenvalues and eigenvectors
502        let eigenresult = self.compute_smallest_eigenvectors(&m_matrix)?;
503
504        Ok(eigenresult)
505    }
506
507    /// Compute geodesic distances using Floyd-Warshall algorithm
508    fn compute_geodesic_distances(
509        &self,
510        distance_matrix: &Array2<f64>,
511        neighbors: &Array2<usize>,
512    ) -> Result<Array2<f64>> {
513        let n_samples = distance_matrix.nrows();
514        let mut geodesic = Array2::from_elem((n_samples, n_samples), f64::INFINITY);
515
516        // Initialize with direct neighbor distances
517        for i in 0..n_samples {
518            geodesic[[i, i]] = 0.0;
519            for &neighbor in neighbors.row(i) {
520                geodesic[[i, neighbor]] = distance_matrix[[i, neighbor]];
521                geodesic[[neighbor, i]] = distance_matrix[[neighbor, i]];
522            }
523        }
524
525        // Floyd-Warshall algorithm
526        for k in 0..n_samples {
527            for i in 0..n_samples {
528                for j in 0..n_samples {
529                    let through_k = geodesic[[i, k]] + geodesic[[k, j]];
530                    if through_k < geodesic[[i, j]] {
531                        geodesic[[i, j]] = through_k;
532                    }
533                }
534            }
535        }
536
537        Ok(geodesic)
538    }
539
540    /// Classical Multidimensional Scaling (MDS)
541    fn classical_mds(&self, distance_matrix: &Array2<f64>) -> Result<Array2<f64>> {
542        let n_samples = distance_matrix.nrows();
543
544        // Convert distances to similarities using double centering
545        let mut similarity = Array2::zeros((n_samples, n_samples));
546
547        for i in 0..n_samples {
548            for j in 0..n_samples {
549                similarity[[i, j]] = -0.5 * distance_matrix[[i, j]] * distance_matrix[[i, j]];
550            }
551        }
552
553        // Double centering
554        let row_means = similarity.mean_axis(Axis(1)).ok_or_else(|| {
555            SklearsError::NumericalError("cannot compute row means of empty matrix".to_string())
556        })?;
557        let col_means = similarity.mean_axis(Axis(0)).ok_or_else(|| {
558            SklearsError::NumericalError("cannot compute col means of empty matrix".to_string())
559        })?;
560        let grand_mean = row_means.mean().ok_or_else(|| {
561            SklearsError::NumericalError("cannot compute grand mean of empty array".to_string())
562        })?;
563
564        for i in 0..n_samples {
565            for j in 0..n_samples {
566                similarity[[i, j]] = similarity[[i, j]] - row_means[i] - col_means[j] + grand_mean;
567            }
568        }
569
570        // Eigendecomposition and take top components
571        self.compute_largest_eigenvectors(&similarity)
572    }
573
574    /// Compute Laplacian weights using heat kernel
575    fn compute_laplacian_weights(
576        &self,
577        x: &Array2<f64>,
578        neighbors: &Array2<usize>,
579    ) -> Result<Array2<f64>> {
580        let (n_samples, _) = x.dim();
581        let mut weight_matrix = Array2::zeros((n_samples, n_samples));
582
583        // Estimate sigma parameter
584        let sigma = self.estimate_sigma(x, neighbors)?;
585
586        for i in 0..n_samples {
587            for &j in neighbors.row(i) {
588                if i != j {
589                    let distance = self.compute_distance(&x.row(i), &x.row(j));
590                    let weight = (-distance * distance / (2.0 * sigma * sigma)).exp();
591                    weight_matrix[[i, j]] = weight;
592                    weight_matrix[[j, i]] = weight;
593                }
594            }
595        }
596
597        Ok(weight_matrix)
598    }
599
600    /// Estimate sigma parameter for heat kernel
601    fn estimate_sigma(&self, x: &Array2<f64>, neighbors: &Array2<usize>) -> Result<f64> {
602        let (n_samples, _) = x.dim();
603        let mut distances = Vec::new();
604
605        for i in 0..n_samples {
606            for &j in neighbors.row(i) {
607                if i != j {
608                    let distance = self.compute_distance(&x.row(i), &x.row(j));
609                    distances.push(distance);
610                }
611            }
612        }
613
614        distances.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
615        let median_distance = distances[distances.len() / 2];
616
617        Ok(median_distance)
618    }
619
620    /// Solve Laplacian eigenvalue problem
621    fn solve_laplacian_eigenproblem(&self, weight_matrix: &Array2<f64>) -> Result<Array2<f64>> {
622        let n_samples = weight_matrix.nrows();
623
624        // Compute degree matrix
625        let mut degree_matrix = Array2::zeros((n_samples, n_samples));
626        for i in 0..n_samples {
627            let degree: f64 = weight_matrix.row(i).sum();
628            degree_matrix[[i, i]] = degree;
629        }
630
631        // Compute Laplacian L = D - W
632        let laplacian = &degree_matrix - weight_matrix;
633
634        // Find smallest eigenvalues (skip the first one which is zero)
635        self.compute_smallest_eigenvectors(&laplacian)
636    }
637
638    /// Compute t-SNE similarities (P matrix)
639    fn compute_tsne_similarities(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
640        let (n_samples, _) = x.dim();
641        let mut p_matrix = Array2::zeros((n_samples, n_samples));
642
643        // Compute pairwise conditional probabilities
644        for i in 0..n_samples {
645            let sigma = self.find_optimal_sigma(x, i)?;
646
647            let mut row_sum = 0.0;
648            for j in 0..n_samples {
649                if i != j {
650                    let distance_sq = self.compute_distance(&x.row(i), &x.row(j)).powi(2);
651                    let prob = (-distance_sq / (2.0 * sigma * sigma)).exp();
652                    p_matrix[[i, j]] = prob;
653                    row_sum += prob;
654                }
655            }
656
657            // Normalize row
658            if row_sum > 1e-12 {
659                for j in 0..n_samples {
660                    if i != j {
661                        p_matrix[[i, j]] /= row_sum;
662                    }
663                }
664            }
665        }
666
667        // Symmetrize: p_ij = (p_i|j + p_j|i) / (2n)
668        let mut symmetric_p = Array2::zeros((n_samples, n_samples));
669        for i in 0..n_samples {
670            for j in 0..n_samples {
671                symmetric_p[[i, j]] =
672                    (p_matrix[[i, j]] + p_matrix[[j, i]]) / (2.0 * n_samples as f64);
673            }
674        }
675
676        Ok(symmetric_p)
677    }
678
679    /// Find optimal sigma for t-SNE using binary search
680    fn find_optimal_sigma(&self, x: &Array2<f64>, i: usize) -> Result<f64> {
681        let target_perplexity = self.perplexity;
682        let mut sigma_min = 1e-20;
683        let mut sigma_max = 1e20;
684        let tolerance = 1e-5;
685        let max_iterations = 50;
686
687        for _ in 0..max_iterations {
688            let sigma = (sigma_min + sigma_max) / 2.0;
689            let perplexity = self.compute_perplexity(x, i, sigma);
690
691            if (perplexity - target_perplexity).abs() < tolerance {
692                return Ok(sigma);
693            }
694
695            if perplexity > target_perplexity {
696                sigma_max = sigma;
697            } else {
698                sigma_min = sigma;
699            }
700        }
701
702        Ok((sigma_min + sigma_max) / 2.0)
703    }
704
705    /// Compute perplexity for given sigma
706    fn compute_perplexity(&self, x: &Array2<f64>, i: usize, sigma: f64) -> f64 {
707        let (n_samples, _) = x.dim();
708        let mut probabilities = Vec::new();
709        let mut sum_prob = 0.0;
710
711        for j in 0..n_samples {
712            if i != j {
713                let distance_sq = self.compute_distance(&x.row(i), &x.row(j)).powi(2);
714                let prob = (-distance_sq / (2.0 * sigma * sigma)).exp();
715                probabilities.push(prob);
716                sum_prob += prob;
717            }
718        }
719
720        // Normalize probabilities
721        for prob in &mut probabilities {
722            *prob /= sum_prob;
723        }
724
725        // Compute entropy
726        let entropy = probabilities
727            .iter()
728            .filter(|&&p| p > 1e-12)
729            .map(|&p| -p * p.ln())
730            .sum::<f64>();
731
732        // Perplexity = 2^entropy
733        2.0_f64.powf(entropy)
734    }
735
736    /// Optimize t-SNE embedding using gradient descent
737    fn optimize_tsne_embedding(
738        &self,
739        embedding: &mut Array2<f64>,
740        p_matrix: &Array2<f64>,
741    ) -> Result<usize> {
742        let (n_samples, n_components) = embedding.dim();
743        let mut momentum = Array2::<f64>::zeros((n_samples, n_components));
744        let momentum_factor = 0.8;
745
746        for iter in 0..self.max_iter {
747            // Compute Q matrix (similarities in low-dimensional space)
748            let q_matrix = self.compute_tsne_q_matrix(embedding)?;
749
750            // Compute gradient
751            let gradient = self.compute_tsne_gradient(embedding, p_matrix, &q_matrix)?;
752
753            // Apply gradient with momentum
754            for i in 0..n_samples {
755                for j in 0..n_components {
756                    momentum[[i, j]] =
757                        momentum_factor * momentum[[i, j]] - self.learning_rate * gradient[[i, j]];
758                    embedding[[i, j]] += momentum[[i, j]];
759                }
760            }
761
762            // Early stopping condition (simplified)
763            if iter > 100 && iter % 100 == 0 {
764                let gradient_norm = gradient.iter().map(|&x| x * x).sum::<f64>().sqrt();
765                if gradient_norm < 1e-6 {
766                    return Ok(iter + 1);
767                }
768            }
769        }
770
771        Ok(self.max_iter)
772    }
773
774    /// Compute Q matrix for t-SNE
775    fn compute_tsne_q_matrix(&self, embedding: &Array2<f64>) -> Result<Array2<f64>> {
776        let (n_samples, _) = embedding.dim();
777        let mut q_matrix = Array2::zeros((n_samples, n_samples));
778        let mut sum_q = 0.0;
779
780        for i in 0..n_samples {
781            for j in 0..n_samples {
782                if i != j {
783                    let mut distance_sq = 0.0;
784                    for d in 0..embedding.ncols() {
785                        let diff = embedding[[i, d]] - embedding[[j, d]];
786                        distance_sq += diff * diff;
787                    }
788
789                    let q = 1.0 / (1.0 + distance_sq);
790                    q_matrix[[i, j]] = q;
791                    sum_q += q;
792                }
793            }
794        }
795
796        // Normalize Q matrix
797        if sum_q > 1e-12 {
798            for i in 0..n_samples {
799                for j in 0..n_samples {
800                    if i != j {
801                        q_matrix[[i, j]] /= sum_q;
802                    }
803                }
804            }
805        }
806
807        Ok(q_matrix)
808    }
809
810    /// Compute t-SNE gradient
811    fn compute_tsne_gradient(
812        &self,
813        embedding: &Array2<f64>,
814        p_matrix: &Array2<f64>,
815        q_matrix: &Array2<f64>,
816    ) -> Result<Array2<f64>> {
817        let (n_samples, n_components) = embedding.dim();
818        let mut gradient = Array2::zeros((n_samples, n_components));
819
820        for i in 0..n_samples {
821            for d in 0..n_components {
822                let mut grad = 0.0;
823
824                for j in 0..n_samples {
825                    if i != j {
826                        let p_ij = p_matrix[[i, j]];
827                        let q_ij = q_matrix[[i, j]];
828
829                        let mut distance_sq = 0.0;
830                        for k in 0..n_components {
831                            let diff = embedding[[i, k]] - embedding[[j, k]];
832                            distance_sq += diff * diff;
833                        }
834
835                        let factor = (p_ij - q_ij) * (embedding[[i, d]] - embedding[[j, d]])
836                            / (1.0 + distance_sq);
837                        grad += 4.0 * factor;
838                    }
839                }
840
841                gradient[[i, d]] = grad;
842            }
843        }
844
845        Ok(gradient)
846    }
847
848    /// Build fuzzy simplicial set for UMAP
849    fn build_fuzzy_simplicial_set(
850        &self,
851        x: &Array2<f64>,
852        neighbors: &Array2<usize>,
853    ) -> Result<Array2<f64>> {
854        let (n_samples, _) = x.dim();
855        let mut fuzzy_graph = Array2::zeros((n_samples, n_samples));
856
857        // Compute local connectivity
858        for i in 0..n_samples {
859            let rho = self.compute_distance(&x.row(i), &x.row(neighbors[[i, 0]]));
860
861            for &j in neighbors.row(i) {
862                if i != j {
863                    let distance = self.compute_distance(&x.row(i), &x.row(j));
864                    let sigma = self.estimate_sigma_umap(x, i, neighbors)?;
865
866                    let weight = if distance > rho {
867                        (-(distance - rho) / sigma).exp()
868                    } else {
869                        1.0
870                    };
871
872                    fuzzy_graph[[i, j]] = weight;
873                }
874            }
875        }
876
877        // Symmetrize the graph
878        let mut symmetric_graph = Array2::zeros((n_samples, n_samples));
879        for i in 0..n_samples {
880            for j in 0..n_samples {
881                let prob_ij = fuzzy_graph[[i, j]];
882                let prob_ji = fuzzy_graph[[j, i]];
883
884                // Combine probabilities: a + b - ab
885                symmetric_graph[[i, j]] = prob_ij + prob_ji - prob_ij * prob_ji;
886            }
887        }
888
889        Ok(symmetric_graph)
890    }
891
892    /// Estimate sigma for UMAP
893    fn estimate_sigma_umap(
894        &self,
895        x: &Array2<f64>,
896        i: usize,
897        neighbors: &Array2<usize>,
898    ) -> Result<f64> {
899        let mut distances = Vec::new();
900
901        for &j in neighbors.row(i) {
902            if i != j {
903                let distance = self.compute_distance(&x.row(i), &x.row(j));
904                distances.push(distance);
905            }
906        }
907
908        distances.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
909
910        // Use median distance as sigma estimate
911        if !distances.is_empty() {
912            Ok(distances[distances.len() / 2])
913        } else {
914            Ok(1.0)
915        }
916    }
917
918    /// Optimize UMAP embedding
919    fn optimize_umap_embedding(
920        &self,
921        embedding: &mut Array2<f64>,
922        fuzzy_graph: &Array2<f64>,
923    ) -> Result<usize> {
924        let (n_samples, _n_components) = embedding.dim();
925        // Initialize random number generator with optional seeding for reproducibility
926        let mut rng: StdRng = match self.random_state {
927            Some(seed) => StdRng::seed_from_u64(seed),
928            None => StdRng::from_rng(&mut make_rng()),
929        };
930
931        for iter in 0..self.max_iter {
932            // Sample edges from the fuzzy graph
933            for i in 0..n_samples {
934                for j in (i + 1)..n_samples {
935                    let weight = fuzzy_graph[[i, j]];
936
937                    if weight > rng.random::<f64>() {
938                        // Attractive force
939                        self.apply_umap_force(embedding, i, j, true)?;
940                    } else {
941                        // Repulsive force
942                        self.apply_umap_force(embedding, i, j, false)?;
943                    }
944                }
945            }
946
947            // Simple convergence check
948            if iter > 100 && iter % 50 == 0 {
949                // Could add more sophisticated convergence criteria
950            }
951        }
952
953        Ok(self.max_iter)
954    }
955
956    /// Apply UMAP force between two points
957    fn apply_umap_force(
958        &self,
959        embedding: &mut Array2<f64>,
960        i: usize,
961        j: usize,
962        attractive: bool,
963    ) -> Result<()> {
964        let n_components = embedding.ncols();
965
966        // Compute distance
967        let mut distance_sq = 0.0;
968        for d in 0..n_components {
969            let diff = embedding[[i, d]] - embedding[[j, d]];
970            distance_sq += diff * diff;
971        }
972
973        let distance = distance_sq.sqrt().max(1e-12);
974
975        // Compute force magnitude
976        let force_magnitude = if attractive {
977            // Attractive force
978            1.0 / (1.0 + self.spread * distance_sq)
979        } else {
980            // Repulsive force
981            self.spread / ((0.001 + distance_sq) * (1.0 + self.spread * distance_sq))
982        };
983
984        // Apply force
985        let learning_rate = self.learning_rate * 0.01; // Scale down for stability
986
987        for d in 0..n_components {
988            let diff = embedding[[i, d]] - embedding[[j, d]];
989            let force_component = force_magnitude * diff / distance;
990
991            if attractive {
992                embedding[[i, d]] -= learning_rate * force_component;
993                embedding[[j, d]] += learning_rate * force_component;
994            } else {
995                embedding[[i, d]] += learning_rate * force_component;
996                embedding[[j, d]] -= learning_rate * force_component;
997            }
998        }
999
1000        Ok(())
1001    }
1002
1003    /// Compute smallest eigenvectors (for LLE and Laplacian Eigenmaps)
1004    fn compute_smallest_eigenvectors(&self, matrix: &Array2<f64>) -> Result<Array2<f64>> {
1005        let n = matrix.nrows();
1006
1007        // Use scirs2-linalg for eigendecomposition
1008        let (eigenvalues, eigenvectors) = matrix.eigh(UPLO::Lower).map_err(|e| {
1009            SklearsError::NumericalError(format!("Eigendecomposition failed: {}", e))
1010        })?;
1011
1012        // eigh returns eigenvalues in ascending order
1013        // Collect eigenvalue-eigenvector pairs
1014        let mut eigen_pairs: Vec<(f64, usize)> = eigenvalues
1015            .iter()
1016            .enumerate()
1017            .map(|(i, &val)| (val, i))
1018            .collect();
1019
1020        // Sort by eigenvalue (already ascending from eigh, but sort for clarity)
1021        eigen_pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1022
1023        // Skip the first eigenvalue if it's close to zero (for Laplacian)
1024        let start_idx = if eigen_pairs[0].0.abs() < 1e-10 { 1 } else { 0 };
1025        let end_idx = (start_idx + self.n_components).min(n);
1026
1027        let mut result = Array2::zeros((n, self.n_components));
1028        for (result_col, i) in (start_idx..end_idx).enumerate() {
1029            if result_col < self.n_components {
1030                let eigen_idx = eigen_pairs[i].1;
1031                for row in 0..n {
1032                    result[[row, result_col]] = eigenvectors[[row, eigen_idx]];
1033                }
1034            }
1035        }
1036
1037        Ok(result)
1038    }
1039
1040    /// Compute largest eigenvectors (for MDS)
1041    fn compute_largest_eigenvectors(&self, matrix: &Array2<f64>) -> Result<Array2<f64>> {
1042        let n = matrix.nrows();
1043
1044        // Use scirs2-linalg for eigendecomposition
1045        let (eigenvalues, eigenvectors) = matrix.eigh(UPLO::Lower).map_err(|e| {
1046            SklearsError::NumericalError(format!("Eigendecomposition failed: {}", e))
1047        })?;
1048
1049        // eigh returns eigenvalues in ascending order, we need descending
1050        let mut eigen_pairs: Vec<(f64, usize)> = eigenvalues
1051            .iter()
1052            .enumerate()
1053            .map(|(i, &val)| (val, i))
1054            .collect();
1055
1056        // Sort in descending order for largest eigenvalues
1057        eigen_pairs.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
1058
1059        let mut result = Array2::zeros((n, self.n_components));
1060        for (result_col, i) in (0..self.n_components).enumerate() {
1061            if i < eigen_pairs.len() && eigen_pairs[i].0 > 0.0 {
1062                let eigen_idx = eigen_pairs[i].1;
1063                let sqrt_eigenval = eigen_pairs[i].0.sqrt();
1064
1065                for row in 0..n {
1066                    result[[row, result_col]] = eigenvectors[[row, eigen_idx]] * sqrt_eigenval;
1067                }
1068            }
1069        }
1070
1071        Ok(result)
1072    }
1073}
1074
1075impl ManifoldLearning<TrainedManifoldLearning> {
1076    /// Get the embedding
1077    pub fn embedding(&self) -> &Array2<f64> {
1078        &self.state.embedding
1079    }
1080
1081    /// Compute distance between two points using the trained metric
1082    fn compute_distance(
1083        &self,
1084        a: &scirs2_core::ndarray::ArrayView1<f64>,
1085        b: &scirs2_core::ndarray::ArrayView1<f64>,
1086    ) -> f64 {
1087        match self.state.metric {
1088            DistanceMetric::Euclidean => a
1089                .iter()
1090                .zip(b.iter())
1091                .map(|(&x, &y)| (x - y) * (x - y))
1092                .sum::<f64>()
1093                .sqrt(),
1094            DistanceMetric::Manhattan => a
1095                .iter()
1096                .zip(b.iter())
1097                .map(|(&x, &y)| (x - y).abs())
1098                .sum::<f64>(),
1099            DistanceMetric::Cosine => {
1100                let dot_product = a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum::<f64>();
1101                let norm_a = a.iter().map(|&x| x * x).sum::<f64>().sqrt();
1102                let norm_b = b.iter().map(|&x| x * x).sum::<f64>().sqrt();
1103
1104                if norm_a > 1e-12 && norm_b > 1e-12 {
1105                    1.0 - (dot_product / (norm_a * norm_b))
1106                } else {
1107                    0.0
1108                }
1109            }
1110        }
1111    }
1112
1113    /// LLE out-of-sample transformation
1114    fn lle_transform(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
1115        // Simplified out-of-sample extension using nearest neighbors
1116        let (n_new_samples, _) = x.dim();
1117        let mut transformed = Array2::zeros((n_new_samples, self.state.n_components));
1118
1119        for i in 0..n_new_samples {
1120            // Find nearest neighbors in training data
1121            let mut distances: Vec<(f64, usize)> = Vec::new();
1122
1123            for j in 0..self.state.training_data.nrows() {
1124                let distance = self.compute_distance(&x.row(i), &self.state.training_data.row(j));
1125                distances.push((distance, j));
1126            }
1127
1128            distances.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1129
1130            // Use weighted average of k nearest neighbors
1131            let k = self.n_neighbors.min(distances.len());
1132            let mut total_weight = 0.0;
1133
1134            for &(distance, neighbor_idx) in distances.iter().take(k) {
1135                let weight = if distance > 1e-12 {
1136                    1.0 / distance
1137                } else {
1138                    1e12
1139                };
1140                total_weight += weight;
1141
1142                for d in 0..self.state.n_components {
1143                    transformed[[i, d]] += weight * self.state.embedding[[neighbor_idx, d]];
1144                }
1145            }
1146
1147            // Normalize by total weight
1148            if total_weight > 1e-12 {
1149                for d in 0..self.state.n_components {
1150                    transformed[[i, d]] /= total_weight;
1151                }
1152            }
1153        }
1154
1155        Ok(transformed)
1156    }
1157
1158    /// Isomap out-of-sample transformation
1159    fn isomap_transform(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
1160        // Use same approach as LLE for simplicity
1161        self.lle_transform(x)
1162    }
1163
1164    /// Laplacian Eigenmaps out-of-sample transformation
1165    fn laplacian_transform(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
1166        // Use same approach as LLE for simplicity
1167        self.lle_transform(x)
1168    }
1169
1170    /// UMAP out-of-sample transformation
1171    fn umap_transform(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
1172        // Use same approach as LLE for simplicity
1173        self.lle_transform(x)
1174    }
1175}
1176
1177impl Default for ManifoldLearning<Untrained> {
1178    fn default() -> Self {
1179        Self::new(ManifoldAlgorithm::LLE, 2)
1180    }
1181}
1182
1183#[allow(non_snake_case)]
1184#[cfg(test)]
1185mod tests {
1186    use super::*;
1187    use scirs2_core::ndarray::array;
1188
1189    #[test]
1190    fn test_manifold_learning_creation() {
1191        let ml = ManifoldLearning::new(ManifoldAlgorithm::LLE, 2)
1192            .n_neighbors(10)
1193            .metric(DistanceMetric::Euclidean)
1194            .max_iter(100)
1195            .learning_rate(200.0)
1196            .perplexity(30.0)
1197            .random_state(42);
1198
1199        assert_eq!(ml.algorithm, ManifoldAlgorithm::LLE);
1200        assert_eq!(ml.n_components, 2);
1201        assert_eq!(ml.n_neighbors, 10);
1202        assert_eq!(ml.metric, DistanceMetric::Euclidean);
1203        assert_eq!(ml.max_iter, 100);
1204        assert_eq!(ml.learning_rate, 200.0);
1205        assert_eq!(ml.perplexity, 30.0);
1206        assert_eq!(ml.random_state, Some(42));
1207    }
1208
1209    #[test]
1210    fn test_manifold_learning_lle() {
1211        // Create a simple 3D dataset that lies on a 2D manifold
1212        let x = array![
1213            [1.0, 0.0, 0.0],
1214            [0.0, 1.0, 0.0],
1215            [0.0, 0.0, 1.0],
1216            [1.0, 1.0, 0.0],
1217            [1.0, 0.0, 1.0],
1218            [0.0, 1.0, 1.0],
1219        ];
1220
1221        let ml = ManifoldLearning::new(ManifoldAlgorithm::LLE, 2)
1222            .n_neighbors(3)
1223            .random_state(42);
1224
1225        let trained_ml = ml.fit(&x, &()).expect("model fitting should succeed");
1226
1227        assert_eq!(trained_ml.embedding().dim(), (6, 2));
1228        assert_eq!(trained_ml.state.algorithm, ManifoldAlgorithm::LLE);
1229
1230        // Test transformation
1231        let new_point = array![[0.5, 0.5, 0.0]];
1232        let transformed = trained_ml
1233            .transform(&new_point)
1234            .expect("transformation should succeed");
1235        assert_eq!(transformed.dim(), (1, 2));
1236    }
1237
1238    #[test]
1239    fn test_manifold_learning_different_metrics() {
1240        let x = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]];
1241
1242        let metrics = vec![
1243            DistanceMetric::Euclidean,
1244            DistanceMetric::Manhattan,
1245            DistanceMetric::Cosine,
1246        ];
1247
1248        for metric in metrics {
1249            let ml = ManifoldLearning::new(ManifoldAlgorithm::LLE, 1)
1250                .n_neighbors(2)
1251                .metric(metric)
1252                .random_state(42);
1253
1254            let trained_ml = ml.fit(&x, &()).expect("model fitting should succeed");
1255            assert_eq!(trained_ml.embedding().dim(), (4, 1));
1256        }
1257    }
1258
1259    #[test]
1260    fn test_manifold_learning_error_cases() {
1261        let x_small = array![[1.0, 2.0]]; // Only 1 sample
1262        let ml = ManifoldLearning::new(ManifoldAlgorithm::LLE, 2);
1263        let result = ml.fit(&x_small, &());
1264        assert!(result.is_err());
1265
1266        let x = array![[1.0, 2.0], [3.0, 4.0]];
1267        let ml_bad_neighbors = ManifoldLearning::new(ManifoldAlgorithm::LLE, 1).n_neighbors(5); // More neighbors than samples
1268        let result = ml_bad_neighbors.fit(&x, &());
1269        assert!(result.is_err());
1270    }
1271
1272    #[test]
1273    fn test_distance_computation() {
1274        let ml = ManifoldLearning::new(ManifoldAlgorithm::LLE, 2);
1275
1276        let a = array![1.0, 2.0, 3.0];
1277        let b = array![4.0, 5.0, 6.0];
1278
1279        let euclidean_dist = ml.compute_distance(&a.view(), &b.view());
1280        assert!((euclidean_dist - (3.0_f64 * 3.0_f64).sqrt() * 3.0_f64.sqrt()).abs() < 1e-10);
1281    }
1282
1283    #[test]
1284    fn test_knn_computation() {
1285        let x = array![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
1286
1287        let ml = ManifoldLearning::new(ManifoldAlgorithm::LLE, 2);
1288        let neighbors = ml.find_knn(&x, 2).expect("operation should succeed");
1289
1290        assert_eq!(neighbors.dim(), (4, 2));
1291
1292        // Check that each point doesn't include itself as a neighbor
1293        for i in 0..4 {
1294            for j in 0..2 {
1295                assert_ne!(neighbors[[i, j]], i);
1296            }
1297        }
1298    }
1299}