1use crate::core::{Graph, IgraphResult, VertexId};
7
8pub fn cocitation(graph: &Graph) -> IgraphResult<Vec<u32>> {
34 cocitation_impl(graph, true)
35}
36
37pub fn bibcoupling(graph: &Graph) -> IgraphResult<Vec<u32>> {
63 cocitation_impl(graph, false)
64}
65
66pub fn similarity_jaccard_pairs(
97 graph: &Graph,
98 pairs: &[(VertexId, VertexId)],
99) -> IgraphResult<Vec<f64>> {
100 let n = graph.vcount();
101 let adj = build_sorted_adjacency(graph)?;
102 let mut result = Vec::with_capacity(pairs.len());
103
104 for &(u, v) in pairs {
105 if u >= n || v >= n {
106 return Err(crate::core::error::IgraphError::InvalidArgument(
107 "vertex ID out of range in similarity_jaccard_pairs".to_string(),
108 ));
109 }
110 if u == v {
111 result.push(1.0);
112 continue;
113 }
114 let (isect, union_size) =
115 sorted_intersection_union_size(&adj[u as usize], &adj[v as usize]);
116 if union_size == 0 {
117 result.push(0.0);
118 } else {
119 #[allow(clippy::cast_precision_loss)]
120 result.push(isect as f64 / union_size as f64);
121 }
122 }
123
124 Ok(result)
125}
126
127pub fn similarity_dice_pairs(
157 graph: &Graph,
158 pairs: &[(VertexId, VertexId)],
159) -> IgraphResult<Vec<f64>> {
160 let n = graph.vcount();
161 let adj = build_sorted_adjacency(graph)?;
162 let mut result = Vec::with_capacity(pairs.len());
163
164 for &(u, v) in pairs {
165 if u >= n || v >= n {
166 return Err(crate::core::error::IgraphError::InvalidArgument(
167 "vertex ID out of range in similarity_dice_pairs".to_string(),
168 ));
169 }
170 if u == v {
171 result.push(1.0);
172 continue;
173 }
174 let deg_sum = adj[u as usize].len() + adj[v as usize].len();
175 if deg_sum == 0 {
176 result.push(0.0);
177 } else {
178 let (isect, _) = sorted_intersection_union_size(&adj[u as usize], &adj[v as usize]);
179 #[allow(clippy::cast_precision_loss)]
180 result.push(2.0 * isect as f64 / deg_sum as f64);
181 }
182 }
183
184 Ok(result)
185}
186
187pub fn similarity_inverse_log_weighted_pairs(
219 graph: &Graph,
220 pairs: &[(VertexId, VertexId)],
221) -> IgraphResult<Vec<f64>> {
222 let n = graph.vcount();
223 let adj = build_sorted_adjacency(graph)?;
224
225 #[allow(clippy::cast_precision_loss)]
227 let weights: Vec<f64> = adj
228 .iter()
229 .map(|neighbors| {
230 let deg = neighbors.len();
231 if deg > 1 {
232 1.0 / (deg as f64).ln()
233 } else {
234 0.0
235 }
236 })
237 .collect();
238
239 let mut result = Vec::with_capacity(pairs.len());
240
241 for &(u, v) in pairs {
242 if u >= n || v >= n {
243 return Err(crate::core::error::IgraphError::InvalidArgument(
244 "vertex ID out of range in similarity_inverse_log_weighted_pairs".to_string(),
245 ));
246 }
247 if u == v {
248 result.push(0.0);
249 continue;
250 }
251 let common = sorted_intersection_weighted(&adj[u as usize], &adj[v as usize], &weights);
252 result.push(common);
253 }
254
255 Ok(result)
256}
257
258fn sorted_intersection_weighted(a: &[VertexId], b: &[VertexId], weights: &[f64]) -> f64 {
259 let mut i = 0;
260 let mut j = 0;
261 let mut sum = 0.0_f64;
262
263 while i < a.len() && j < b.len() {
264 match a[i].cmp(&b[j]) {
265 std::cmp::Ordering::Less => i += 1,
266 std::cmp::Ordering::Greater => j += 1,
267 std::cmp::Ordering::Equal => {
268 sum += weights[a[i] as usize];
269 i += 1;
270 j += 1;
271 }
272 }
273 }
274
275 sum
276}
277
278fn cocitation_impl(graph: &Graph, is_cocitation: bool) -> IgraphResult<Vec<u32>> {
281 let n = graph.vcount() as usize;
282 let mut result = vec![0u32; n * n];
283
284 if n == 0 {
285 return Ok(result);
286 }
287
288 let ecount = graph.ecount();
289 let directed = graph.is_directed();
290
291 let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n];
300
301 for eid in 0..ecount {
302 #[allow(clippy::cast_possible_truncation)]
303 let (src, tgt) = graph.edge(eid as u32)?;
304
305 if directed {
306 if is_cocitation {
307 adj[src as usize].push(tgt);
310 } else {
311 adj[tgt as usize].push(src);
313 }
314 } else {
315 adj[src as usize].push(tgt);
317 if src != tgt {
318 adj[tgt as usize].push(src);
319 }
320 }
321 }
322
323 for neighbors in &adj {
325 for (i, &nei_u) in neighbors.iter().enumerate() {
326 let u = nei_u as usize;
327 for &nei_v in &neighbors[(i + 1)..] {
328 let v = nei_v as usize;
329 result[u * n + v] += 1;
330 result[v * n + u] += 1;
331 }
332 }
333 }
334
335 Ok(result)
336}
337
338fn build_sorted_adjacency(graph: &Graph) -> IgraphResult<Vec<Vec<VertexId>>> {
339 let n = graph.vcount() as usize;
340 let ecount = graph.ecount();
341 let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n];
342
343 for eid in 0..ecount {
344 #[allow(clippy::cast_possible_truncation)]
345 let (src, tgt) = graph.edge(eid as u32)?;
346 adj[src as usize].push(tgt);
347 if src != tgt {
348 adj[tgt as usize].push(src);
349 }
350 }
351
352 for neighbors in &mut adj {
353 neighbors.sort_unstable();
354 neighbors.dedup();
355 }
356
357 Ok(adj)
358}
359
360pub fn similarity_jaccard(graph: &Graph) -> IgraphResult<Vec<f64>> {
386 let n = graph.vcount() as usize;
387 let adj = build_sorted_adjacency(graph)?;
388 let mut result = vec![0.0_f64; n * n];
389
390 for u in 0..n {
391 result[u * n + u] = 1.0;
392 for v in (u + 1)..n {
393 let (isect, union_size) = sorted_intersection_union_size(&adj[u], &adj[v]);
394 let sim = if union_size == 0 {
395 0.0
396 } else {
397 #[allow(clippy::cast_precision_loss)]
398 {
399 isect as f64 / union_size as f64
400 }
401 };
402 result[u * n + v] = sim;
403 result[v * n + u] = sim;
404 }
405 }
406
407 Ok(result)
408}
409
410pub fn similarity_dice(graph: &Graph) -> IgraphResult<Vec<f64>> {
433 let n = graph.vcount() as usize;
434 let adj = build_sorted_adjacency(graph)?;
435 let mut result = vec![0.0_f64; n * n];
436
437 for u in 0..n {
438 result[u * n + u] = 1.0;
439 for v in (u + 1)..n {
440 let deg_sum = adj[u].len() + adj[v].len();
441 let sim = if deg_sum == 0 {
442 0.0
443 } else {
444 let (isect, _) = sorted_intersection_union_size(&adj[u], &adj[v]);
445 #[allow(clippy::cast_precision_loss)]
446 {
447 2.0 * isect as f64 / deg_sum as f64
448 }
449 };
450 result[u * n + v] = sim;
451 result[v * n + u] = sim;
452 }
453 }
454
455 Ok(result)
456}
457
458pub fn similarity_inverse_log_weighted(graph: &Graph) -> IgraphResult<Vec<f64>> {
482 let n = graph.vcount() as usize;
483 let adj = build_sorted_adjacency(graph)?;
484
485 #[allow(clippy::cast_precision_loss)]
486 let weights: Vec<f64> = adj
487 .iter()
488 .map(|neighbors| {
489 let deg = neighbors.len();
490 if deg > 1 {
491 1.0 / (deg as f64).ln()
492 } else {
493 0.0
494 }
495 })
496 .collect();
497
498 let mut result = vec![0.0_f64; n * n];
499
500 for u in 0..n {
501 for v in (u + 1)..n {
502 let sim = sorted_intersection_weighted(&adj[u], &adj[v], &weights);
503 result[u * n + v] = sim;
504 result[v * n + u] = sim;
505 }
506 }
507
508 Ok(result)
509}
510
511fn sorted_intersection_union_size(a: &[VertexId], b: &[VertexId]) -> (usize, usize) {
512 let mut i = 0;
513 let mut j = 0;
514 let mut isect = 0usize;
515
516 while i < a.len() && j < b.len() {
517 match a[i].cmp(&b[j]) {
518 std::cmp::Ordering::Less => i += 1,
519 std::cmp::Ordering::Greater => j += 1,
520 std::cmp::Ordering::Equal => {
521 isect += 1;
522 i += 1;
523 j += 1;
524 }
525 }
526 }
527
528 let union_size = a.len() + b.len() - isect;
529 (isect, union_size)
530}
531
532pub fn similarity_jaccard_es(graph: &Graph, eids: &[u32]) -> IgraphResult<Vec<f64>> {
560 let pairs: Vec<(VertexId, VertexId)> = eids
561 .iter()
562 .map(|&e| graph.edge(e))
563 .collect::<IgraphResult<Vec<_>>>()?;
564 similarity_jaccard_pairs(graph, &pairs)
565}
566
567pub fn similarity_dice_es(graph: &Graph, eids: &[u32]) -> IgraphResult<Vec<f64>> {
597 let pairs: Vec<(VertexId, VertexId)> = eids
598 .iter()
599 .map(|&e| graph.edge(e))
600 .collect::<IgraphResult<Vec<_>>>()?;
601 similarity_dice_pairs(graph, &pairs)
602}
603
604#[cfg(test)]
605mod tests {
606 use super::*;
607
608 #[test]
609 fn test_cocitation_directed() {
610 let mut g = Graph::new(3, true).unwrap();
612 g.add_edge(2, 0).unwrap();
613 g.add_edge(2, 1).unwrap();
614
615 let scores = cocitation(&g).unwrap();
616 assert_eq!(scores[1], 1); assert_eq!(scores[3], 1); assert_eq!(scores[0], 0); }
621
622 #[test]
623 fn test_cocitation_multiple_citers() {
624 let mut g = Graph::new(4, true).unwrap();
626 g.add_edge(2, 0).unwrap();
627 g.add_edge(2, 1).unwrap();
628 g.add_edge(3, 0).unwrap();
629 g.add_edge(3, 1).unwrap();
630
631 let scores = cocitation(&g).unwrap();
632 assert_eq!(scores[1], 2);
634 assert_eq!(scores[4], 2);
635 }
636
637 #[test]
638 fn test_bibcoupling_directed() {
639 let mut g = Graph::new(3, true).unwrap();
641 g.add_edge(0, 2).unwrap();
642 g.add_edge(1, 2).unwrap();
643
644 let scores = bibcoupling(&g).unwrap();
645 assert_eq!(scores[1], 1);
647 assert_eq!(scores[3], 1);
648 }
649
650 #[test]
651 fn test_cocitation_undirected() {
652 let mut g = Graph::with_vertices(4);
654 g.add_edge(0, 2).unwrap();
655 g.add_edge(1, 2).unwrap();
656 g.add_edge(0, 3).unwrap();
657 g.add_edge(1, 3).unwrap();
658
659 let scores = cocitation(&g).unwrap();
660 assert_eq!(scores[1], 2);
663 assert_eq!(scores[4], 2);
664 }
665
666 #[test]
667 fn test_cocitation_empty() {
668 let g = Graph::with_vertices(0);
669 let scores = cocitation(&g).unwrap();
670 assert!(scores.is_empty());
671 }
672
673 #[test]
674 fn test_cocitation_isolated() {
675 let g = Graph::with_vertices(3);
676 let scores = cocitation(&g).unwrap();
677 assert!(scores.iter().all(|&x| x == 0));
678 }
679
680 #[test]
681 fn test_jaccard_complete_overlap() {
682 let mut g = Graph::with_vertices(3);
685 g.add_edge(0, 1).unwrap();
686 g.add_edge(1, 2).unwrap();
687 g.add_edge(0, 2).unwrap();
688
689 let sim = similarity_jaccard_pairs(&g, &[(0, 1)]).unwrap();
690 assert!((sim[0] - 1.0 / 3.0).abs() < 1e-10);
692 }
693
694 #[test]
695 fn test_jaccard_self() {
696 let mut g = Graph::with_vertices(3);
697 g.add_edge(0, 1).unwrap();
698 let sim = similarity_jaccard_pairs(&g, &[(0, 0)]).unwrap();
699 assert!((sim[0] - 1.0).abs() < 1e-10);
700 }
701
702 #[test]
703 fn test_jaccard_isolated() {
704 let g = Graph::with_vertices(3);
705 let sim = similarity_jaccard_pairs(&g, &[(0, 1)]).unwrap();
706 assert!((sim[0]).abs() < 1e-10);
707 }
708
709 #[test]
710 fn test_jaccard_no_overlap() {
711 let mut g = Graph::with_vertices(4);
713 g.add_edge(0, 1).unwrap();
714 g.add_edge(2, 3).unwrap();
715 let sim = similarity_jaccard_pairs(&g, &[(0, 2)]).unwrap();
716 assert!((sim[0]).abs() < 1e-10);
717 }
718
719 #[test]
720 fn test_jaccard_multiple_pairs() {
721 let mut g = Graph::with_vertices(4);
722 g.add_edge(0, 2).unwrap();
723 g.add_edge(0, 3).unwrap();
724 g.add_edge(1, 2).unwrap();
725 g.add_edge(1, 3).unwrap();
726
727 let sim = similarity_jaccard_pairs(&g, &[(0, 1), (0, 2), (2, 3)]).unwrap();
728 assert!((sim[0] - 1.0).abs() < 1e-10);
730 assert!((sim[1]).abs() < 1e-10);
732 assert!((sim[2] - 1.0).abs() < 1e-10);
734 }
735
736 #[test]
737 fn test_dice_basic() {
738 let mut g = Graph::with_vertices(5);
739 g.add_edge(0, 2).unwrap();
740 g.add_edge(0, 3).unwrap();
741 g.add_edge(1, 2).unwrap();
742 g.add_edge(1, 3).unwrap();
743 g.add_edge(1, 4).unwrap();
744
745 let sim = similarity_dice_pairs(&g, &[(0, 1)]).unwrap();
746 assert!((sim[0] - 0.8).abs() < 1e-10);
749 }
750
751 #[test]
752 fn test_dice_self() {
753 let mut g = Graph::with_vertices(3);
754 g.add_edge(0, 1).unwrap();
755 let sim = similarity_dice_pairs(&g, &[(0, 0)]).unwrap();
756 assert!((sim[0] - 1.0).abs() < 1e-10);
757 }
758
759 #[test]
760 fn test_dice_isolated() {
761 let g = Graph::with_vertices(3);
762 let sim = similarity_dice_pairs(&g, &[(0, 1)]).unwrap();
763 assert!((sim[0]).abs() < 1e-10);
764 }
765
766 #[test]
767 fn test_jaccard_dice_relationship() {
768 let mut g = Graph::with_vertices(5);
769 g.add_edge(0, 2).unwrap();
770 g.add_edge(0, 3).unwrap();
771 g.add_edge(1, 2).unwrap();
772 g.add_edge(1, 3).unwrap();
773 g.add_edge(1, 4).unwrap();
774
775 let jac = similarity_jaccard_pairs(&g, &[(0, 1)]).unwrap();
776 let dice = similarity_dice_pairs(&g, &[(0, 1)]).unwrap();
777 let expected_dice = 2.0 * jac[0] / (1.0 + jac[0]);
779 assert!((dice[0] - expected_dice).abs() < 1e-10);
780 }
781
782 #[test]
783 fn test_jaccard_out_of_range() {
784 let g = Graph::with_vertices(3);
785 assert!(similarity_jaccard_pairs(&g, &[(0, 5)]).is_err());
786 }
787
788 #[test]
789 fn test_inverse_log_weighted_basic() {
790 let mut g = Graph::with_vertices(4);
791 g.add_edge(0, 2).unwrap();
792 g.add_edge(0, 3).unwrap();
793 g.add_edge(1, 2).unwrap();
794 g.add_edge(1, 3).unwrap();
795
796 let sim = similarity_inverse_log_weighted_pairs(&g, &[(0, 1)]).unwrap();
797 assert!((sim[0] - 2.0 / 2.0_f64.ln()).abs() < 1e-10);
800 }
801
802 #[test]
803 fn test_inverse_log_weighted_self() {
804 let mut g = Graph::with_vertices(3);
805 g.add_edge(0, 1).unwrap();
806 let sim = similarity_inverse_log_weighted_pairs(&g, &[(0, 0)]).unwrap();
807 assert!((sim[0]).abs() < 1e-10);
808 }
809
810 #[test]
811 fn test_inverse_log_weighted_isolated() {
812 let g = Graph::with_vertices(3);
813 let sim = similarity_inverse_log_weighted_pairs(&g, &[(0, 1)]).unwrap();
814 assert!((sim[0]).abs() < 1e-10);
815 }
816
817 #[test]
818 fn test_inverse_log_weighted_no_common() {
819 let mut g = Graph::with_vertices(4);
820 g.add_edge(0, 1).unwrap();
821 g.add_edge(2, 3).unwrap();
822 let sim = similarity_inverse_log_weighted_pairs(&g, &[(0, 2)]).unwrap();
823 assert!((sim[0]).abs() < 1e-10);
824 }
825
826 #[test]
827 fn test_inverse_log_weighted_high_degree() {
828 let mut g = Graph::with_vertices(5);
830 g.add_edge(0, 4).unwrap();
831 g.add_edge(1, 4).unwrap();
832 g.add_edge(2, 4).unwrap();
833 g.add_edge(3, 4).unwrap();
834
835 let sim = similarity_inverse_log_weighted_pairs(&g, &[(0, 1)]).unwrap();
836 assert!((sim[0] - 1.0 / 4.0_f64.ln()).abs() < 1e-10);
839 }
840
841 #[test]
842 fn test_inverse_log_weighted_degree_one_neighbor() {
843 let mut g = Graph::with_vertices(3);
845 g.add_edge(0, 2).unwrap();
846 let sim = similarity_inverse_log_weighted_pairs(&g, &[(0, 1)]).unwrap();
848 assert!((sim[0]).abs() < 1e-10);
849 }
850
851 #[test]
852 fn test_inverse_log_weighted_out_of_range() {
853 let g = Graph::with_vertices(3);
854 assert!(similarity_inverse_log_weighted_pairs(&g, &[(0, 5)]).is_err());
855 }
856
857 #[test]
860 fn test_jaccard_matrix_empty() {
861 let g = Graph::with_vertices(0);
862 let sim = similarity_jaccard(&g).unwrap();
863 assert!(sim.is_empty());
864 }
865
866 #[test]
867 fn test_jaccard_matrix_isolated() {
868 let g = Graph::with_vertices(3);
869 let sim = similarity_jaccard(&g).unwrap();
870 for u in 0..3 {
872 assert!((sim[u * 3 + u] - 1.0).abs() < 1e-10);
873 for v in 0..3 {
874 if u != v {
875 assert!(sim[u * 3 + v].abs() < 1e-10);
876 }
877 }
878 }
879 }
880
881 #[test]
882 fn test_jaccard_matrix_agrees_with_pairs() {
883 let mut g = Graph::with_vertices(5);
884 g.add_edge(0, 2).unwrap();
885 g.add_edge(0, 3).unwrap();
886 g.add_edge(1, 2).unwrap();
887 g.add_edge(1, 3).unwrap();
888 g.add_edge(1, 4).unwrap();
889
890 let n = 5;
891 let matrix = similarity_jaccard(&g).unwrap();
892 let pairs = similarity_jaccard_pairs(&g, &[(0, 1), (0, 2), (2, 3)]).unwrap();
893 assert!((matrix[1] - pairs[0]).abs() < 1e-10);
894 assert!((matrix[2] - pairs[1]).abs() < 1e-10);
895 assert!((matrix[2 * n + 3] - pairs[2]).abs() < 1e-10);
896 }
897
898 #[test]
899 fn test_jaccard_matrix_symmetric() {
900 let mut g = Graph::with_vertices(4);
901 g.add_edge(0, 1).unwrap();
902 g.add_edge(1, 2).unwrap();
903 g.add_edge(2, 3).unwrap();
904 let sim = similarity_jaccard(&g).unwrap();
905 for u in 0..4 {
906 for v in 0..4 {
907 assert!(
908 (sim[u * 4 + v] - sim[v * 4 + u]).abs() < 1e-10,
909 "not symmetric at ({u},{v})"
910 );
911 }
912 }
913 }
914
915 #[test]
916 fn test_dice_matrix_agrees_with_pairs() {
917 let mut g = Graph::with_vertices(5);
918 g.add_edge(0, 2).unwrap();
919 g.add_edge(0, 3).unwrap();
920 g.add_edge(1, 2).unwrap();
921 g.add_edge(1, 3).unwrap();
922 g.add_edge(1, 4).unwrap();
923
924 let n = 5;
925 let matrix = similarity_dice(&g).unwrap();
926 let pairs = similarity_dice_pairs(&g, &[(0, 1), (0, 2), (2, 3)]).unwrap();
927 assert!((matrix[1] - pairs[0]).abs() < 1e-10);
928 assert!((matrix[2] - pairs[1]).abs() < 1e-10);
929 assert!((matrix[2 * n + 3] - pairs[2]).abs() < 1e-10);
930 }
931
932 #[test]
933 fn test_dice_matrix_diagonal() {
934 let mut g = Graph::with_vertices(3);
935 g.add_edge(0, 1).unwrap();
936 g.add_edge(1, 2).unwrap();
937 let sim = similarity_dice(&g).unwrap();
938 for u in 0..3 {
939 assert!((sim[u * 3 + u] - 1.0).abs() < 1e-10);
940 }
941 }
942
943 #[test]
944 fn test_inverse_log_weighted_matrix_agrees_with_pairs() {
945 let mut g = Graph::with_vertices(4);
946 g.add_edge(0, 2).unwrap();
947 g.add_edge(0, 3).unwrap();
948 g.add_edge(1, 2).unwrap();
949 g.add_edge(1, 3).unwrap();
950
951 let n = 4;
952 let matrix = similarity_inverse_log_weighted(&g).unwrap();
953 let pairs = similarity_inverse_log_weighted_pairs(&g, &[(0, 1), (0, 2), (2, 3)]).unwrap();
954 assert!((matrix[1] - pairs[0]).abs() < 1e-10);
955 assert!((matrix[2] - pairs[1]).abs() < 1e-10);
956 assert!((matrix[2 * n + 3] - pairs[2]).abs() < 1e-10);
957 }
958
959 #[test]
960 fn test_inverse_log_weighted_matrix_diagonal_zero() {
961 let mut g = Graph::with_vertices(3);
962 g.add_edge(0, 1).unwrap();
963 g.add_edge(1, 2).unwrap();
964 let sim = similarity_inverse_log_weighted(&g).unwrap();
965 for u in 0..3 {
966 assert!(sim[u * 3 + u].abs() < 1e-10);
967 }
968 }
969
970 #[test]
971 fn test_jaccard_dice_matrix_relationship() {
972 let mut g = Graph::with_vertices(5);
973 g.add_edge(0, 2).unwrap();
974 g.add_edge(0, 3).unwrap();
975 g.add_edge(1, 2).unwrap();
976 g.add_edge(1, 3).unwrap();
977 g.add_edge(1, 4).unwrap();
978 g.add_edge(2, 4).unwrap();
979
980 let jac = similarity_jaccard(&g).unwrap();
981 let dice = similarity_dice(&g).unwrap();
982 for u in 0..5 {
983 for v in 0..5 {
984 if u != v {
985 let j = jac[u * 5 + v];
986 let d = dice[u * 5 + v];
987 let expected_d = if j == 0.0 { 0.0 } else { 2.0 * j / (1.0 + j) };
988 assert!(
989 (d - expected_d).abs() < 1e-10,
990 "Dice/Jaccard mismatch at ({u},{v}): d={d}, expected={expected_d}"
991 );
992 }
993 }
994 }
995 }
996
997 #[test]
1000 fn test_jaccard_es_matches_pairs() {
1001 let mut g = Graph::with_vertices(5);
1002 g.add_edge(0, 2).unwrap(); g.add_edge(0, 3).unwrap(); g.add_edge(1, 2).unwrap(); g.add_edge(1, 3).unwrap(); g.add_edge(1, 4).unwrap(); g.add_edge(0, 1).unwrap(); let es_result = similarity_jaccard_es(&g, &[0, 5]).unwrap();
1010 let pairs_result = similarity_jaccard_pairs(&g, &[(0, 2), (0, 1)]).unwrap();
1011 assert!((es_result[0] - pairs_result[0]).abs() < 1e-12);
1012 assert!((es_result[1] - pairs_result[1]).abs() < 1e-12);
1013 }
1014
1015 #[test]
1016 fn test_jaccard_es_empty() {
1017 let mut g = Graph::with_vertices(3);
1018 g.add_edge(0, 1).unwrap();
1019 let result = similarity_jaccard_es(&g, &[]).unwrap();
1020 assert!(result.is_empty());
1021 }
1022
1023 #[test]
1024 fn test_jaccard_es_self_loop() {
1025 let mut g = Graph::with_vertices(3);
1026 g.add_edge(0, 0).unwrap(); g.add_edge(0, 1).unwrap(); let result = similarity_jaccard_es(&g, &[0]).unwrap();
1029 assert!((result[0] - 1.0).abs() < 1e-12);
1031 }
1032
1033 #[test]
1034 fn test_jaccard_es_invalid_edge_id() {
1035 let mut g = Graph::with_vertices(3);
1036 g.add_edge(0, 1).unwrap();
1037 assert!(similarity_jaccard_es(&g, &[99]).is_err());
1038 }
1039
1040 #[test]
1041 fn test_dice_es_matches_pairs() {
1042 let mut g = Graph::with_vertices(5);
1043 g.add_edge(0, 2).unwrap(); g.add_edge(0, 3).unwrap(); g.add_edge(1, 2).unwrap(); g.add_edge(1, 3).unwrap(); g.add_edge(1, 4).unwrap(); g.add_edge(0, 1).unwrap(); let es_result = similarity_dice_es(&g, &[0, 5]).unwrap();
1051 let pairs_result = similarity_dice_pairs(&g, &[(0, 2), (0, 1)]).unwrap();
1052 assert!((es_result[0] - pairs_result[0]).abs() < 1e-12);
1053 assert!((es_result[1] - pairs_result[1]).abs() < 1e-12);
1054 }
1055
1056 #[test]
1057 fn test_dice_es_empty() {
1058 let mut g = Graph::with_vertices(3);
1059 g.add_edge(0, 1).unwrap();
1060 let result = similarity_dice_es(&g, &[]).unwrap();
1061 assert!(result.is_empty());
1062 }
1063
1064 #[test]
1065 fn test_dice_es_jaccard_relationship() {
1066 let mut g = Graph::with_vertices(5);
1067 g.add_edge(0, 2).unwrap(); g.add_edge(0, 3).unwrap(); g.add_edge(1, 2).unwrap(); g.add_edge(1, 3).unwrap(); g.add_edge(1, 4).unwrap(); g.add_edge(0, 1).unwrap(); let jac = similarity_jaccard_es(&g, &[5]).unwrap();
1075 let dice = similarity_dice_es(&g, &[5]).unwrap();
1076 let j = jac[0];
1077 let expected_d = 2.0 * j / (1.0 + j);
1078 assert!((dice[0] - expected_d).abs() < 1e-12);
1079 }
1080
1081 #[test]
1082 fn test_jaccard_es_all_edges() {
1083 let mut g = Graph::with_vertices(4);
1084 g.add_edge(0, 1).unwrap(); g.add_edge(1, 2).unwrap(); g.add_edge(2, 3).unwrap(); g.add_edge(0, 3).unwrap(); let eids: Vec<u32> = (0..4).collect();
1090 let es_result = similarity_jaccard_es(&g, &eids).unwrap();
1091 let pairs = [(0u32, 1u32), (1, 2), (2, 3), (0, 3)];
1092 let pairs_result = similarity_jaccard_pairs(&g, &pairs).unwrap();
1093 for i in 0..4 {
1094 assert!((es_result[i] - pairs_result[i]).abs() < 1e-12);
1095 }
1096 }
1097}