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