1use 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
21type ManifoldResult = Result<(Array2<f64>, Option<Array2<f64>>, Option<Array2<f64>>, usize)>;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
28pub enum ManifoldAlgorithm {
29 #[default]
31 LLE,
32 Isomap,
34 LaplacianEigenmaps,
36 TSNE,
38 UMAP,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
45pub enum DistanceMetric {
46 #[default]
48 Euclidean,
49 Manhattan,
51 Cosine,
53}
54
55#[derive(Debug, Clone)]
57pub struct ManifoldLearning<State = Untrained> {
58 pub algorithm: ManifoldAlgorithm,
60 pub n_components: usize,
62 pub n_neighbors: usize,
64 pub metric: DistanceMetric,
66 pub max_iter: usize,
68 pub learning_rate: f64,
70 pub perplexity: f64,
72 pub random_state: Option<u64>,
74 pub early_exaggeration: f64,
76 pub min_dist: f64,
78 pub spread: f64,
80
81 state: State,
83}
84
85#[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 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 pub fn n_neighbors(mut self, n_neighbors: usize) -> Self {
121 self.n_neighbors = n_neighbors;
122 self
123 }
124
125 pub fn metric(mut self, metric: DistanceMetric) -> Self {
127 self.metric = metric;
128 self
129 }
130
131 pub fn max_iter(mut self, max_iter: usize) -> Self {
133 self.max_iter = max_iter;
134 self
135 }
136
137 pub fn learning_rate(mut self, learning_rate: f64) -> Self {
139 self.learning_rate = learning_rate;
140 self
141 }
142
143 pub fn perplexity(mut self, perplexity: f64) -> Self {
145 self.perplexity = perplexity;
146 self
147 }
148
149 pub fn random_state(mut self, random_state: u64) -> Self {
151 self.random_state = Some(random_state);
152 self
153 }
154
155 pub fn early_exaggeration(mut self, early_exaggeration: f64) -> Self {
157 self.early_exaggeration = early_exaggeration;
158 self
159 }
160
161 pub fn min_dist(mut self, min_dist: f64) -> Self {
163 self.min_dist = min_dist;
164 self
165 }
166
167 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 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 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 fn locally_linear_embedding(&self, x: &Array2<f64>) -> ManifoldResult {
257 let (_n_samples, _) = x.dim();
258
259 let neighbors = self.find_knn(x, self.n_neighbors)?;
261
262 let weights = self.compute_lle_weights(x, &neighbors)?;
264
265 let embedding = self.compute_lle_embedding(&weights)?;
267
268 Ok((embedding, Some(weights), None, 1))
269 }
270
271 fn isomap(&self, x: &Array2<f64>) -> ManifoldResult {
273 let (_n_samples, _) = x.dim();
274
275 let neighbors = self.find_knn(x, self.n_neighbors)?;
277 let distance_matrix = self.compute_distance_matrix(x)?;
278
279 let geodesic_distances = self.compute_geodesic_distances(&distance_matrix, &neighbors)?;
281
282 let embedding = self.classical_mds(&geodesic_distances)?;
284
285 Ok((embedding, None, Some(geodesic_distances), 1))
286 }
287
288 fn laplacian_eigenmaps(&self, x: &Array2<f64>) -> ManifoldResult {
290 let (_n_samples, _) = x.dim();
291
292 let neighbors = self.find_knn(x, self.n_neighbors)?;
294
295 let weight_matrix = self.compute_laplacian_weights(x, &neighbors)?;
297
298 let embedding = self.solve_laplacian_eigenproblem(&weight_matrix)?;
300
301 Ok((embedding, Some(weight_matrix), None, 1))
302 }
303
304 fn tsne(&self, x: &Array2<f64>) -> ManifoldResult {
306 let (n_samples, _) = x.dim();
307
308 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 let p_matrix = self.compute_tsne_similarities(x)?;
316
317 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 let n_iter = self.optimize_tsne_embedding(&mut embedding, &p_matrix)?;
327
328 Ok((embedding, Some(p_matrix), None, n_iter))
329 }
330
331 fn umap(&self, x: &Array2<f64>) -> ManifoldResult {
333 let (n_samples, _) = x.dim();
334
335 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 let neighbors = self.find_knn(x, self.n_neighbors)?;
343 let fuzzy_graph = self.build_fuzzy_simplicial_set(x, &neighbors)?;
344
345 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 let n_iter = self.optimize_umap_embedding(&mut embedding, &fuzzy_graph)?;
355
356 Ok((embedding, Some(fuzzy_graph), None, n_iter))
357 }
358
359 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 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 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 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 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 for j in 0..k {
463 local_cov[[j, j]] += 1e-3;
464 }
465
466 let ones = Array1::ones(k);
468 let weights_local = self.solve_linear_system(&local_cov, &ones)?;
469
470 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 fn solve_linear_system(&self, a: &Array2<f64>, b: &Array1<f64>) -> Result<Array1<f64>> {
484 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 fn compute_lle_embedding(&self, weights: &Array2<f64>) -> Result<Array2<f64>> {
494 let n_samples = weights.nrows();
495
496 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 let eigenresult = self.compute_smallest_eigenvectors(&m_matrix)?;
503
504 Ok(eigenresult)
505 }
506
507 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 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 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 fn classical_mds(&self, distance_matrix: &Array2<f64>) -> Result<Array2<f64>> {
542 let n_samples = distance_matrix.nrows();
543
544 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 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 self.compute_largest_eigenvectors(&similarity)
572 }
573
574 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 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 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 fn solve_laplacian_eigenproblem(&self, weight_matrix: &Array2<f64>) -> Result<Array2<f64>> {
622 let n_samples = weight_matrix.nrows();
623
624 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 let laplacian = °ree_matrix - weight_matrix;
633
634 self.compute_smallest_eigenvectors(&laplacian)
636 }
637
638 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 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 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 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 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 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 for prob in &mut probabilities {
722 *prob /= sum_prob;
723 }
724
725 let entropy = probabilities
727 .iter()
728 .filter(|&&p| p > 1e-12)
729 .map(|&p| -p * p.ln())
730 .sum::<f64>();
731
732 2.0_f64.powf(entropy)
734 }
735
736 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 let q_matrix = self.compute_tsne_q_matrix(embedding)?;
749
750 let gradient = self.compute_tsne_gradient(embedding, p_matrix, &q_matrix)?;
752
753 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 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 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 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 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 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 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 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 symmetric_graph[[i, j]] = prob_ij + prob_ji - prob_ij * prob_ji;
886 }
887 }
888
889 Ok(symmetric_graph)
890 }
891
892 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 if !distances.is_empty() {
912 Ok(distances[distances.len() / 2])
913 } else {
914 Ok(1.0)
915 }
916 }
917
918 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 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 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 self.apply_umap_force(embedding, i, j, true)?;
940 } else {
941 self.apply_umap_force(embedding, i, j, false)?;
943 }
944 }
945 }
946
947 if iter > 100 && iter % 50 == 0 {
949 }
951 }
952
953 Ok(self.max_iter)
954 }
955
956 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 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 let force_magnitude = if attractive {
977 1.0 / (1.0 + self.spread * distance_sq)
979 } else {
980 self.spread / ((0.001 + distance_sq) * (1.0 + self.spread * distance_sq))
982 };
983
984 let learning_rate = self.learning_rate * 0.01; 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 fn compute_smallest_eigenvectors(&self, matrix: &Array2<f64>) -> Result<Array2<f64>> {
1005 let n = matrix.nrows();
1006
1007 let (eigenvalues, eigenvectors) = matrix.eigh(UPLO::Lower).map_err(|e| {
1009 SklearsError::NumericalError(format!("Eigendecomposition failed: {}", e))
1010 })?;
1011
1012 let mut eigen_pairs: Vec<(f64, usize)> = eigenvalues
1015 .iter()
1016 .enumerate()
1017 .map(|(i, &val)| (val, i))
1018 .collect();
1019
1020 eigen_pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1022
1023 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 fn compute_largest_eigenvectors(&self, matrix: &Array2<f64>) -> Result<Array2<f64>> {
1042 let n = matrix.nrows();
1043
1044 let (eigenvalues, eigenvectors) = matrix.eigh(UPLO::Lower).map_err(|e| {
1046 SklearsError::NumericalError(format!("Eigendecomposition failed: {}", e))
1047 })?;
1048
1049 let mut eigen_pairs: Vec<(f64, usize)> = eigenvalues
1051 .iter()
1052 .enumerate()
1053 .map(|(i, &val)| (val, i))
1054 .collect();
1055
1056 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 pub fn embedding(&self) -> &Array2<f64> {
1078 &self.state.embedding
1079 }
1080
1081 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 fn lle_transform(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
1115 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 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 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 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 fn isomap_transform(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
1160 self.lle_transform(x)
1162 }
1163
1164 fn laplacian_transform(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
1166 self.lle_transform(x)
1168 }
1169
1170 fn umap_transform(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
1172 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 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 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]]; 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); 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 for i in 0..4 {
1294 for j in 0..2 {
1295 assert_ne!(neighbors[[i, j]], i);
1296 }
1297 }
1298 }
1299}