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 mut simple: Vec<VertexId> = graph.neighbors_iter(v)?.filter(|&u| u != v).collect();
264 simple.sort_unstable();
265 simple.dedup();
266 adj.push(simple);
267 }
268
269 let degrees: Vec<u64> = adj.iter().map(|nei| nei.len() as u64).collect();
270 let mut tri_counts: Vec<u64> = vec![0; n_us];
271 let mut mark: Vec<u32> = vec![0; n_us];
272
273 for v1 in 0..n {
274 let nei1 = &adj[v1 as usize];
275 if nei1.len() < 2 {
276 continue;
277 }
278 let v1_mark = v1
279 .checked_add(1)
280 .ok_or(IgraphError::Internal("vertex id overflow"))?;
281 for &v2 in nei1 {
282 if v2 >= v1 {
283 break;
284 }
285 mark[v2 as usize] = v1_mark;
286 }
287 for &v2 in nei1 {
288 if v2 >= v1 {
289 break;
290 }
291 let nei2 = &adj[v2 as usize];
292 for &v3 in nei2 {
293 if v3 >= v2 {
294 break;
295 }
296 if mark[v3 as usize] == v1_mark {
297 tri_counts[v1 as usize] += 1;
298 tri_counts[v2 as usize] += 1;
299 tri_counts[v3 as usize] += 1;
300 }
301 }
302 }
303 }
304
305 Ok((tri_counts, degrees))
306}
307
308pub fn transitivity_barrat(graph: &Graph, weights: &[f64]) -> IgraphResult<Vec<Option<f64>>> {
347 if graph.is_directed() {
348 return Err(IgraphError::Internal(
349 "transitivity_barrat: directed graphs not supported in Phase-1 minimal slice",
350 ));
351 }
352 let n = graph.vcount();
353 let n_us = n as usize;
354 let m = graph.ecount();
355 if weights.len() != m {
356 return Err(IgraphError::Internal(
357 "weights length does not match ecount in transitivity_barrat",
358 ));
359 }
360 for &w in weights {
361 if !w.is_finite() || w < 0.0 {
362 return Err(IgraphError::Internal(
363 "weights must be finite and non-negative in transitivity_barrat",
364 ));
365 }
366 }
367 if !crate::algorithms::properties::is_simple::is_simple(graph)? {
371 return Err(IgraphError::Internal(
372 "transitivity_barrat requires a simple graph (no multi-edges, no self-loops)",
373 ));
374 }
375
376 if n_us == 0 {
377 return Ok(Vec::new());
378 }
379
380 let mut nei_mark: Vec<u64> = vec![0; n_us];
385 let mut actw: Vec<f64> = vec![0.0; n_us];
386 let mut out: Vec<Option<f64>> = Vec::with_capacity(n_us);
387
388 for v in 0..n {
389 let i_mark = u64::from(v) + 1;
390 let inc_v = graph.incident(v)?;
391 let mut strength = 0.0_f64;
392 for &e in &inc_v {
393 let u = graph.edge_other(e, v)?;
394 nei_mark[u as usize] = i_mark;
395 actw[u as usize] = weights[e as usize];
396 strength += weights[e as usize];
397 }
398 let deg_v = inc_v.len();
399 if deg_v < 2 {
400 out.push(None);
401 continue;
402 }
403 #[allow(clippy::cast_precision_loss)]
406 let triples = strength * (deg_v as f64 - 1.0);
407 let mut triangles_w = 0.0_f64;
408
409 for &e1 in &inc_v {
410 let u = graph.edge_other(e1, v)?;
411 let w1 = weights[e1 as usize];
412 let inc_u = graph.incident(u)?;
413 for &e2 in &inc_u {
414 let u2 = graph.edge_other(e2, u)?;
415 if nei_mark[u2 as usize] == i_mark {
416 triangles_w += f64::midpoint(actw[u2 as usize], w1);
417 }
418 }
419 }
420
421 if triples > 0.0 {
422 out.push(Some(triangles_w / triples));
423 } else {
424 out.push(None);
425 }
426 }
427
428 Ok(out)
429}
430
431fn triangles_and_triples(graph: &Graph) -> IgraphResult<(u64, u64)> {
432 let n = graph.vcount();
433 let n_us = n as usize;
434
435 let mut adj: Vec<Vec<VertexId>> = Vec::with_capacity(n_us);
438 for v in 0..n {
439 let mut simple: Vec<VertexId> = graph.neighbors_iter(v)?.filter(|&u| u != v).collect();
440 simple.sort_unstable();
441 simple.dedup();
442 adj.push(simple);
443 }
444
445 let mut mark: Vec<u32> = vec![0; n_us];
448 let mut triangles: u64 = 0;
449 let mut triples: u64 = 0;
450
451 for v1 in 0..n {
452 let nei1 = &adj[v1 as usize];
453 let d1 = nei1.len();
454 if d1 < 2 {
455 continue;
456 }
457 triples = triples.saturating_add((d1 as u64) * ((d1 - 1) as u64) / 2);
459
460 let v1_mark = v1
462 .checked_add(1)
463 .ok_or(IgraphError::Internal("vertex id overflow"))?;
464 for &v2 in nei1 {
465 if v2 >= v1 {
466 break;
467 }
468 mark[v2 as usize] = v1_mark;
469 }
470
471 for &v2 in nei1 {
473 if v2 >= v1 {
474 break;
475 }
476 let nei2 = &adj[v2 as usize];
477 for &v3 in nei2 {
478 if v3 >= v2 {
479 break;
480 }
481 if mark[v3 as usize] == v1_mark {
482 triangles = triangles.saturating_add(1);
483 }
484 }
485 }
486 }
487
488 Ok((triangles, triples))
489}
490
491#[cfg(test)]
492mod tests {
493 use super::*;
494
495 #[test]
496 fn empty_graph_has_no_triangles_or_transitivity() {
497 let g = Graph::with_vertices(0);
498 assert_eq!(count_triangles(&g).unwrap(), 0);
499 assert_eq!(transitivity_undirected(&g).unwrap(), None);
500 }
501
502 #[test]
503 fn isolated_vertices_give_no_triples() {
504 let g = Graph::with_vertices(5);
505 assert_eq!(count_triangles(&g).unwrap(), 0);
506 assert_eq!(transitivity_undirected(&g).unwrap(), None);
507 }
508
509 #[test]
510 fn triangle_count_is_one_transitivity_is_one() {
511 let mut g = Graph::with_vertices(3);
512 g.add_edge(0, 1).unwrap();
513 g.add_edge(1, 2).unwrap();
514 g.add_edge(2, 0).unwrap();
515 assert_eq!(count_triangles(&g).unwrap(), 1);
516 assert_eq!(transitivity_undirected(&g).unwrap(), Some(1.0));
517 }
518
519 #[test]
520 fn k4_has_4_triangles_transitivity_1() {
521 let mut g = Graph::with_vertices(4);
522 for u in 0..4u32 {
523 for v in (u + 1)..4 {
524 g.add_edge(u, v).unwrap();
525 }
526 }
527 assert_eq!(count_triangles(&g).unwrap(), 4);
528 assert_eq!(transitivity_undirected(&g).unwrap(), Some(1.0));
529 }
530
531 #[test]
532 fn cycle_4_has_no_triangles_transitivity_zero() {
533 let mut g = Graph::with_vertices(4);
534 for i in 0..4u32 {
535 g.add_edge(i, (i + 1) % 4).unwrap();
536 }
537 assert_eq!(count_triangles(&g).unwrap(), 0);
538 assert_eq!(transitivity_undirected(&g).unwrap(), Some(0.0));
539 }
540
541 #[test]
542 fn star_has_no_triangles_transitivity_zero() {
543 let mut g = Graph::with_vertices(4);
544 for v in 1..4 {
545 g.add_edge(0, v).unwrap();
546 }
547 assert_eq!(count_triangles(&g).unwrap(), 0);
548 assert_eq!(transitivity_undirected(&g).unwrap(), Some(0.0));
550 }
551
552 #[test]
553 fn path_has_one_triple_no_triangle() {
554 let mut g = Graph::with_vertices(3);
556 g.add_edge(0, 1).unwrap();
557 g.add_edge(1, 2).unwrap();
558 assert_eq!(count_triangles(&g).unwrap(), 0);
559 assert_eq!(transitivity_undirected(&g).unwrap(), Some(0.0));
560 }
561
562 #[test]
563 fn self_loop_is_ignored() {
564 let mut g = Graph::with_vertices(3);
565 g.add_edge(0, 0).unwrap(); g.add_edge(0, 1).unwrap();
567 g.add_edge(1, 2).unwrap();
568 g.add_edge(2, 0).unwrap();
569 assert_eq!(count_triangles(&g).unwrap(), 1);
571 assert_eq!(transitivity_undirected(&g).unwrap(), Some(1.0));
572 }
573
574 #[test]
575 fn parallel_edges_are_ignored() {
576 let mut g = Graph::with_vertices(3);
578 g.add_edge(0, 1).unwrap();
579 g.add_edge(0, 1).unwrap(); g.add_edge(1, 2).unwrap();
581 g.add_edge(2, 0).unwrap();
582 assert_eq!(count_triangles(&g).unwrap(), 1);
583 assert_eq!(transitivity_undirected(&g).unwrap(), Some(1.0));
584 }
585
586 #[test]
587 fn two_disjoint_triangles_count_as_two() {
588 let mut g = Graph::with_vertices(6);
589 g.add_edge(0, 1).unwrap();
590 g.add_edge(1, 2).unwrap();
591 g.add_edge(2, 0).unwrap();
592 g.add_edge(3, 4).unwrap();
593 g.add_edge(4, 5).unwrap();
594 g.add_edge(5, 3).unwrap();
595 assert_eq!(count_triangles(&g).unwrap(), 2);
596 assert_eq!(transitivity_undirected(&g).unwrap(), Some(1.0));
598 }
599
600 #[test]
601 fn local_transitivity_triangle_is_all_ones() {
602 let mut g = Graph::with_vertices(3);
603 g.add_edge(0, 1).unwrap();
604 g.add_edge(1, 2).unwrap();
605 g.add_edge(2, 0).unwrap();
606 assert_eq!(
607 transitivity_local_undirected(&g).unwrap(),
608 vec![Some(1.0), Some(1.0), Some(1.0)]
609 );
610 }
611
612 #[test]
613 fn local_transitivity_star_centre_zero_leaves_none() {
614 let mut g = Graph::with_vertices(4);
615 for v in 1..4 {
616 g.add_edge(0, v).unwrap();
617 }
618 assert_eq!(
619 transitivity_local_undirected(&g).unwrap(),
620 vec![Some(0.0), None, None, None]
621 );
622 }
623
624 #[test]
625 fn local_transitivity_isolated_vertices_all_none() {
626 let g = Graph::with_vertices(3);
627 assert_eq!(
628 transitivity_local_undirected(&g).unwrap(),
629 vec![None, None, None]
630 );
631 }
632
633 #[test]
634 fn local_transitivity_diamond_per_vertex() {
635 let mut g = Graph::with_vertices(4);
639 g.add_edge(0, 1).unwrap();
640 g.add_edge(0, 2).unwrap();
641 g.add_edge(1, 2).unwrap();
642 g.add_edge(1, 3).unwrap();
643 g.add_edge(2, 3).unwrap();
644 let r = transitivity_local_undirected(&g).unwrap();
645 assert_eq!(r[0], Some(1.0));
646 assert_eq!(r[3], Some(1.0));
647 let two_thirds = 2.0_f64 / 3.0;
650 assert_eq!(r[1], Some(two_thirds));
651 assert_eq!(r[2], Some(two_thirds));
652 }
653
654 #[test]
655 fn barrat_unit_weights_match_unweighted_on_k4() {
656 let mut g = Graph::with_vertices(4);
659 for u in 0..4u32 {
660 for v in (u + 1)..4 {
661 g.add_edge(u, v).unwrap();
662 }
663 }
664 let r = transitivity_barrat(&g, &[1.0; 6]).unwrap();
665 assert_eq!(r, vec![Some(1.0); 4]);
666 }
667
668 #[test]
669 fn barrat_triangle_unit_weights_all_ones() {
670 let mut g = Graph::with_vertices(3);
671 g.add_edge(0, 1).unwrap();
672 g.add_edge(1, 2).unwrap();
673 g.add_edge(2, 0).unwrap();
674 let r = transitivity_barrat(&g, &[1.0; 3]).unwrap();
675 assert_eq!(r, vec![Some(1.0), Some(1.0), Some(1.0)]);
676 }
677
678 #[test]
679 fn barrat_triangle_unequal_weights_hand_check() {
680 let mut g = Graph::with_vertices(3);
688 g.add_edge(0, 1).unwrap();
689 g.add_edge(1, 2).unwrap();
690 g.add_edge(2, 0).unwrap();
691 let r = transitivity_barrat(&g, &[1.0, 2.0, 4.0]).unwrap();
692 assert_eq!(r, vec![Some(1.0), Some(1.0), Some(1.0)]);
693 }
694
695 #[test]
696 fn barrat_path_no_triangles_yields_zero_inner_none_endpoints() {
697 let mut g = Graph::with_vertices(3);
700 g.add_edge(0, 1).unwrap();
701 g.add_edge(1, 2).unwrap();
702 let r = transitivity_barrat(&g, &[1.0, 1.0]).unwrap();
703 assert_eq!(r, vec![None, Some(0.0), None]);
704 }
705
706 #[test]
707 fn barrat_isolated_vertex_yields_none() {
708 let mut g = Graph::with_vertices(3);
709 g.add_edge(0, 1).unwrap();
710 let r = transitivity_barrat(&g, &[1.0]).unwrap();
711 assert_eq!(r, vec![None, None, None]);
712 }
713
714 #[test]
715 fn barrat_empty_graph_empty_result() {
716 let g = Graph::with_vertices(0);
717 let r = transitivity_barrat(&g, &[]).unwrap();
718 assert!(r.is_empty());
719 }
720
721 #[test]
722 fn barrat_diamond_unit_weights() {
723 let mut g = Graph::with_vertices(4);
726 g.add_edge(0, 1).unwrap();
727 g.add_edge(0, 2).unwrap();
728 g.add_edge(1, 2).unwrap();
729 g.add_edge(1, 3).unwrap();
730 g.add_edge(2, 3).unwrap();
731 let r = transitivity_barrat(&g, &[1.0; 5]).unwrap();
732 let two_thirds = 2.0_f64 / 3.0;
733 assert_eq!(r[0], Some(1.0));
734 assert_eq!(r[3], Some(1.0));
735 match (r[1], r[2]) {
736 (Some(a), Some(b)) => {
737 assert!((a - two_thirds).abs() < 1e-12);
738 assert!((b - two_thirds).abs() < 1e-12);
739 }
740 _ => panic!("middle vertices must be Some"),
741 }
742 }
743
744 #[test]
745 fn barrat_invalid_weight_length_errors() {
746 let mut g = Graph::with_vertices(3);
747 g.add_edge(0, 1).unwrap();
748 g.add_edge(1, 2).unwrap();
749 assert!(transitivity_barrat(&g, &[1.0]).is_err());
750 }
751
752 #[test]
753 fn barrat_negative_weight_errors() {
754 let mut g = Graph::with_vertices(3);
755 g.add_edge(0, 1).unwrap();
756 g.add_edge(1, 2).unwrap();
757 assert!(transitivity_barrat(&g, &[1.0, -1.0]).is_err());
758 }
759
760 #[test]
761 fn barrat_self_loop_rejected() {
762 let mut g = Graph::with_vertices(2);
763 g.add_edge(0, 0).unwrap();
764 g.add_edge(0, 1).unwrap();
765 assert!(transitivity_barrat(&g, &[1.0, 1.0]).is_err());
766 }
767
768 #[test]
769 fn barrat_multi_edge_rejected() {
770 let mut g = Graph::with_vertices(2);
771 g.add_edge(0, 1).unwrap();
772 g.add_edge(0, 1).unwrap();
773 assert!(transitivity_barrat(&g, &[1.0, 1.0]).is_err());
774 }
775
776 #[test]
777 fn barrat_directed_rejected() {
778 let mut g = Graph::new(2, true).unwrap();
779 g.add_edge(0, 1).unwrap();
780 assert!(transitivity_barrat(&g, &[1.0]).is_err());
781 }
782
783 #[test]
786 fn adjacent_triangles_empty_graph() {
787 let g = Graph::with_vertices(0);
788 assert_eq!(count_adjacent_triangles(&g).unwrap(), Vec::<u64>::new());
789 }
790
791 #[test]
792 fn adjacent_triangles_isolated_vertices() {
793 let g = Graph::with_vertices(5);
794 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![0; 5]);
795 }
796
797 #[test]
798 fn adjacent_triangles_single_triangle() {
799 let mut g = Graph::with_vertices(3);
800 g.add_edge(0, 1).unwrap();
801 g.add_edge(1, 2).unwrap();
802 g.add_edge(2, 0).unwrap();
803 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![1, 1, 1]);
804 }
805
806 #[test]
807 fn adjacent_triangles_k4_each_vertex_sees_3() {
808 let mut g = Graph::with_vertices(4);
809 for u in 0..4u32 {
810 for v in (u + 1)..4 {
811 g.add_edge(u, v).unwrap();
812 }
813 }
814 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![3, 3, 3, 3]);
816 }
817
818 #[test]
819 fn adjacent_triangles_diamond_k4_minus_edge() {
820 let mut g = Graph::with_vertices(4);
823 g.add_edge(0, 1).unwrap();
824 g.add_edge(0, 2).unwrap();
825 g.add_edge(1, 2).unwrap();
826 g.add_edge(1, 3).unwrap();
827 g.add_edge(2, 3).unwrap();
828 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![1, 2, 2, 1]);
829 }
830
831 #[test]
832 fn adjacent_triangles_star_all_zero() {
833 let mut g = Graph::with_vertices(5);
834 for v in 1..5u32 {
835 g.add_edge(0, v).unwrap();
836 }
837 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![0; 5]);
838 }
839
840 #[test]
841 fn adjacent_triangles_self_loops_ignored() {
842 let mut g = Graph::with_vertices(3);
843 g.add_edge(0, 0).unwrap();
844 g.add_edge(1, 1).unwrap();
845 g.add_edge(0, 1).unwrap();
846 g.add_edge(1, 2).unwrap();
847 g.add_edge(2, 0).unwrap();
848 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![1, 1, 1]);
850 }
851
852 #[test]
853 fn adjacent_triangles_parallel_edges_ignored() {
854 let mut g = Graph::with_vertices(3);
855 g.add_edge(0, 1).unwrap();
856 g.add_edge(0, 1).unwrap();
857 g.add_edge(1, 2).unwrap();
858 g.add_edge(1, 2).unwrap();
859 g.add_edge(2, 0).unwrap();
860 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![1, 1, 1]);
861 }
862
863 #[test]
864 fn adjacent_triangles_two_disjoint_triangles() {
865 let mut g = Graph::with_vertices(6);
866 g.add_edge(0, 1).unwrap();
867 g.add_edge(1, 2).unwrap();
868 g.add_edge(2, 0).unwrap();
869 g.add_edge(3, 4).unwrap();
870 g.add_edge(4, 5).unwrap();
871 g.add_edge(5, 3).unwrap();
872 assert_eq!(
873 count_adjacent_triangles(&g).unwrap(),
874 vec![1, 1, 1, 1, 1, 1]
875 );
876 }
877
878 #[test]
879 fn adjacent_triangles_sum_equals_three_times_count_triangles() {
880 let mut g = Graph::with_vertices(4);
882 for u in 0..4u32 {
883 for v in (u + 1)..4 {
884 g.add_edge(u, v).unwrap();
885 }
886 }
887 let per_vertex = count_adjacent_triangles(&g).unwrap();
888 let total = count_triangles(&g).unwrap();
889 assert_eq!(per_vertex.iter().sum::<u64>(), 3 * total);
890 }
891
892 #[test]
893 fn adjacent_triangles_consistent_with_local_transitivity() {
894 let mut g = Graph::with_vertices(3);
897 g.add_edge(0, 1).unwrap();
898 g.add_edge(1, 2).unwrap();
899 g.add_edge(2, 0).unwrap();
900 let adj = count_adjacent_triangles(&g).unwrap();
901 let local = transitivity_local_undirected(&g).unwrap();
902 for v in 0..3 {
903 assert_eq!(adj[v], 1);
904 assert_eq!(local[v], Some(1.0));
905 }
906 }
907
908 #[test]
909 fn diamond_k4_minus_edge_transitivity_below_one() {
910 let mut g = Graph::with_vertices(4);
914 g.add_edge(0, 1).unwrap();
915 g.add_edge(0, 2).unwrap();
916 g.add_edge(1, 2).unwrap();
917 g.add_edge(1, 3).unwrap();
918 g.add_edge(2, 3).unwrap();
919 assert_eq!(count_triangles(&g).unwrap(), 2);
920 assert_eq!(transitivity_undirected(&g).unwrap(), Some(0.75));
921 }
922}