1use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
27
28pub fn count_triangles(graph: &Graph) -> IgraphResult<u64> {
35 let (triangles, _) = triangles_and_triples(graph)?;
36 Ok(triangles)
37}
38
39pub fn count_adjacent_triangles(graph: &Graph) -> IgraphResult<Vec<u64>> {
75 let (per_vertex_triangles, _) = per_vertex_triangle_stats(graph)?;
76 Ok(per_vertex_triangles)
77}
78
79pub fn transitivity_undirected(graph: &Graph) -> IgraphResult<Option<f64>> {
109 let (triangles, triples) = triangles_and_triples(graph)?;
110 if triples == 0 {
111 return Ok(None);
112 }
113 #[allow(clippy::cast_precision_loss)]
117 let t = (triangles as f64) * 3.0 / (triples as f64);
118 Ok(Some(t))
119}
120
121pub fn transitivity_local_undirected(graph: &Graph) -> IgraphResult<Vec<Option<f64>>> {
155 let n = graph.vcount();
156 let n_us = n as usize;
157 let (per_vertex_triangles, simple_degrees) = per_vertex_triangle_stats(graph)?;
158 let mut out: Vec<Option<f64>> = Vec::with_capacity(n_us);
159 for v in 0..n_us {
160 let d = simple_degrees[v];
161 if d < 2 {
162 out.push(None);
163 continue;
164 }
165 let t = per_vertex_triangles[v];
166 #[allow(clippy::cast_precision_loss)]
169 let val = 2.0 * (t as f64) / ((d as f64) * ((d - 1) as f64));
170 out.push(Some(val));
171 }
172 Ok(out)
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub enum TransitivityMode {
178 Nan,
180 Zero,
182}
183
184pub fn transitivity_avglocal_undirected(
206 graph: &Graph,
207 mode: TransitivityMode,
208) -> IgraphResult<f64> {
209 let n = graph.vcount() as usize;
210 if n == 0 {
211 return Ok(if mode == TransitivityMode::Zero {
212 0.0
213 } else {
214 f64::NAN
215 });
216 }
217
218 let local = transitivity_local_undirected(graph)?;
219 let mut sum = 0.0_f64;
220 let mut count = 0_u64;
221
222 for val in &local {
223 match val {
224 Some(t) => {
225 sum += t;
226 count += 1;
227 }
228 None => {
229 if mode == TransitivityMode::Zero {
230 count += 1;
231 }
232 }
233 }
234 }
235
236 if count == 0 {
237 Ok(f64::NAN)
238 } else {
239 #[allow(clippy::cast_precision_loss)]
240 Ok(sum / count as f64)
241 }
242}
243
244fn per_vertex_triangle_stats(graph: &Graph) -> IgraphResult<(Vec<u64>, Vec<u64>)> {
247 let n = graph.vcount();
248 let n_us = n as usize;
249
250 let mut adj: Vec<Vec<VertexId>> = Vec::with_capacity(n_us);
251 for v in 0..n {
252 let raw = graph.neighbors(v)?;
253 let mut simple: Vec<VertexId> = raw.into_iter().filter(|&u| u != v).collect();
254 simple.sort_unstable();
255 simple.dedup();
256 adj.push(simple);
257 }
258
259 let degrees: Vec<u64> = adj.iter().map(|nei| nei.len() as u64).collect();
260 let mut tri_counts: Vec<u64> = vec![0; n_us];
261 let mut mark: Vec<u32> = vec![0; n_us];
262
263 for v1 in 0..n {
264 let nei1 = &adj[v1 as usize];
265 if nei1.len() < 2 {
266 continue;
267 }
268 let v1_mark = v1
269 .checked_add(1)
270 .ok_or(IgraphError::Internal("vertex id overflow"))?;
271 for &v2 in nei1 {
272 if v2 >= v1 {
273 break;
274 }
275 mark[v2 as usize] = v1_mark;
276 }
277 for &v2 in nei1 {
278 if v2 >= v1 {
279 break;
280 }
281 let nei2 = &adj[v2 as usize];
282 for &v3 in nei2 {
283 if v3 >= v2 {
284 break;
285 }
286 if mark[v3 as usize] == v1_mark {
287 tri_counts[v1 as usize] += 1;
288 tri_counts[v2 as usize] += 1;
289 tri_counts[v3 as usize] += 1;
290 }
291 }
292 }
293 }
294
295 Ok((tri_counts, degrees))
296}
297
298pub fn transitivity_barrat(graph: &Graph, weights: &[f64]) -> IgraphResult<Vec<Option<f64>>> {
337 if graph.is_directed() {
338 return Err(IgraphError::Internal(
339 "transitivity_barrat: directed graphs not supported in Phase-1 minimal slice",
340 ));
341 }
342 let n = graph.vcount();
343 let n_us = n as usize;
344 let m = graph.ecount();
345 if weights.len() != m {
346 return Err(IgraphError::Internal(
347 "weights length does not match ecount in transitivity_barrat",
348 ));
349 }
350 for &w in weights {
351 if !w.is_finite() || w < 0.0 {
352 return Err(IgraphError::Internal(
353 "weights must be finite and non-negative in transitivity_barrat",
354 ));
355 }
356 }
357 if !crate::algorithms::properties::is_simple::is_simple(graph)? {
361 return Err(IgraphError::Internal(
362 "transitivity_barrat requires a simple graph (no multi-edges, no self-loops)",
363 ));
364 }
365
366 if n_us == 0 {
367 return Ok(Vec::new());
368 }
369
370 let mut nei_mark: Vec<u64> = vec![0; n_us];
375 let mut actw: Vec<f64> = vec![0.0; n_us];
376 let mut out: Vec<Option<f64>> = Vec::with_capacity(n_us);
377
378 for v in 0..n {
379 let i_mark = u64::from(v) + 1;
380 let inc_v = graph.incident(v)?;
381 let mut strength = 0.0_f64;
382 for &e in &inc_v {
383 let u = graph.edge_other(e, v)?;
384 nei_mark[u as usize] = i_mark;
385 actw[u as usize] = weights[e as usize];
386 strength += weights[e as usize];
387 }
388 let deg_v = inc_v.len();
389 if deg_v < 2 {
390 out.push(None);
391 continue;
392 }
393 #[allow(clippy::cast_precision_loss)]
396 let triples = strength * (deg_v as f64 - 1.0);
397 let mut triangles_w = 0.0_f64;
398
399 for &e1 in &inc_v {
400 let u = graph.edge_other(e1, v)?;
401 let w1 = weights[e1 as usize];
402 let inc_u = graph.incident(u)?;
403 for &e2 in &inc_u {
404 let u2 = graph.edge_other(e2, u)?;
405 if nei_mark[u2 as usize] == i_mark {
406 triangles_w += f64::midpoint(actw[u2 as usize], w1);
407 }
408 }
409 }
410
411 if triples > 0.0 {
412 out.push(Some(triangles_w / triples));
413 } else {
414 out.push(None);
415 }
416 }
417
418 Ok(out)
419}
420
421fn triangles_and_triples(graph: &Graph) -> IgraphResult<(u64, u64)> {
422 let n = graph.vcount();
423 let n_us = n as usize;
424
425 let mut adj: Vec<Vec<VertexId>> = Vec::with_capacity(n_us);
428 for v in 0..n {
429 let raw = graph.neighbors(v)?;
430 let mut simple: Vec<VertexId> = raw.into_iter().filter(|&u| u != v).collect();
431 simple.sort_unstable();
432 simple.dedup();
433 adj.push(simple);
434 }
435
436 let mut mark: Vec<u32> = vec![0; n_us];
439 let mut triangles: u64 = 0;
440 let mut triples: u64 = 0;
441
442 for v1 in 0..n {
443 let nei1 = &adj[v1 as usize];
444 let d1 = nei1.len();
445 if d1 < 2 {
446 continue;
447 }
448 triples = triples.saturating_add((d1 as u64) * ((d1 - 1) as u64) / 2);
450
451 let v1_mark = v1
453 .checked_add(1)
454 .ok_or(IgraphError::Internal("vertex id overflow"))?;
455 for &v2 in nei1 {
456 if v2 >= v1 {
457 break;
458 }
459 mark[v2 as usize] = v1_mark;
460 }
461
462 for &v2 in nei1 {
464 if v2 >= v1 {
465 break;
466 }
467 let nei2 = &adj[v2 as usize];
468 for &v3 in nei2 {
469 if v3 >= v2 {
470 break;
471 }
472 if mark[v3 as usize] == v1_mark {
473 triangles = triangles.saturating_add(1);
474 }
475 }
476 }
477 }
478
479 Ok((triangles, triples))
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485
486 #[test]
487 fn empty_graph_has_no_triangles_or_transitivity() {
488 let g = Graph::with_vertices(0);
489 assert_eq!(count_triangles(&g).unwrap(), 0);
490 assert_eq!(transitivity_undirected(&g).unwrap(), None);
491 }
492
493 #[test]
494 fn isolated_vertices_give_no_triples() {
495 let g = Graph::with_vertices(5);
496 assert_eq!(count_triangles(&g).unwrap(), 0);
497 assert_eq!(transitivity_undirected(&g).unwrap(), None);
498 }
499
500 #[test]
501 fn triangle_count_is_one_transitivity_is_one() {
502 let mut g = Graph::with_vertices(3);
503 g.add_edge(0, 1).unwrap();
504 g.add_edge(1, 2).unwrap();
505 g.add_edge(2, 0).unwrap();
506 assert_eq!(count_triangles(&g).unwrap(), 1);
507 assert_eq!(transitivity_undirected(&g).unwrap(), Some(1.0));
508 }
509
510 #[test]
511 fn k4_has_4_triangles_transitivity_1() {
512 let mut g = Graph::with_vertices(4);
513 for u in 0..4u32 {
514 for v in (u + 1)..4 {
515 g.add_edge(u, v).unwrap();
516 }
517 }
518 assert_eq!(count_triangles(&g).unwrap(), 4);
519 assert_eq!(transitivity_undirected(&g).unwrap(), Some(1.0));
520 }
521
522 #[test]
523 fn cycle_4_has_no_triangles_transitivity_zero() {
524 let mut g = Graph::with_vertices(4);
525 for i in 0..4u32 {
526 g.add_edge(i, (i + 1) % 4).unwrap();
527 }
528 assert_eq!(count_triangles(&g).unwrap(), 0);
529 assert_eq!(transitivity_undirected(&g).unwrap(), Some(0.0));
530 }
531
532 #[test]
533 fn star_has_no_triangles_transitivity_zero() {
534 let mut g = Graph::with_vertices(4);
535 for v in 1..4 {
536 g.add_edge(0, v).unwrap();
537 }
538 assert_eq!(count_triangles(&g).unwrap(), 0);
539 assert_eq!(transitivity_undirected(&g).unwrap(), Some(0.0));
541 }
542
543 #[test]
544 fn path_has_one_triple_no_triangle() {
545 let mut g = Graph::with_vertices(3);
547 g.add_edge(0, 1).unwrap();
548 g.add_edge(1, 2).unwrap();
549 assert_eq!(count_triangles(&g).unwrap(), 0);
550 assert_eq!(transitivity_undirected(&g).unwrap(), Some(0.0));
551 }
552
553 #[test]
554 fn self_loop_is_ignored() {
555 let mut g = Graph::with_vertices(3);
556 g.add_edge(0, 0).unwrap(); g.add_edge(0, 1).unwrap();
558 g.add_edge(1, 2).unwrap();
559 g.add_edge(2, 0).unwrap();
560 assert_eq!(count_triangles(&g).unwrap(), 1);
562 assert_eq!(transitivity_undirected(&g).unwrap(), Some(1.0));
563 }
564
565 #[test]
566 fn parallel_edges_are_ignored() {
567 let mut g = Graph::with_vertices(3);
569 g.add_edge(0, 1).unwrap();
570 g.add_edge(0, 1).unwrap(); g.add_edge(1, 2).unwrap();
572 g.add_edge(2, 0).unwrap();
573 assert_eq!(count_triangles(&g).unwrap(), 1);
574 assert_eq!(transitivity_undirected(&g).unwrap(), Some(1.0));
575 }
576
577 #[test]
578 fn two_disjoint_triangles_count_as_two() {
579 let mut g = Graph::with_vertices(6);
580 g.add_edge(0, 1).unwrap();
581 g.add_edge(1, 2).unwrap();
582 g.add_edge(2, 0).unwrap();
583 g.add_edge(3, 4).unwrap();
584 g.add_edge(4, 5).unwrap();
585 g.add_edge(5, 3).unwrap();
586 assert_eq!(count_triangles(&g).unwrap(), 2);
587 assert_eq!(transitivity_undirected(&g).unwrap(), Some(1.0));
589 }
590
591 #[test]
592 fn local_transitivity_triangle_is_all_ones() {
593 let mut g = Graph::with_vertices(3);
594 g.add_edge(0, 1).unwrap();
595 g.add_edge(1, 2).unwrap();
596 g.add_edge(2, 0).unwrap();
597 assert_eq!(
598 transitivity_local_undirected(&g).unwrap(),
599 vec![Some(1.0), Some(1.0), Some(1.0)]
600 );
601 }
602
603 #[test]
604 fn local_transitivity_star_centre_zero_leaves_none() {
605 let mut g = Graph::with_vertices(4);
606 for v in 1..4 {
607 g.add_edge(0, v).unwrap();
608 }
609 assert_eq!(
610 transitivity_local_undirected(&g).unwrap(),
611 vec![Some(0.0), None, None, None]
612 );
613 }
614
615 #[test]
616 fn local_transitivity_isolated_vertices_all_none() {
617 let g = Graph::with_vertices(3);
618 assert_eq!(
619 transitivity_local_undirected(&g).unwrap(),
620 vec![None, None, None]
621 );
622 }
623
624 #[test]
625 fn local_transitivity_diamond_per_vertex() {
626 let mut g = Graph::with_vertices(4);
630 g.add_edge(0, 1).unwrap();
631 g.add_edge(0, 2).unwrap();
632 g.add_edge(1, 2).unwrap();
633 g.add_edge(1, 3).unwrap();
634 g.add_edge(2, 3).unwrap();
635 let r = transitivity_local_undirected(&g).unwrap();
636 assert_eq!(r[0], Some(1.0));
637 assert_eq!(r[3], Some(1.0));
638 let two_thirds = 2.0_f64 / 3.0;
641 assert_eq!(r[1], Some(two_thirds));
642 assert_eq!(r[2], Some(two_thirds));
643 }
644
645 #[test]
646 fn barrat_unit_weights_match_unweighted_on_k4() {
647 let mut g = Graph::with_vertices(4);
650 for u in 0..4u32 {
651 for v in (u + 1)..4 {
652 g.add_edge(u, v).unwrap();
653 }
654 }
655 let r = transitivity_barrat(&g, &[1.0; 6]).unwrap();
656 assert_eq!(r, vec![Some(1.0); 4]);
657 }
658
659 #[test]
660 fn barrat_triangle_unit_weights_all_ones() {
661 let mut g = Graph::with_vertices(3);
662 g.add_edge(0, 1).unwrap();
663 g.add_edge(1, 2).unwrap();
664 g.add_edge(2, 0).unwrap();
665 let r = transitivity_barrat(&g, &[1.0; 3]).unwrap();
666 assert_eq!(r, vec![Some(1.0), Some(1.0), Some(1.0)]);
667 }
668
669 #[test]
670 fn barrat_triangle_unequal_weights_hand_check() {
671 let mut g = Graph::with_vertices(3);
679 g.add_edge(0, 1).unwrap();
680 g.add_edge(1, 2).unwrap();
681 g.add_edge(2, 0).unwrap();
682 let r = transitivity_barrat(&g, &[1.0, 2.0, 4.0]).unwrap();
683 assert_eq!(r, vec![Some(1.0), Some(1.0), Some(1.0)]);
684 }
685
686 #[test]
687 fn barrat_path_no_triangles_yields_zero_inner_none_endpoints() {
688 let mut g = Graph::with_vertices(3);
691 g.add_edge(0, 1).unwrap();
692 g.add_edge(1, 2).unwrap();
693 let r = transitivity_barrat(&g, &[1.0, 1.0]).unwrap();
694 assert_eq!(r, vec![None, Some(0.0), None]);
695 }
696
697 #[test]
698 fn barrat_isolated_vertex_yields_none() {
699 let mut g = Graph::with_vertices(3);
700 g.add_edge(0, 1).unwrap();
701 let r = transitivity_barrat(&g, &[1.0]).unwrap();
702 assert_eq!(r, vec![None, None, None]);
703 }
704
705 #[test]
706 fn barrat_empty_graph_empty_result() {
707 let g = Graph::with_vertices(0);
708 let r = transitivity_barrat(&g, &[]).unwrap();
709 assert!(r.is_empty());
710 }
711
712 #[test]
713 fn barrat_diamond_unit_weights() {
714 let mut g = Graph::with_vertices(4);
717 g.add_edge(0, 1).unwrap();
718 g.add_edge(0, 2).unwrap();
719 g.add_edge(1, 2).unwrap();
720 g.add_edge(1, 3).unwrap();
721 g.add_edge(2, 3).unwrap();
722 let r = transitivity_barrat(&g, &[1.0; 5]).unwrap();
723 let two_thirds = 2.0_f64 / 3.0;
724 assert_eq!(r[0], Some(1.0));
725 assert_eq!(r[3], Some(1.0));
726 match (r[1], r[2]) {
727 (Some(a), Some(b)) => {
728 assert!((a - two_thirds).abs() < 1e-12);
729 assert!((b - two_thirds).abs() < 1e-12);
730 }
731 _ => panic!("middle vertices must be Some"),
732 }
733 }
734
735 #[test]
736 fn barrat_invalid_weight_length_errors() {
737 let mut g = Graph::with_vertices(3);
738 g.add_edge(0, 1).unwrap();
739 g.add_edge(1, 2).unwrap();
740 assert!(transitivity_barrat(&g, &[1.0]).is_err());
741 }
742
743 #[test]
744 fn barrat_negative_weight_errors() {
745 let mut g = Graph::with_vertices(3);
746 g.add_edge(0, 1).unwrap();
747 g.add_edge(1, 2).unwrap();
748 assert!(transitivity_barrat(&g, &[1.0, -1.0]).is_err());
749 }
750
751 #[test]
752 fn barrat_self_loop_rejected() {
753 let mut g = Graph::with_vertices(2);
754 g.add_edge(0, 0).unwrap();
755 g.add_edge(0, 1).unwrap();
756 assert!(transitivity_barrat(&g, &[1.0, 1.0]).is_err());
757 }
758
759 #[test]
760 fn barrat_multi_edge_rejected() {
761 let mut g = Graph::with_vertices(2);
762 g.add_edge(0, 1).unwrap();
763 g.add_edge(0, 1).unwrap();
764 assert!(transitivity_barrat(&g, &[1.0, 1.0]).is_err());
765 }
766
767 #[test]
768 fn barrat_directed_rejected() {
769 let mut g = Graph::new(2, true).unwrap();
770 g.add_edge(0, 1).unwrap();
771 assert!(transitivity_barrat(&g, &[1.0]).is_err());
772 }
773
774 #[test]
777 fn adjacent_triangles_empty_graph() {
778 let g = Graph::with_vertices(0);
779 assert_eq!(count_adjacent_triangles(&g).unwrap(), Vec::<u64>::new());
780 }
781
782 #[test]
783 fn adjacent_triangles_isolated_vertices() {
784 let g = Graph::with_vertices(5);
785 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![0; 5]);
786 }
787
788 #[test]
789 fn adjacent_triangles_single_triangle() {
790 let mut g = Graph::with_vertices(3);
791 g.add_edge(0, 1).unwrap();
792 g.add_edge(1, 2).unwrap();
793 g.add_edge(2, 0).unwrap();
794 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![1, 1, 1]);
795 }
796
797 #[test]
798 fn adjacent_triangles_k4_each_vertex_sees_3() {
799 let mut g = Graph::with_vertices(4);
800 for u in 0..4u32 {
801 for v in (u + 1)..4 {
802 g.add_edge(u, v).unwrap();
803 }
804 }
805 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![3, 3, 3, 3]);
807 }
808
809 #[test]
810 fn adjacent_triangles_diamond_k4_minus_edge() {
811 let mut g = Graph::with_vertices(4);
814 g.add_edge(0, 1).unwrap();
815 g.add_edge(0, 2).unwrap();
816 g.add_edge(1, 2).unwrap();
817 g.add_edge(1, 3).unwrap();
818 g.add_edge(2, 3).unwrap();
819 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![1, 2, 2, 1]);
820 }
821
822 #[test]
823 fn adjacent_triangles_star_all_zero() {
824 let mut g = Graph::with_vertices(5);
825 for v in 1..5u32 {
826 g.add_edge(0, v).unwrap();
827 }
828 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![0; 5]);
829 }
830
831 #[test]
832 fn adjacent_triangles_self_loops_ignored() {
833 let mut g = Graph::with_vertices(3);
834 g.add_edge(0, 0).unwrap();
835 g.add_edge(1, 1).unwrap();
836 g.add_edge(0, 1).unwrap();
837 g.add_edge(1, 2).unwrap();
838 g.add_edge(2, 0).unwrap();
839 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![1, 1, 1]);
841 }
842
843 #[test]
844 fn adjacent_triangles_parallel_edges_ignored() {
845 let mut g = Graph::with_vertices(3);
846 g.add_edge(0, 1).unwrap();
847 g.add_edge(0, 1).unwrap();
848 g.add_edge(1, 2).unwrap();
849 g.add_edge(1, 2).unwrap();
850 g.add_edge(2, 0).unwrap();
851 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![1, 1, 1]);
852 }
853
854 #[test]
855 fn adjacent_triangles_two_disjoint_triangles() {
856 let mut g = Graph::with_vertices(6);
857 g.add_edge(0, 1).unwrap();
858 g.add_edge(1, 2).unwrap();
859 g.add_edge(2, 0).unwrap();
860 g.add_edge(3, 4).unwrap();
861 g.add_edge(4, 5).unwrap();
862 g.add_edge(5, 3).unwrap();
863 assert_eq!(
864 count_adjacent_triangles(&g).unwrap(),
865 vec![1, 1, 1, 1, 1, 1]
866 );
867 }
868
869 #[test]
870 fn adjacent_triangles_sum_equals_three_times_count_triangles() {
871 let mut g = Graph::with_vertices(4);
873 for u in 0..4u32 {
874 for v in (u + 1)..4 {
875 g.add_edge(u, v).unwrap();
876 }
877 }
878 let per_vertex = count_adjacent_triangles(&g).unwrap();
879 let total = count_triangles(&g).unwrap();
880 assert_eq!(per_vertex.iter().sum::<u64>(), 3 * total);
881 }
882
883 #[test]
884 fn adjacent_triangles_consistent_with_local_transitivity() {
885 let mut g = Graph::with_vertices(3);
888 g.add_edge(0, 1).unwrap();
889 g.add_edge(1, 2).unwrap();
890 g.add_edge(2, 0).unwrap();
891 let adj = count_adjacent_triangles(&g).unwrap();
892 let local = transitivity_local_undirected(&g).unwrap();
893 for v in 0..3 {
894 assert_eq!(adj[v], 1);
895 assert_eq!(local[v], Some(1.0));
896 }
897 }
898
899 #[test]
900 fn diamond_k4_minus_edge_transitivity_below_one() {
901 let mut g = Graph::with_vertices(4);
905 g.add_edge(0, 1).unwrap();
906 g.add_edge(0, 2).unwrap();
907 g.add_edge(1, 2).unwrap();
908 g.add_edge(1, 3).unwrap();
909 g.add_edge(2, 3).unwrap();
910 assert_eq!(count_triangles(&g).unwrap(), 2);
911 assert_eq!(transitivity_undirected(&g).unwrap(), Some(0.75));
912 }
913}