1use crate::core::{Graph, IgraphError, IgraphResult};
12
13const FORCE_LIMIT: f64 = 4.0;
14const MIN_DISTANCE_ATTRACTION: f64 = 0.0001;
15const CORRECT_DISTANCE_REPULSION: f64 = 0.01;
16
17#[derive(Debug, Clone)]
19pub struct UmapParams {
20 pub min_dist: f64,
23 pub epochs: u32,
25 pub distances_are_weights: bool,
28}
29
30impl Default for UmapParams {
31 fn default() -> Self {
32 Self {
33 min_dist: 0.01,
34 epochs: 500,
35 distances_are_weights: false,
36 }
37 }
38}
39
40pub fn umap_compute_weights(graph: &Graph, distances: Option<&[f64]>) -> IgraphResult<Vec<f64>> {
66 let n = graph.vcount() as usize;
67 let m = graph.ecount();
68
69 if let Some(d) = distances {
70 if d.len() != m {
71 return Err(IgraphError::InvalidArgument(
72 "distances length must equal edge count".into(),
73 ));
74 }
75 if m > 0 {
76 let min_d = d.iter().copied().fold(f64::INFINITY, f64::min);
77 if min_d < 0.0 {
78 return Err(IgraphError::InvalidArgument(
79 "distances must not be negative".into(),
80 ));
81 }
82 if min_d.is_nan() {
83 return Err(IgraphError::InvalidArgument(
84 "distances must not be NaN".into(),
85 ));
86 }
87 }
88 }
89
90 let mut out_edges: Vec<Vec<(usize, usize)>> = vec![Vec::new(); n];
92 for eid in 0..m {
93 let (src, tgt) = graph.edge(eid as u32)?;
94 out_edges[src as usize].push((tgt as usize, eid));
95 }
96
97 let mut neighbors_seen: Vec<Vec<usize>> = vec![Vec::new(); n];
99 let mut weights_seen: Vec<Vec<f64>> = vec![Vec::new(); n];
100
101 for i in 0..n {
102 let neis = &out_edges[i];
103 if neis.is_empty() {
104 continue;
105 }
106
107 let (rho, dist_max) = if let Some(dists) = distances {
109 let mut rho = f64::INFINITY;
110 let mut dmax = f64::NEG_INFINITY;
111 for &(_nb, eid) in neis {
112 let d = dists[eid];
113 if d < rho {
114 rho = d;
115 }
116 if d > dmax {
117 dmax = d;
118 }
119 }
120 (rho, dmax)
121 } else {
122 (0.0, 0.0)
123 };
124
125 let sigma = if dist_max == rho {
127 -1.0 } else {
129 let target = (neis.len() as f64).log2();
130 let dists = distances.ok_or(IgraphError::Internal(
131 "distances must be Some when dist_max != rho",
132 ))?;
133 find_sigma(dists, neis, rho, target)
134 };
135
136 for &(nb, eid) in neis {
138 let weight = if sigma < 0.0 {
139 1.0
140 } else {
141 let dists = distances.ok_or(IgraphError::Internal(
142 "distances must be Some when sigma >= 0",
143 ))?;
144 let d = dists[eid];
145 (-(d - rho) / sigma).exp()
146 };
147
148 if nb == i {
150 return Err(IgraphError::InvalidArgument(
151 "input graph must contain no self-loops".into(),
152 ));
153 }
154
155 neighbors_seen[i].push(nb);
156 weights_seen[i].push(weight);
157 }
158 }
159
160 let mut result = vec![0.0_f64; m];
162
163 for eid in 0..m {
164 let (src, tgt) = graph.edge(eid as u32)?;
165 let i = src as usize;
166 let k = tgt as usize;
167
168 let mut weight = 0.0_f64;
170 let mut found_idx = None;
171 for (l, &nb) in neighbors_seen[i].iter().enumerate() {
172 if nb == k {
173 weight = weights_seen[i][l];
174 found_idx = Some(l);
175 break;
176 }
177 }
178
179 if weight < 0.0 {
181 result[eid] = 0.0;
182 continue;
183 }
184
185 if let Some(idx) = found_idx {
187 weights_seen[i][idx] = -1.0;
188 }
189
190 let mut weight_inv = 0.0_f64;
192 let mut found_inv_idx = None;
193 for (l, &nb) in neighbors_seen[k].iter().enumerate() {
194 if nb == i {
195 weight_inv = weights_seen[k][l];
196 found_inv_idx = Some(l);
197 break;
198 }
199 }
200
201 if weight_inv < 0.0 {
202 result[eid] = 0.0;
203 continue;
204 }
205
206 if let Some(idx) = found_inv_idx {
207 weights_seen[k][idx] = -1.0;
208 }
209
210 result[eid] = weight + weight_inv - weight * weight_inv;
212 }
213
214 Ok(result)
215}
216
217fn find_sigma(distances: &[f64], neis: &[(usize, usize)], rho: f64, target: f64) -> f64 {
219 let mut sigma = 1.0_f64;
220 let tol = 0.01;
221 let maxiter = 100;
222 let mut step = sigma;
223 let mut seen_max = false;
224
225 for iter in 0..maxiter {
226 let sum: f64 = neis
227 .iter()
228 .map(|&(_nb, eid)| (-(distances[eid] - rho) / sigma).exp())
229 .sum();
230
231 if sum < target {
232 if seen_max {
233 step /= 2.0;
234 } else if iter > 0 {
235 step *= 2.0;
236 }
237 sigma += step;
238 } else {
239 seen_max = true;
240 step /= 2.0;
241 sigma -= step;
242 }
243
244 if (sum - target).abs() < tol {
245 break;
246 }
247 }
248
249 sigma
250}
251
252fn fit_ab(min_dist: f64) -> (f64, f64) {
257 let nr_points = 300;
258 let end_point = 3.0_f64;
259 let mut a = 1.8_f64;
260 let mut b = 0.8_f64;
261 let tol = 0.001;
262 let maxiter = 100;
263
264 let x: Vec<f64> = (0..nr_points)
266 .map(|i| (end_point / nr_points as f64) * i as f64 + 0.001)
267 .collect();
268
269 let mut residuals = vec![0.0_f64; nr_points];
270 let mut powb = vec![0.0_f64; nr_points];
271 let mut squared_sum_res;
272 let mut squared_sum_res_old = f64::INFINITY;
273
274 for iter in 0..maxiter {
275 squared_sum_res = 0.0;
277 for i in 0..nr_points {
278 powb[i] = x[i].powf(2.0 * b);
279 let q = 1.0 / (1.0 + a * powb[i]);
280 let p = if x[i] <= min_dist {
281 1.0
282 } else {
283 (-(x[i] - min_dist)).exp()
284 };
285 residuals[i] = q - p;
286 squared_sum_res += residuals[i] * residuals[i];
287 }
288
289 if squared_sum_res < tol * tol {
290 break;
291 }
292 if iter > 0 && (squared_sum_res_old.sqrt() - squared_sum_res.sqrt()).abs() < tol {
293 break;
294 }
295
296 let mut j_a = vec![0.0_f64; nr_points];
298 let mut j_b = vec![0.0_f64; nr_points];
299 for i in 0..nr_points {
300 let tmp = 1.0 + a * powb[i];
301 j_a[i] = -2.0 * powb[i] / (tmp * tmp);
302 j_b[i] = j_a[i] * a * x[i].ln() * 2.0;
303 }
304
305 let mut jtj = [[0.0_f64; 2]; 2];
307 let mut jtr = [0.0_f64; 2];
308 for i in 0..nr_points {
309 let jrow = [j_a[i], j_b[i]];
310 for j1 in 0..2 {
311 for j2 in 0..2 {
312 jtj[j1][j2] += jrow[j1] * jrow[j2];
313 }
314 jtr[j1] += jrow[j1] * residuals[i];
315 }
316 }
317
318 let det = jtj[0][0] * jtj[1][1] - jtj[0][1] * jtj[1][0];
320 if det.abs() < 1e-30 {
321 break;
322 }
323 let da = -(jtj[1][1] * jtr[0] - jtj[0][1] * jtr[1]) / det;
324 let db = -(jtj[0][0] * jtr[1] - jtj[1][0] * jtr[0]) / det;
325
326 let mut da_step = da;
328 let mut db_step = db;
329 squared_sum_res_old = squared_sum_res;
330
331 let mut best_ssr = compute_ssr(&x, a + da_step, b + db_step, min_dist);
332
333 for _k in 0..30 {
334 da_step /= 2.0;
335 db_step /= 2.0;
336 let new_ssr = compute_ssr(&x, a + da_step, b + db_step, min_dist);
337 if new_ssr > best_ssr - tol {
338 da_step *= 2.0;
339 db_step *= 2.0;
340 break;
341 }
342 best_ssr = new_ssr;
343 }
344
345 a += da_step;
346 b += db_step;
347 }
348
349 (a, b)
350}
351
352fn compute_ssr(x: &[f64], a: f64, b: f64, min_dist: f64) -> f64 {
353 let mut ssr = 0.0;
354 for &xi in x {
355 let q = 1.0 / (1.0 + a * xi.powf(2.0 * b));
356 let p = if xi <= min_dist {
357 1.0
358 } else {
359 (-(xi - min_dist)).exp()
360 };
361 let r = q - p;
362 ssr += r * r;
363 }
364 ssr
365}
366
367fn clip_force(force: f64) -> f64 {
368 force.clamp(-FORCE_LIMIT, FORCE_LIMIT)
369}
370
371fn attract(dsq: f64, a: f64, b: f64) -> f64 {
372 -(2.0 * a * b * dsq.powf(b - 1.0)) / (1.0 + a * dsq.powf(b))
373}
374
375fn repel(dsq: f64, a: f64, b: f64) -> f64 {
376 let dsq_min = CORRECT_DISTANCE_REPULSION * CORRECT_DISTANCE_REPULSION;
377 (2.0 * b) / (dsq_min + dsq) / (1.0 + a * dsq.powf(b))
378}
379
380pub fn layout_umap(
418 graph: &Graph,
419 seed: Option<&[[f64; 2]]>,
420 distances: Option<&[f64]>,
421 params: &UmapParams,
422) -> IgraphResult<Vec<[f64; 2]>> {
423 let n = graph.vcount() as usize;
424
425 if params.min_dist < 0.0 {
426 return Err(IgraphError::InvalidArgument(
427 "min_dist must not be negative".into(),
428 ));
429 }
430
431 if n == 0 {
432 return Ok(Vec::new());
433 }
434 if n == 1 {
435 if let Some(s) = seed {
436 if s.len() != 1 {
437 return Err(IgraphError::InvalidArgument(
438 "seed must have exactly vcount positions".into(),
439 ));
440 }
441 return Ok(s.to_vec());
442 }
443 return Ok(vec![[0.0, 0.0]]);
444 }
445
446 let weights = if params.distances_are_weights {
448 distances
449 .ok_or_else(|| {
450 IgraphError::InvalidArgument(
451 "distances_are_weights=true but no distances provided".into(),
452 )
453 })?
454 .to_vec()
455 } else {
456 umap_compute_weights(graph, distances)?
457 };
458
459 let mut pos: Vec<[f64; 2]> = if let Some(s) = seed {
461 if s.len() != n {
462 return Err(IgraphError::InvalidArgument(format!(
463 "seed length {} != vcount {}",
464 s.len(),
465 n
466 )));
467 }
468 s.to_vec()
469 } else {
470 let mut rng = SplitMix64::new(42);
471 (0..n)
472 .map(|_| [rng.next_uniform(), rng.next_uniform()])
473 .collect()
474 };
475
476 let (a, b) = fit_ab(params.min_dist);
478
479 let m = graph.ecount();
481 let mut edges: Vec<(usize, usize)> = Vec::with_capacity(m);
482 for eid in 0..m as u32 {
483 let (s, t) = graph.edge(eid)?;
484 edges.push((s as usize, t as usize));
485 }
486
487 let avoid_neighbor_repulsion = n < 100;
489 let adj: Vec<Vec<usize>> = if avoid_neighbor_repulsion {
490 let mut a = vec![Vec::new(); n];
491 for &(s, t) in &edges {
492 a[s].push(t);
493 if s != t {
494 a[t].push(s);
495 }
496 }
497 a
498 } else {
499 Vec::new()
500 };
501
502 let negative_sampling_rate = 5usize.min(n - 1);
503
504 let mut next_epoch: Vec<f64> = vec![0.0; m];
506
507 let mut rng = SplitMix64::new(123);
508
509 for epoch in 0..params.epochs {
511 let learning_rate = 1.0 - (epoch as f64 + 1.0) / params.epochs as f64;
512
513 for eid in 0..m {
514 if weights[eid] <= 0.0 {
515 continue;
516 }
517 if next_epoch[eid] - epoch as f64 >= 1.0 {
518 continue;
519 }
520
521 next_epoch[eid] += 1.0 / weights[eid];
522
523 let (from_v, to_v) = edges[eid];
524
525 for swap in 0..2u8 {
527 let (from, to) = if swap == 0 {
528 (from_v, to_v)
529 } else {
530 (to_v, from_v)
531 };
532
533 let dx = pos[from][0] - pos[to][0];
535 let dy = pos[from][1] - pos[to][1];
536 let dsq = dx * dx + dy * dy;
537
538 if dsq >= MIN_DISTANCE_ATTRACTION * MIN_DISTANCE_ATTRACTION {
539 let force = attract(dsq, a, b);
540 let fx = clip_force(force * dx);
541 let fy = clip_force(force * dy);
542 pos[from][0] += learning_rate * fx;
543 pos[from][1] += learning_rate * fy;
544 }
545
546 for _j in 0..negative_sampling_rate {
548 let mut neg = rng.next_usize(n - 1);
549 if neg >= from {
550 neg += 1;
551 }
552
553 if avoid_neighbor_repulsion && adj[from].contains(&neg) {
555 continue;
556 }
557
558 let dx = pos[from][0] - pos[neg][0];
559 let dy = pos[from][1] - pos[neg][1];
560 let dsq = dx * dx + dy * dy;
561
562 let force = repel(dsq, a, b);
563 let fx = clip_force(force * dx);
564 let fy = clip_force(force * dy);
565 pos[from][0] += learning_rate * fx;
566 pos[from][1] += learning_rate * fy;
567 }
568 }
569 }
570 }
571
572 let mut cx = 0.0_f64;
574 let mut cy = 0.0_f64;
575 for p in &pos {
576 cx += p[0];
577 cy += p[1];
578 }
579 cx /= n as f64;
580 cy /= n as f64;
581 for p in &mut pos {
582 p[0] -= cx;
583 p[1] -= cy;
584 }
585
586 Ok(pos)
587}
588
589pub fn layout_umap_3d(
614 graph: &Graph,
615 seed: Option<&[[f64; 3]]>,
616 distances: Option<&[f64]>,
617 params: &UmapParams,
618) -> IgraphResult<Vec<[f64; 3]>> {
619 let n = graph.vcount() as usize;
620
621 if params.min_dist < 0.0 {
622 return Err(IgraphError::InvalidArgument(
623 "min_dist must not be negative".into(),
624 ));
625 }
626
627 if n == 0 {
628 return Ok(Vec::new());
629 }
630 if n == 1 {
631 if let Some(s) = seed {
632 if s.len() != 1 {
633 return Err(IgraphError::InvalidArgument(
634 "seed must have exactly vcount positions".into(),
635 ));
636 }
637 return Ok(s.to_vec());
638 }
639 return Ok(vec![[0.0, 0.0, 0.0]]);
640 }
641
642 let weights = if params.distances_are_weights {
643 distances
644 .ok_or_else(|| {
645 IgraphError::InvalidArgument(
646 "distances_are_weights=true but no distances provided".into(),
647 )
648 })?
649 .to_vec()
650 } else {
651 umap_compute_weights(graph, distances)?
652 };
653
654 let mut pos: Vec<[f64; 3]> = if let Some(s) = seed {
655 if s.len() != n {
656 return Err(IgraphError::InvalidArgument(format!(
657 "seed length {} != vcount {}",
658 s.len(),
659 n
660 )));
661 }
662 s.to_vec()
663 } else {
664 let mut rng = SplitMix64::new(42);
665 (0..n)
666 .map(|_| [rng.next_uniform(), rng.next_uniform(), rng.next_uniform()])
667 .collect()
668 };
669
670 let (a, b) = fit_ab(params.min_dist);
671
672 let m = graph.ecount();
673 let mut edges: Vec<(usize, usize)> = Vec::with_capacity(m);
674 for eid in 0..m as u32 {
675 let (s, t) = graph.edge(eid)?;
676 edges.push((s as usize, t as usize));
677 }
678
679 let avoid_neighbor_repulsion = n < 100;
680 let adj: Vec<Vec<usize>> = if avoid_neighbor_repulsion {
681 let mut a = vec![Vec::new(); n];
682 for &(s, t) in &edges {
683 a[s].push(t);
684 if s != t {
685 a[t].push(s);
686 }
687 }
688 a
689 } else {
690 Vec::new()
691 };
692
693 let negative_sampling_rate = 5usize.min(n - 1);
694 let mut next_epoch: Vec<f64> = vec![0.0; m];
695 let mut rng = SplitMix64::new(123);
696
697 for epoch in 0..params.epochs {
698 let learning_rate = 1.0 - (epoch as f64 + 1.0) / params.epochs as f64;
699
700 for eid in 0..m {
701 if weights[eid] <= 0.0 {
702 continue;
703 }
704 if next_epoch[eid] - epoch as f64 >= 1.0 {
705 continue;
706 }
707 next_epoch[eid] += 1.0 / weights[eid];
708
709 let (from_v, to_v) = edges[eid];
710
711 for swap in 0..2u8 {
712 let (from, to) = if swap == 0 {
713 (from_v, to_v)
714 } else {
715 (to_v, from_v)
716 };
717
718 let dx = pos[from][0] - pos[to][0];
719 let dy = pos[from][1] - pos[to][1];
720 let dz = pos[from][2] - pos[to][2];
721 let dsq = dx * dx + dy * dy + dz * dz;
722
723 if dsq >= MIN_DISTANCE_ATTRACTION * MIN_DISTANCE_ATTRACTION {
724 let force = attract(dsq, a, b);
725 let fx = clip_force(force * dx);
726 let fy = clip_force(force * dy);
727 let fz = clip_force(force * dz);
728 pos[from][0] += learning_rate * fx;
729 pos[from][1] += learning_rate * fy;
730 pos[from][2] += learning_rate * fz;
731 }
732
733 for _j in 0..negative_sampling_rate {
734 let mut neg = rng.next_usize(n - 1);
735 if neg >= from {
736 neg += 1;
737 }
738
739 if avoid_neighbor_repulsion && adj[from].contains(&neg) {
740 continue;
741 }
742
743 let dx = pos[from][0] - pos[neg][0];
744 let dy = pos[from][1] - pos[neg][1];
745 let dz = pos[from][2] - pos[neg][2];
746 let dsq = dx * dx + dy * dy + dz * dz;
747
748 let force = repel(dsq, a, b);
749 let fx = clip_force(force * dx);
750 let fy = clip_force(force * dy);
751 let fz = clip_force(force * dz);
752 pos[from][0] += learning_rate * fx;
753 pos[from][1] += learning_rate * fy;
754 pos[from][2] += learning_rate * fz;
755 }
756 }
757 }
758 }
759
760 let mut cx = 0.0_f64;
762 let mut cy = 0.0_f64;
763 let mut cz = 0.0_f64;
764 for p in &pos {
765 cx += p[0];
766 cy += p[1];
767 cz += p[2];
768 }
769 cx /= n as f64;
770 cy /= n as f64;
771 cz /= n as f64;
772 for p in &mut pos {
773 p[0] -= cx;
774 p[1] -= cy;
775 p[2] -= cz;
776 }
777
778 Ok(pos)
779}
780
781struct SplitMix64 {
786 state: u64,
787}
788
789impl SplitMix64 {
790 fn new(seed: u64) -> Self {
791 Self { state: seed }
792 }
793
794 fn next_u64(&mut self) -> u64 {
795 self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
796 let mut z = self.state;
797 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
798 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
799 z ^ (z >> 31)
800 }
801
802 fn next_uniform(&mut self) -> f64 {
803 (self.next_u64() >> 11) as f64 / ((1u64 << 53) as f64)
804 }
805
806 fn next_usize(&mut self, max: usize) -> usize {
807 (self.next_u64() as usize) % max
808 }
809}
810
811#[cfg(test)]
816mod tests {
817 use super::*;
818
819 #[test]
820 fn test_umap_empty() {
821 let g = Graph::with_vertices(0);
822 let params = UmapParams::default();
823 let pos = layout_umap(&g, None, None, ¶ms).unwrap();
824 assert!(pos.is_empty());
825 }
826
827 #[test]
828 fn test_umap_single() {
829 let g = Graph::with_vertices(1);
830 let params = UmapParams::default();
831 let pos = layout_umap(&g, None, None, ¶ms).unwrap();
832 assert_eq!(pos.len(), 1);
833 assert!(pos[0][0].abs() < 1e-10 && pos[0][1].abs() < 1e-10);
834 }
835
836 #[test]
837 fn test_umap_path() {
838 let mut g = Graph::with_vertices(5);
839 for i in 0..4 {
840 g.add_edge(i, i + 1).unwrap();
841 }
842 let params = UmapParams {
843 epochs: 30,
844 ..UmapParams::default()
845 };
846 let pos = layout_umap(&g, None, None, ¶ms).unwrap();
847 assert_eq!(pos.len(), 5);
848 for p in &pos {
849 assert!(p[0].is_finite() && p[1].is_finite());
850 }
851 }
852
853 #[test]
854 fn test_umap_cycle() {
855 let mut g = Graph::with_vertices(6);
856 for i in 0..6 {
857 g.add_edge(i, (i + 1) % 6).unwrap();
858 }
859 let params = UmapParams {
860 epochs: 30,
861 ..UmapParams::default()
862 };
863 let pos = layout_umap(&g, None, None, ¶ms).unwrap();
864 assert_eq!(pos.len(), 6);
865 for p in &pos {
866 assert!(p[0].is_finite() && p[1].is_finite());
867 }
868 }
869
870 #[test]
871 fn test_umap_with_distances() {
872 let mut g = Graph::with_vertices(4);
873 g.add_edge(0, 1).unwrap();
874 g.add_edge(1, 2).unwrap();
875 g.add_edge(2, 3).unwrap();
876 let distances = vec![1.0, 2.0, 0.5];
877 let params = UmapParams {
878 epochs: 30,
879 ..UmapParams::default()
880 };
881 let pos = layout_umap(&g, None, Some(&distances), ¶ms).unwrap();
882 assert_eq!(pos.len(), 4);
883 for p in &pos {
884 assert!(p[0].is_finite() && p[1].is_finite());
885 }
886 }
887
888 #[test]
889 fn test_umap_with_seed() {
890 let mut g = Graph::with_vertices(3);
891 g.add_edge(0, 1).unwrap();
892 g.add_edge(1, 2).unwrap();
893 let seed = vec![[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
894 let params = UmapParams {
895 epochs: 30,
896 ..UmapParams::default()
897 };
898 let pos = layout_umap(&g, Some(&seed), None, ¶ms).unwrap();
899 assert_eq!(pos.len(), 3);
900 for p in &pos {
901 assert!(p[0].is_finite() && p[1].is_finite());
902 }
903 }
904
905 #[test]
906 fn test_umap_distances_are_weights() {
907 let mut g = Graph::with_vertices(4);
908 g.add_edge(0, 1).unwrap();
909 g.add_edge(1, 2).unwrap();
910 g.add_edge(2, 3).unwrap();
911 let weights = vec![0.9, 0.5, 0.8];
912 let params = UmapParams {
913 epochs: 30,
914 distances_are_weights: true,
915 ..UmapParams::default()
916 };
917 let pos = layout_umap(&g, None, Some(&weights), ¶ms).unwrap();
918 assert_eq!(pos.len(), 4);
919 for p in &pos {
920 assert!(p[0].is_finite() && p[1].is_finite());
921 }
922 }
923
924 #[test]
925 fn test_umap_3d() {
926 let mut g = Graph::with_vertices(5);
927 for i in 0..4 {
928 g.add_edge(i, i + 1).unwrap();
929 }
930 let params = UmapParams {
931 epochs: 30,
932 ..UmapParams::default()
933 };
934 let pos = layout_umap_3d(&g, None, None, ¶ms).unwrap();
935 assert_eq!(pos.len(), 5);
936 for p in &pos {
937 assert!(p[0].is_finite() && p[1].is_finite() && p[2].is_finite());
938 }
939 }
940
941 #[test]
942 fn test_umap_negative_distances_error() {
943 let mut g = Graph::with_vertices(3);
944 g.add_edge(0, 1).unwrap();
945 g.add_edge(1, 2).unwrap();
946 let distances = vec![1.0, -0.5];
947 let params = UmapParams::default();
948 assert!(layout_umap(&g, None, Some(&distances), ¶ms).is_err());
949 }
950
951 #[test]
952 fn test_umap_wrong_distances_length() {
953 let mut g = Graph::with_vertices(3);
954 g.add_edge(0, 1).unwrap();
955 g.add_edge(1, 2).unwrap();
956 let distances = vec![1.0]; let params = UmapParams::default();
958 assert!(layout_umap(&g, None, Some(&distances), ¶ms).is_err());
959 }
960
961 #[test]
962 fn test_umap_wrong_seed_length() {
963 let mut g = Graph::with_vertices(3);
964 g.add_edge(0, 1).unwrap();
965 g.add_edge(1, 2).unwrap();
966 let seed = vec![[0.0, 0.0], [1.0, 0.0]]; let params = UmapParams {
968 epochs: 10,
969 ..UmapParams::default()
970 };
971 assert!(layout_umap(&g, Some(&seed), None, ¶ms).is_err());
972 }
973
974 #[test]
975 fn test_umap_complete() {
976 let mut g = Graph::with_vertices(5);
977 for i in 0..5u32 {
978 for j in (i + 1)..5 {
979 g.add_edge(i, j).unwrap();
980 }
981 }
982 let params = UmapParams {
983 epochs: 30,
984 ..UmapParams::default()
985 };
986 let pos = layout_umap(&g, None, None, ¶ms).unwrap();
987 assert_eq!(pos.len(), 5);
988 for p in &pos {
989 assert!(p[0].is_finite() && p[1].is_finite());
990 }
991 }
992
993 #[test]
994 fn test_umap_deterministic() {
995 let mut g = Graph::with_vertices(4);
996 g.add_edge(0, 1).unwrap();
997 g.add_edge(1, 2).unwrap();
998 g.add_edge(2, 3).unwrap();
999 g.add_edge(3, 0).unwrap();
1000 let params = UmapParams {
1001 epochs: 30,
1002 ..UmapParams::default()
1003 };
1004 let pos1 = layout_umap(&g, None, None, ¶ms).unwrap();
1005 let pos2 = layout_umap(&g, None, None, ¶ms).unwrap();
1006 for i in 0..4 {
1007 assert!((pos1[i][0] - pos2[i][0]).abs() < 1e-10);
1008 assert!((pos1[i][1] - pos2[i][1]).abs() < 1e-10);
1009 }
1010 }
1011
1012 #[test]
1013 fn test_compute_weights_basic() {
1014 let mut g = Graph::with_vertices(4);
1015 g.add_edge(0, 1).unwrap();
1016 g.add_edge(1, 2).unwrap();
1017 g.add_edge(2, 3).unwrap();
1018 let distances = vec![1.0, 2.0, 0.5];
1019 let w = umap_compute_weights(&g, Some(&distances)).unwrap();
1020 assert_eq!(w.len(), 3);
1021 for &wi in &w {
1022 assert!((0.0..=1.0).contains(&wi));
1023 }
1024 }
1025
1026 #[test]
1027 fn test_compute_weights_no_distances() {
1028 let mut g = Graph::with_vertices(3);
1029 g.add_edge(0, 1).unwrap();
1030 g.add_edge(1, 2).unwrap();
1031 let w = umap_compute_weights(&g, None).unwrap();
1032 assert_eq!(w.len(), 2);
1033 for &wi in &w {
1035 assert!((wi - 1.0).abs() < 1e-10);
1036 }
1037 }
1038
1039 #[test]
1040 fn test_fit_ab() {
1041 let (a, b) = fit_ab(0.1);
1042 assert!(a > 0.0 && a < 100.0);
1043 assert!(b > 0.0 && b < 5.0);
1044 }
1045}