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