1use num_traits::Float;
9use rayon::prelude::*;
10use std::iter::Sum;
11use std::time::Instant;
12
13use crate::graph::graph_structures::UnionFind;
14use crate::prelude::*;
15use bixverse_core::core::math::sparse::{CompressedSparseData2, CompressedSparseFormat};
16
17const MIN_COMPONENT_SIZE: usize = 10;
24
25const SNN_ROW_CHUNK: usize = 2048;
29
30const SMOOTH_K_TOLERANCE: f64 = 1e-5;
33
34const MIN_K_DIST_SCALE: f64 = 1e-3;
37
38const SMOOTH_K_ITERS: usize = 64;
40
41pub const SNN_PRUNE_DEFAULT: f64 = 1.0 / 16.0;
47
48const SNN_RANK_FLOOR: f64 = 1e-6;
50
51#[derive(Clone, Copy, Debug, Default, PartialEq)]
57pub enum SnnSimilarity {
58 #[default]
60 Jaccard,
61 Rank,
64}
65
66#[derive(Clone, Copy, Debug, PartialEq)]
68pub enum KnnSymmetrisation {
69 Knn {
72 binary: bool,
74 },
75 Snn {
78 similarity: SnnSimilarity,
80 prune: f64,
82 rescue: bool,
85 },
86 Umap {
88 local_connectivity: f64,
91 mix_ratio: f64,
94 },
95}
96
97impl Default for KnnSymmetrisation {
100 fn default() -> Self {
101 KnnSymmetrisation::Snn {
102 similarity: SnnSimilarity::Jaccard,
103 prune: SNN_PRUNE_DEFAULT,
104 rescue: true,
105 }
106 }
107}
108
109pub fn parse_snn_similarity(s: &str) -> Option<SnnSimilarity> {
119 match s.to_lowercase().as_str() {
120 "jaccard" => Some(SnnSimilarity::Jaccard),
121 "rank" => Some(SnnSimilarity::Rank),
122 _ => None,
123 }
124}
125
126#[inline(always)]
140fn t<T: BixverseFloat>(x: f64) -> T {
141 T::from_f64(x).expect("f64 fits every float type")
142}
143
144fn check_knn(idx: &[u32], n_dist: Option<usize>, k: usize, n: usize) -> Result<(), MethodsErrors> {
157 if idx.len() != n * k {
158 return Err(MethodsErrors::GraphLengthMismatch {
159 name: "idx",
160 expected: n * k,
161 got: idx.len(),
162 });
163 }
164 if let Some(got) = n_dist
165 && got != n * k
166 {
167 return Err(MethodsErrors::GraphLengthMismatch {
168 name: "dist",
169 expected: n * k,
170 got,
171 });
172 }
173 if let Some(&bad) = idx.par_iter().find_any(|&&j| j as usize >= n) {
174 return Err(MethodsErrors::GraphIndexOutOfRange {
175 name: "idx",
176 index: bad as usize,
177 n_nodes: n,
178 });
179 }
180 Ok(())
181}
182
183fn csr_to_graph<T>(indptr: Vec<usize>, indices: Vec<u32>, data: Vec<T>, n: usize) -> SparseGraph<T>
196where
197 T: BixverseFloat + Sum,
198{
199 let adjacency = CompressedSparseData2 {
200 data,
201 indices,
202 indptr: indptr.into_iter().map(|p| p as u32).collect(),
203 cs_type: CompressedSparseFormat::Csr,
204 data_2: None,
205 shape: (n, n),
206 };
207 SparseGraph::new(n, adjacency, false)
208}
209
210fn offsets(lens: &[usize]) -> Vec<usize> {
220 let mut ptr = Vec::with_capacity(lens.len() + 1);
221 ptr.push(0);
222 let mut acc = 0;
223 for &l in lens {
224 acc += l;
225 ptr.push(acc);
226 }
227 ptr
228}
229
230fn row_slices_mut<'a, T>(mut data: &'a mut [T], ptr: &[usize]) -> Vec<&'a mut [T]> {
241 let mut out = Vec::with_capacity(ptr.len().saturating_sub(1));
242 for w in ptr.windows(2) {
243 let (row, rest) = data.split_at_mut(w[1] - w[0]);
244 out.push(row);
245 data = rest;
246 }
247 out
248}
249
250fn lists_to_graph<T>(rows: &[Vec<(u32, T)>], n: usize) -> SparseGraph<T>
262where
263 T: BixverseFloat + Send + Sync + Sum,
264{
265 let lens: Vec<usize> = rows.iter().map(|r| r.len()).collect();
266 let ptr = offsets(&lens);
267 let mut indices = vec![0u32; ptr[n]];
268 let mut data = vec![T::zero(); ptr[n]];
269 row_slices_mut(&mut indices, &ptr)
270 .into_par_iter()
271 .zip(row_slices_mut(&mut data, &ptr))
272 .zip(rows.par_iter())
273 .for_each(|((ix, dx), row)| {
274 for (e, &(c, v)) in row.iter().enumerate() {
275 ix[e] = c;
276 dx[e] = v;
277 }
278 });
279 csr_to_graph(ptr, indices, data, n)
280}
281
282fn symmetrise<T, F>(idx: &[u32], w: &[T], k: usize, n: usize, combine: F) -> SparseGraph<T>
301where
302 T: BixverseFloat + Send + Sync + Sum,
303 F: Fn(T, T) -> T + Sync,
304{
305 let out: Vec<Vec<(u32, T)>> = (0..n)
307 .into_par_iter()
308 .map(|i| {
309 let mut row: Vec<(u32, T)> = idx[i * k..(i + 1) * k]
310 .iter()
311 .zip(&w[i * k..(i + 1) * k])
312 .filter(|&(&j, _)| j as usize != i)
313 .map(|(&j, &wt)| (j, wt))
314 .collect();
315 row.sort_unstable_by_key(|e| e.0);
316 row
317 })
318 .collect();
319
320 let mut in_len = vec![0usize; n];
323 for row in &out {
324 for &(j, _) in row {
325 in_len[j as usize] += 1;
326 }
327 }
328 let in_ptr = offsets(&in_len);
329 let mut in_entries = vec![(0u32, T::zero()); in_ptr[n]];
330 let mut cursor = in_ptr.clone();
331 for (i, row) in out.iter().enumerate() {
332 for &(j, wt) in row {
333 in_entries[cursor[j as usize]] = (i as u32, wt);
334 cursor[j as usize] += 1;
335 }
336 }
337
338 let merged: Vec<Vec<(u32, T)>> = (0..n)
340 .into_par_iter()
341 .map(|i| {
342 let a = &out[i];
343 let b = &in_entries[in_ptr[i]..in_ptr[i + 1]];
344 let mut row = Vec::with_capacity(a.len() + b.len());
345 let (mut p, mut q) = (0, 0);
346 while p < a.len() || q < b.len() {
347 let (col, wij, wji) = match (a.get(p), b.get(q)) {
348 (Some(&(ca, wa)), Some(&(cb, wb))) if ca == cb => {
349 p += 1;
350 q += 1;
351 (ca, wa, wb)
352 }
353 (Some(&(ca, wa)), Some(&(cb, _))) if ca < cb => {
354 p += 1;
355 (ca, wa, T::zero())
356 }
357 (Some(&(ca, wa)), None) => {
358 p += 1;
359 (ca, wa, T::zero())
360 }
361 (_, Some(&(cb, wb))) => {
362 q += 1;
363 (cb, T::zero(), wb)
364 }
365 (None, None) => unreachable!(),
366 };
367 let v = combine(wij, wji);
368 if v != T::zero() {
369 row.push((col, v));
370 }
371 }
372 row
373 })
374 .collect();
375
376 lists_to_graph(&merged, n)
377}
378
379fn knn_union<T>(idx: &[u32], k: usize, n: usize, binary: bool) -> SparseGraph<T>
396where
397 T: BixverseFloat + Send + Sync + Sum,
398{
399 let ones = vec![T::one(); idx.len()];
400 if binary {
401 symmetrise(idx, &ones, k, n, |_, _| T::one())
402 } else {
403 symmetrise(idx, &ones, k, n, |a, b| a + b)
404 }
405}
406
407#[inline]
424fn snn_weight<T: BixverseFloat>(
425 count: u32,
426 min_rank: u32,
427 k: usize,
428 similarity: SnnSimilarity,
429) -> T {
430 let kf: T = t(k as f64);
431 match similarity {
432 SnnSimilarity::Jaccard => {
433 let c: T = t(count as f64);
434 c / (t::<T>(2.0) * (kf + T::one()) - c)
435 }
436 SnnSimilarity::Rank => {
437 let raw = kf - t::<T>(min_rank as f64) / t::<T>(2.0);
438 let floor: T = t(SNN_RANK_FLOOR);
439 (if raw > floor { raw } else { floor }) / kf
440 }
441 }
442}
443
444fn pair_snn_weight<T: BixverseFloat>(
460 idx: &[u32],
461 k: usize,
462 a: usize,
463 b: usize,
464 similarity: SnnSimilarity,
465) -> T {
466 let ranked = |c: usize| -> Vec<(u32, u32)> {
467 let mut v: Vec<(u32, u32)> = std::iter::once((c as u32, 0))
468 .chain(
469 idx[c * k..(c + 1) * k]
470 .iter()
471 .enumerate()
472 .map(|(r, &nb)| (nb, r as u32 + 1)),
473 )
474 .collect();
475 v.sort_unstable();
476 v
477 };
478 let (sa, sb) = (ranked(a), ranked(b));
479 let (mut pa, mut pb) = (0, 0);
480 let mut count = 0u32;
481 let mut min_rank = 2 * k as u32;
482 while pa < sa.len() && pb < sb.len() {
483 match sa[pa].0.cmp(&sb[pb].0) {
484 std::cmp::Ordering::Equal => {
485 count += 1;
486 min_rank = min_rank.min(sa[pa].1 + sb[pb].1);
487 pa += 1;
488 pb += 1;
489 }
490 std::cmp::Ordering::Less => pa += 1,
491 std::cmp::Ordering::Greater => pb += 1,
492 }
493 }
494 snn_weight(count, min_rank, k, similarity)
495}
496
497fn rescue_small_components<T>(
520 idx: &[u32],
521 k: usize,
522 n: usize,
523 similarity: SnnSimilarity,
524 lower: &mut [Vec<(u32, T)>],
525) -> usize
526where
527 T: BixverseFloat + Send + Sync,
528{
529 if k == 0 {
530 return 0;
531 }
532 let mut uf = UnionFind::new(n);
533 for (j, row) in lower.iter().enumerate() {
534 for &(i, _) in row {
535 uf.union(i, j as u32);
536 }
537 }
538
539 let mut n_rescued = 0;
540 loop {
541 let roots: Vec<u32> = (0..n as u32).map(|i| uf.find(i)).collect();
542 let mut size = vec![0usize; n];
543 for &r in &roots {
544 size[r as usize] += 1;
545 }
546
547 let mut candidates: Vec<(u32, T, usize, usize)> = (0..n)
549 .into_par_iter()
550 .filter(|&i| size[roots[i] as usize] < MIN_COMPONENT_SIZE)
551 .filter_map(|i| {
552 idx[i * k..(i + 1) * k]
553 .iter()
554 .map(|&nb| nb as usize)
555 .filter(|&nb| roots[nb] != roots[i])
556 .map(|nb| (pair_snn_weight::<T>(idx, k, i, nb, similarity), nb))
557 .fold(None, |acc: Option<(T, usize)>, cur| match acc {
558 Some(a) if a.0 >= cur.0 => Some(a),
559 _ => Some(cur),
560 })
561 .map(|(w, nb)| (roots[i], w, i, nb))
562 })
563 .collect();
564 candidates.sort_unstable_by(|a, b| {
565 a.0.cmp(&b.0)
566 .then(b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal))
567 .then(a.2.cmp(&b.2))
568 });
569 candidates.dedup_by_key(|c| c.0);
570
571 let before = n_rescued;
572 for (_, w, i, nb) in candidates {
573 if uf.union(i as u32, nb as u32) {
575 let (lo, hi) = (i.min(nb), i.max(nb));
576 let row = &mut lower[hi];
577 let pos = row.partition_point(|e| (e.0 as usize) < lo);
578 row.insert(pos, (lo as u32, w));
579 n_rescued += 1;
580 }
581 }
582 if n_rescued == before {
583 break;
584 }
585 }
586 n_rescued
587}
588
589fn snn_full<T>(
611 idx: &[u32],
612 k: usize,
613 n: usize,
614 similarity: SnnSimilarity,
615 prune: T,
616 rescue: bool,
617) -> (SparseGraph<T>, usize)
618where
619 T: BixverseFloat + Send + Sync + Sum,
620{
621 let mut rev_len = vec![1usize; n];
624 for &nb in idx {
625 rev_len[nb as usize] += 1;
626 }
627 let rev_ptr = offsets(&rev_len);
628 let mut rev = vec![(0u32, 0u32); rev_ptr[n]];
629 let mut cursor = rev_ptr.clone();
630 for i in 0..n {
631 rev[cursor[i]] = (i as u32, 0);
632 cursor[i] += 1;
633 for (r, &nb) in idx[i * k..(i + 1) * k].iter().enumerate() {
634 let nb = nb as usize;
635 rev[cursor[nb]] = (i as u32, r as u32 + 1);
636 cursor[nb] += 1;
637 }
638 }
639
640 let mut lower: Vec<Vec<(u32, T)>> = vec![Vec::new(); n];
642 lower
643 .par_chunks_mut(SNN_ROW_CHUNK)
644 .enumerate()
645 .for_each(|(c, chunk)| {
646 let mut scores = vec![u32::MAX; n];
647 let mut added: Vec<u32> = Vec::with_capacity(256);
648 for (off, out) in chunk.iter_mut().enumerate() {
649 let j = c * SNN_ROW_CHUNK + off;
650 added.clear();
651 for r in 0..=k {
652 let v = if r == 0 {
653 j
654 } else {
655 idx[j * k + r - 1] as usize
656 };
657 for &(o, o_rank) in &rev[rev_ptr[v]..rev_ptr[v + 1]] {
658 if (o as usize) >= j {
659 continue;
660 }
661 let s = &mut scores[o as usize];
662 match similarity {
663 SnnSimilarity::Jaccard => {
664 if *s == u32::MAX {
665 *s = 1;
666 added.push(o);
667 } else {
668 *s += 1;
669 }
670 }
671 SnnSimilarity::Rank => {
672 let comb = r as u32 + o_rank;
673 if *s == u32::MAX {
674 *s = comb;
675 added.push(o);
676 } else if comb < *s {
677 *s = comb;
678 }
679 }
680 }
681 }
682 }
683 added.sort_unstable();
684 out.extend(added.iter().filter_map(|&o| {
685 let s = scores[o as usize];
686 let w: T = match similarity {
687 SnnSimilarity::Jaccard => snn_weight(s, 0, k, similarity),
688 SnnSimilarity::Rank => snn_weight(0, s, k, similarity),
689 };
690 (w >= prune).then_some((o, w))
691 }));
692 for &o in &added {
693 scores[o as usize] = u32::MAX;
694 }
695 }
696 });
697
698 let n_rescued = if rescue {
699 rescue_small_components(idx, k, n, similarity, &mut lower)
700 } else {
701 0
702 };
703
704 (lower_to_graph(&lower, n), n_rescued)
705}
706
707fn lower_to_graph<T>(lower: &[Vec<(u32, T)>], n: usize) -> SparseGraph<T>
721where
722 T: BixverseFloat + Send + Sync + Sum,
723{
724 let mut lens: Vec<usize> = lower.iter().map(|r| r.len()).collect();
725 for row in lower {
726 for &(i, _) in row {
727 lens[i as usize] += 1;
728 }
729 }
730 let ptr = offsets(&lens);
731 let mut indices = vec![0u32; ptr[n]];
732 let mut data = vec![T::zero(); ptr[n]];
733
734 let mut cursor: Vec<usize> = (0..n).map(|r| ptr[r] + lower[r].len()).collect();
736 row_slices_mut(&mut indices, &ptr)
737 .into_par_iter()
738 .zip(row_slices_mut(&mut data, &ptr))
739 .zip(lower.par_iter())
740 .for_each(|((ix, dx), row)| {
741 for (e, &(c, v)) in row.iter().enumerate() {
742 ix[e] = c;
743 dx[e] = v;
744 }
745 });
746 for (j, row) in lower.iter().enumerate() {
748 for &(i, v) in row {
749 let p = &mut cursor[i as usize];
750 indices[*p] = j as u32;
751 data[*p] = v;
752 *p += 1;
753 }
754 }
755 csr_to_graph(ptr, indices, data, n)
756}
757
758fn smooth_knn_dist<T>(dist: &[T], k: usize, n: usize, local_connectivity: T) -> (Vec<T>, Vec<T>)
779where
780 T: BixverseFloat + Send + Sync + Sum,
781{
782 let target: T = t::<T>((k + 1) as f64).log2();
783 let tol: T = t(SMOOTH_K_TOLERANCE);
784 let scale: T = t(MIN_K_DIST_SCALE);
785 let two: T = t(2.0);
786 let kp1: T = t((k + 1) as f64);
787 let mean_all: T = t(dist
788 .par_iter()
789 .map(|d| d.to_f64().unwrap_or(0.0))
790 .sum::<f64>()
791 / (n * (k + 1)) as f64);
792 let index = Float::floor(local_connectivity).to_usize().unwrap_or(0);
793 let interp: T = local_connectivity - t::<T>(index as f64);
794
795 (0..n)
796 .into_par_iter()
797 .map(|i| {
798 let row = &dist[i * k..(i + 1) * k];
799 let mut rho = T::zero();
800 let non_zero: Vec<T> = row.iter().copied().filter(|&d| d > T::zero()).collect();
801 if t::<T>(non_zero.len() as f64) >= local_connectivity {
802 if index > 0 {
803 rho = non_zero[index - 1];
804 if interp > tol {
805 rho += interp * (non_zero[index] - non_zero[index - 1]);
806 }
807 } else {
808 rho = interp * non_zero[0];
809 }
810 } else if let Some(m) = non_zero
811 .iter()
812 .copied()
813 .reduce(|a, b| if a > b { a } else { b })
814 {
815 rho = m;
816 }
817
818 let (mut lo, mut hi, mut mid) = (T::zero(), T::max_value(), T::one());
819 for _ in 0..SMOOTH_K_ITERS {
820 let psum: T = row
821 .iter()
822 .map(|&d| {
823 let d = d - rho;
824 if d > T::zero() {
825 (-(d / mid)).exp()
826 } else {
827 T::one()
828 }
829 })
830 .sum();
831 if (psum - target).abs() < tol {
832 break;
833 }
834 if psum > target {
835 hi = mid;
836 mid = (lo + hi) / two;
837 } else {
838 lo = mid;
839 if hi >= T::max_value() {
840 mid *= two;
841 } else {
842 mid = (lo + hi) / two;
843 }
844 }
845 }
846
847 let floor = if rho > T::zero() {
848 scale * row.iter().copied().sum::<T>() / kp1
849 } else {
850 scale * mean_all
851 };
852 (mid.max(floor), rho)
853 })
854 .unzip()
855}
856
857fn umap_graph<T>(
881 idx: &[u32],
882 dist: &[T],
883 k: usize,
884 n: usize,
885 local_connectivity: T,
886 mix_ratio: T,
887) -> SparseGraph<T>
888where
889 T: BixverseFloat + Send + Sync + Sum,
890{
891 let (sigmas, rhos) = smooth_knn_dist(dist, k, n, local_connectivity);
892 let mut w = vec![T::zero(); n * k];
893 if k > 0 {
894 w.par_chunks_mut(k).enumerate().for_each(|(i, row)| {
895 for (e, slot) in row.iter_mut().enumerate() {
896 let d = dist[i * k + e] - rhos[i];
897 *slot = if d <= T::zero() || sigmas[i] == T::zero() {
898 T::one()
899 } else {
900 (-(d / sigmas[i])).exp()
901 };
902 }
903 });
904 }
905 symmetrise(idx, &w, k, n, |a, b| {
906 let prod = a * b;
907 mix_ratio * (a + b - prod) + (T::one() - mix_ratio) * prod
908 })
909}
910
911pub fn build_knn_graph<T>(
931 idx: &[u32],
932 dist: Option<&[T]>,
933 k: usize,
934 n: usize,
935 sym: &KnnSymmetrisation,
936 verbose: usize,
937) -> Result<SparseGraph<T>, MethodsErrors>
938where
939 T: BixverseFloat + Send + Sync + Sum,
940{
941 check_knn(idx, dist.map(|d| d.len()), k, n)?;
942 let verbosity = parse_verbosity_level(verbose);
943 let start = Instant::now();
944
945 let graph = match *sym {
946 KnnSymmetrisation::Knn { binary } => knn_union(idx, k, n, binary),
947 KnnSymmetrisation::Snn {
948 similarity,
949 prune,
950 rescue,
951 } => {
952 let (g, n_rescued) = snn_full(idx, k, n, similarity, t(prune), rescue);
953 if verbosity.normal_verbosity() && rescue {
954 println!("Added {n_rescued} edges to reconnect components that pruning cut off.");
955 }
956 g
957 }
958 KnnSymmetrisation::Umap {
959 local_connectivity,
960 mix_ratio,
961 } => {
962 let d = dist.ok_or(MethodsErrors::KnnGraphNeedsDistances)?;
963 umap_graph(idx, d, k, n, t(local_connectivity), t(mix_ratio))
964 }
965 };
966
967 if verbosity.normal_verbosity() {
968 println!("Built the {sym:?} graph: {:.2?}", start.elapsed());
969 }
970 Ok(graph)
971}
972
973pub fn flatten_knn(knn: &[Vec<usize>]) -> (Vec<u32>, usize) {
984 let k = knn.first().map_or(0, |r| r.len());
985 let mut idx = Vec::with_capacity(knn.len() * k);
986 for row in knn {
987 idx.extend(row.iter().map(|&j| j as u32));
988 }
989 (idx, k)
990}
991
992#[cfg(test)]
997mod tests {
998 use super::*;
999 use approx::assert_relative_eq;
1000
1001 fn small_knn() -> (Vec<u32>, usize, usize) {
1004 (vec![1, 2, 0, 2, 0, 1, 0, 1], 2, 4)
1005 }
1006
1007 fn weight(g: &SparseGraph<f32>, a: usize, b: usize) -> Option<f32> {
1008 let (nb, w) = g.get_neighbours_raw(a);
1009 nb.iter().position(|&x| x as usize == b).map(|p| w[p])
1010 }
1011
1012 fn all_weights(g: &SparseGraph<f32>) -> Vec<f32> {
1013 (0..g.get_node_number())
1014 .flat_map(|i| g.get_neighbours_raw(i).1.to_vec())
1015 .collect()
1016 }
1017
1018 fn assert_symmetric_sorted(g: &SparseGraph<f32>) {
1019 for u in 0..g.get_node_number() {
1020 let (nb, w) = g.get_neighbours_raw(u);
1021 assert!(nb.windows(2).all(|p| p[0] < p[1]), "row {u} not sorted");
1022 for (&v, &wv) in nb.iter().zip(w) {
1023 assert_ne!(v as usize, u, "self loop at {u}");
1024 assert_eq!(weight(g, v as usize, u), Some(wv));
1025 }
1026 }
1027 }
1028
1029 fn random_knn(n: usize, k: usize, seed: u64) -> (Vec<u32>, Vec<f32>) {
1032 let mut s = seed;
1033 let mut next = || {
1034 s ^= s << 13;
1035 s ^= s >> 7;
1036 s ^= s << 17;
1037 s
1038 };
1039 let mut idx = Vec::with_capacity(n * k);
1040 let mut dist = Vec::with_capacity(n * k);
1041 for i in 0..n {
1042 let mut row: Vec<u32> = Vec::with_capacity(k);
1043 while row.len() < k {
1044 let off = (next() % 40) as i64 - 20;
1046 let j = (i as i64 + off).rem_euclid(n as i64) as u32;
1047 if j as usize != i && !row.contains(&j) {
1048 row.push(j);
1049 }
1050 }
1051 let mut d: Vec<f32> = (0..k)
1052 .map(|_| (next() % 1000) as f32 / 100.0 + 0.01)
1053 .collect();
1054 d.sort_by(f32::total_cmp);
1055 idx.extend(row);
1056 dist.extend(d);
1057 }
1058 (idx, dist)
1059 }
1060
1061 #[test]
1063 fn test_knn_union_weights() {
1064 let (idx, k, n) = small_knn();
1065 let g = build_knn_graph(
1066 &idx,
1067 None,
1068 k,
1069 n,
1070 &KnnSymmetrisation::Knn { binary: false },
1071 0,
1072 )
1073 .unwrap();
1074 assert_symmetric_sorted(&g);
1075 assert_eq!(weight(&g, 0, 1), Some(2.0));
1076 assert_eq!(weight(&g, 0, 3), Some(1.0));
1077 assert_eq!(weight(&g, 2, 3), None);
1078 let gb = build_knn_graph(
1079 &idx,
1080 None,
1081 k,
1082 n,
1083 &KnnSymmetrisation::Knn { binary: true },
1084 0,
1085 )
1086 .unwrap();
1087 assert_eq!(weight(&gb, 0, 1), Some(1.0));
1088 assert_eq!(gb.get_node_degree(0), 3);
1089 }
1090
1091 #[test]
1093 fn test_snn_jaccard_known_weights() {
1094 let (idx, k, n) = small_knn();
1095 let sym = KnnSymmetrisation::Snn {
1096 similarity: SnnSimilarity::Jaccard,
1097 prune: 0.0,
1098 rescue: true,
1099 };
1100 let g = build_knn_graph::<f32>(&idx, None, k, n, &sym, 0).unwrap();
1101 assert_symmetric_sorted(&g);
1102 assert_eq!((0..n).map(|i| g.get_node_degree(i)).sum::<usize>(), 12);
1103 assert_relative_eq!(weight(&g, 0, 1).unwrap(), 1.0);
1104 assert_relative_eq!(weight(&g, 0, 3).unwrap(), 0.5);
1105 assert_relative_eq!(weight(&g, 2, 3).unwrap(), 0.5);
1107 }
1108
1109 #[test]
1112 fn test_snn_rank_known_weights() {
1113 let (idx, k, n) = small_knn();
1114 let sym = KnnSymmetrisation::Snn {
1115 similarity: SnnSimilarity::Rank,
1116 prune: 0.0,
1117 rescue: false,
1118 };
1119 let g = build_knn_graph::<f32>(&idx, None, k, n, &sym, 0).unwrap();
1120 assert_relative_eq!(weight(&g, 0, 1).unwrap(), 0.75);
1121 assert_relative_eq!(weight(&g, 2, 3).unwrap(), 0.5);
1123 assert!(all_weights(&g).iter().all(|&w| w > 0.0 && w <= 1.0));
1124 }
1125
1126 #[test]
1129 fn test_snn_prune_and_rescue() {
1130 let (idx, k, n) = small_knn();
1131 let mk = |rescue| KnnSymmetrisation::Snn {
1132 similarity: SnnSimilarity::Jaccard,
1133 prune: 0.6,
1134 rescue,
1135 };
1136 let g = build_knn_graph::<f32>(&idx, None, k, n, &mk(true), 0).unwrap();
1137 assert_symmetric_sorted(&g);
1138 assert_eq!(weight(&g, 0, 3), Some(0.5));
1139 assert_eq!(g.get_node_degree(3), 1);
1140 let g0 = build_knn_graph::<f32>(&idx, None, k, n, &mk(false), 0).unwrap();
1141 assert_eq!(g0.get_node_degree(3), 0);
1142 }
1143
1144 #[test]
1146 fn test_snn_matches_pairwise() {
1147 let (n, k) = (300, 8);
1148 let (idx, _) = random_knn(n, k, 7);
1149 for similarity in [SnnSimilarity::Jaccard, SnnSimilarity::Rank] {
1150 let sym = KnnSymmetrisation::Snn {
1151 similarity,
1152 prune: 0.0,
1153 rescue: false,
1154 };
1155 let g = build_knn_graph::<f32>(&idx, None, k, n, &sym, 0).unwrap();
1156 assert_symmetric_sorted(&g);
1157 for u in 0..n {
1158 let (nb, w) = g.get_neighbours_raw(u);
1159 for (&v, &wv) in nb.iter().zip(w) {
1160 assert_eq!(
1161 wv,
1162 pair_snn_weight::<f32>(&idx, k, u, v as usize, similarity)
1163 );
1164 }
1165 }
1166 }
1167 }
1168
1169 #[test]
1172 fn test_umap_two_nodes() {
1173 let idx = vec![1u32, 0];
1174 let dist = vec![0.5f32, 0.5];
1175 let sym = KnnSymmetrisation::Umap {
1176 local_connectivity: 1.0,
1177 mix_ratio: 1.0,
1178 };
1179 let g = build_knn_graph(&idx, Some(&dist), 1, 2, &sym, 0).unwrap();
1180 assert_eq!(weight(&g, 0, 1), Some(1.0));
1181 }
1182
1183 #[test]
1186 fn test_umap_no_singletons() {
1187 let (n, k) = (500, 10);
1188 let (idx, dist) = random_knn(n, k, 11);
1189 let sym = KnnSymmetrisation::Umap {
1190 local_connectivity: 1.0,
1191 mix_ratio: 1.0,
1192 };
1193 let g = build_knn_graph(&idx, Some(&dist), k, n, &sym, 0).unwrap();
1194 assert_symmetric_sorted(&g);
1195 for i in 0..n {
1196 assert!(g.get_node_degree(i) >= 1);
1197 assert_relative_eq!(
1199 weight(&g, i, idx[i * k] as usize).unwrap(),
1200 1.0,
1201 epsilon = 1e-6
1202 );
1203 }
1204 assert!(all_weights(&g).iter().all(|&w| w > 0.0 && w <= 1.0));
1205 }
1206
1207 #[test]
1210 fn test_umap_mix_ratio() {
1211 let (idx, k, n) = small_knn();
1212 let dist = vec![1.0f32, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0];
1213 let mk = |mix_ratio| KnnSymmetrisation::Umap {
1214 local_connectivity: 1.0,
1215 mix_ratio,
1216 };
1217 let gu = build_knn_graph(&idx, Some(&dist), k, n, &mk(1.0), 0).unwrap();
1218 let gi = build_knn_graph(&idx, Some(&dist), k, n, &mk(0.0), 0).unwrap();
1219 assert_eq!(weight(&gu, 0, 3), Some(1.0));
1221 assert_eq!(weight(&gi, 0, 3), None);
1222 assert!(gi.get_node_degree(0) < gu.get_node_degree(0));
1223 }
1224
1225 #[test]
1228 fn test_build_knn_graph_errors() {
1229 let (idx, k, n) = small_knn();
1230 let umap = KnnSymmetrisation::Umap {
1231 local_connectivity: 1.0,
1232 mix_ratio: 1.0,
1233 };
1234 assert!(matches!(
1235 build_knn_graph::<f32>(&idx, None, k, n, &umap, 0),
1236 Err(MethodsErrors::KnnGraphNeedsDistances)
1237 ));
1238 assert!(matches!(
1239 build_knn_graph::<f32>(&idx[..7], None, k, n, &KnnSymmetrisation::default(), 0),
1240 Err(MethodsErrors::GraphLengthMismatch { .. })
1241 ));
1242 let mut bad = idx.clone();
1243 bad[0] = 9;
1244 assert!(matches!(
1245 build_knn_graph::<f32>(&bad, None, k, n, &KnnSymmetrisation::default(), 0),
1246 Err(MethodsErrors::GraphIndexOutOfRange { .. })
1247 ));
1248 }
1249
1250 #[test]
1252 fn test_parse_snn_similarity() {
1253 assert_eq!(
1254 parse_snn_similarity("JACCARD"),
1255 Some(SnnSimilarity::Jaccard)
1256 );
1257 assert_eq!(parse_snn_similarity("rank"), Some(SnnSimilarity::Rank));
1258 assert_eq!(parse_snn_similarity("nope"), None);
1259 }
1260}