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
175fn per_vertex_triangle_stats(graph: &Graph) -> IgraphResult<(Vec<u64>, Vec<u64>)> {
178 let n = graph.vcount();
179 let n_us = n as usize;
180
181 let mut adj: Vec<Vec<VertexId>> = Vec::with_capacity(n_us);
182 for v in 0..n {
183 let raw = graph.neighbors(v)?;
184 let mut simple: Vec<VertexId> = raw.into_iter().filter(|&u| u != v).collect();
185 simple.sort_unstable();
186 simple.dedup();
187 adj.push(simple);
188 }
189
190 let degrees: Vec<u64> = adj.iter().map(|nei| nei.len() as u64).collect();
191 let mut tri_counts: Vec<u64> = vec![0; n_us];
192 let mut mark: Vec<u32> = vec![0; n_us];
193
194 for v1 in 0..n {
195 let nei1 = &adj[v1 as usize];
196 if nei1.len() < 2 {
197 continue;
198 }
199 let v1_mark = v1
200 .checked_add(1)
201 .ok_or(IgraphError::Internal("vertex id overflow"))?;
202 for &v2 in nei1 {
203 if v2 >= v1 {
204 break;
205 }
206 mark[v2 as usize] = v1_mark;
207 }
208 for &v2 in nei1 {
209 if v2 >= v1 {
210 break;
211 }
212 let nei2 = &adj[v2 as usize];
213 for &v3 in nei2 {
214 if v3 >= v2 {
215 break;
216 }
217 if mark[v3 as usize] == v1_mark {
218 tri_counts[v1 as usize] += 1;
219 tri_counts[v2 as usize] += 1;
220 tri_counts[v3 as usize] += 1;
221 }
222 }
223 }
224 }
225
226 Ok((tri_counts, degrees))
227}
228
229pub fn transitivity_barrat(graph: &Graph, weights: &[f64]) -> IgraphResult<Vec<Option<f64>>> {
268 if graph.is_directed() {
269 return Err(IgraphError::Internal(
270 "transitivity_barrat: directed graphs not supported in Phase-1 minimal slice",
271 ));
272 }
273 let n = graph.vcount();
274 let n_us = n as usize;
275 let m = graph.ecount();
276 if weights.len() != m {
277 return Err(IgraphError::Internal(
278 "weights length does not match ecount in transitivity_barrat",
279 ));
280 }
281 for &w in weights {
282 if !w.is_finite() || w < 0.0 {
283 return Err(IgraphError::Internal(
284 "weights must be finite and non-negative in transitivity_barrat",
285 ));
286 }
287 }
288 if !crate::algorithms::properties::is_simple::is_simple(graph)? {
292 return Err(IgraphError::Internal(
293 "transitivity_barrat requires a simple graph (no multi-edges, no self-loops)",
294 ));
295 }
296
297 if n_us == 0 {
298 return Ok(Vec::new());
299 }
300
301 let mut nei_mark: Vec<u64> = vec![0; n_us];
306 let mut actw: Vec<f64> = vec![0.0; n_us];
307 let mut out: Vec<Option<f64>> = Vec::with_capacity(n_us);
308
309 for v in 0..n {
310 let i_mark = u64::from(v) + 1;
311 let inc_v = graph.incident(v)?;
312 let mut strength = 0.0_f64;
313 for &e in &inc_v {
314 let u = graph.edge_other(e, v)?;
315 nei_mark[u as usize] = i_mark;
316 actw[u as usize] = weights[e as usize];
317 strength += weights[e as usize];
318 }
319 let deg_v = inc_v.len();
320 if deg_v < 2 {
321 out.push(None);
322 continue;
323 }
324 #[allow(clippy::cast_precision_loss)]
327 let triples = strength * (deg_v as f64 - 1.0);
328 let mut triangles_w = 0.0_f64;
329
330 for &e1 in &inc_v {
331 let u = graph.edge_other(e1, v)?;
332 let w1 = weights[e1 as usize];
333 let inc_u = graph.incident(u)?;
334 for &e2 in &inc_u {
335 let u2 = graph.edge_other(e2, u)?;
336 if nei_mark[u2 as usize] == i_mark {
337 triangles_w += f64::midpoint(actw[u2 as usize], w1);
338 }
339 }
340 }
341
342 if triples > 0.0 {
343 out.push(Some(triangles_w / triples));
344 } else {
345 out.push(None);
346 }
347 }
348
349 Ok(out)
350}
351
352fn triangles_and_triples(graph: &Graph) -> IgraphResult<(u64, u64)> {
353 let n = graph.vcount();
354 let n_us = n as usize;
355
356 let mut adj: Vec<Vec<VertexId>> = Vec::with_capacity(n_us);
359 for v in 0..n {
360 let raw = graph.neighbors(v)?;
361 let mut simple: Vec<VertexId> = raw.into_iter().filter(|&u| u != v).collect();
362 simple.sort_unstable();
363 simple.dedup();
364 adj.push(simple);
365 }
366
367 let mut mark: Vec<u32> = vec![0; n_us];
370 let mut triangles: u64 = 0;
371 let mut triples: u64 = 0;
372
373 for v1 in 0..n {
374 let nei1 = &adj[v1 as usize];
375 let d1 = nei1.len();
376 if d1 < 2 {
377 continue;
378 }
379 triples = triples.saturating_add((d1 as u64) * ((d1 - 1) as u64) / 2);
381
382 let v1_mark = v1
384 .checked_add(1)
385 .ok_or(IgraphError::Internal("vertex id overflow"))?;
386 for &v2 in nei1 {
387 if v2 >= v1 {
388 break;
389 }
390 mark[v2 as usize] = v1_mark;
391 }
392
393 for &v2 in nei1 {
395 if v2 >= v1 {
396 break;
397 }
398 let nei2 = &adj[v2 as usize];
399 for &v3 in nei2 {
400 if v3 >= v2 {
401 break;
402 }
403 if mark[v3 as usize] == v1_mark {
404 triangles = triangles.saturating_add(1);
405 }
406 }
407 }
408 }
409
410 Ok((triangles, triples))
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416
417 #[test]
418 fn empty_graph_has_no_triangles_or_transitivity() {
419 let g = Graph::with_vertices(0);
420 assert_eq!(count_triangles(&g).unwrap(), 0);
421 assert_eq!(transitivity_undirected(&g).unwrap(), None);
422 }
423
424 #[test]
425 fn isolated_vertices_give_no_triples() {
426 let g = Graph::with_vertices(5);
427 assert_eq!(count_triangles(&g).unwrap(), 0);
428 assert_eq!(transitivity_undirected(&g).unwrap(), None);
429 }
430
431 #[test]
432 fn triangle_count_is_one_transitivity_is_one() {
433 let mut g = Graph::with_vertices(3);
434 g.add_edge(0, 1).unwrap();
435 g.add_edge(1, 2).unwrap();
436 g.add_edge(2, 0).unwrap();
437 assert_eq!(count_triangles(&g).unwrap(), 1);
438 assert_eq!(transitivity_undirected(&g).unwrap(), Some(1.0));
439 }
440
441 #[test]
442 fn k4_has_4_triangles_transitivity_1() {
443 let mut g = Graph::with_vertices(4);
444 for u in 0..4u32 {
445 for v in (u + 1)..4 {
446 g.add_edge(u, v).unwrap();
447 }
448 }
449 assert_eq!(count_triangles(&g).unwrap(), 4);
450 assert_eq!(transitivity_undirected(&g).unwrap(), Some(1.0));
451 }
452
453 #[test]
454 fn cycle_4_has_no_triangles_transitivity_zero() {
455 let mut g = Graph::with_vertices(4);
456 for i in 0..4u32 {
457 g.add_edge(i, (i + 1) % 4).unwrap();
458 }
459 assert_eq!(count_triangles(&g).unwrap(), 0);
460 assert_eq!(transitivity_undirected(&g).unwrap(), Some(0.0));
461 }
462
463 #[test]
464 fn star_has_no_triangles_transitivity_zero() {
465 let mut g = Graph::with_vertices(4);
466 for v in 1..4 {
467 g.add_edge(0, v).unwrap();
468 }
469 assert_eq!(count_triangles(&g).unwrap(), 0);
470 assert_eq!(transitivity_undirected(&g).unwrap(), Some(0.0));
472 }
473
474 #[test]
475 fn path_has_one_triple_no_triangle() {
476 let mut g = Graph::with_vertices(3);
478 g.add_edge(0, 1).unwrap();
479 g.add_edge(1, 2).unwrap();
480 assert_eq!(count_triangles(&g).unwrap(), 0);
481 assert_eq!(transitivity_undirected(&g).unwrap(), Some(0.0));
482 }
483
484 #[test]
485 fn self_loop_is_ignored() {
486 let mut g = Graph::with_vertices(3);
487 g.add_edge(0, 0).unwrap(); g.add_edge(0, 1).unwrap();
489 g.add_edge(1, 2).unwrap();
490 g.add_edge(2, 0).unwrap();
491 assert_eq!(count_triangles(&g).unwrap(), 1);
493 assert_eq!(transitivity_undirected(&g).unwrap(), Some(1.0));
494 }
495
496 #[test]
497 fn parallel_edges_are_ignored() {
498 let mut g = Graph::with_vertices(3);
500 g.add_edge(0, 1).unwrap();
501 g.add_edge(0, 1).unwrap(); g.add_edge(1, 2).unwrap();
503 g.add_edge(2, 0).unwrap();
504 assert_eq!(count_triangles(&g).unwrap(), 1);
505 assert_eq!(transitivity_undirected(&g).unwrap(), Some(1.0));
506 }
507
508 #[test]
509 fn two_disjoint_triangles_count_as_two() {
510 let mut g = Graph::with_vertices(6);
511 g.add_edge(0, 1).unwrap();
512 g.add_edge(1, 2).unwrap();
513 g.add_edge(2, 0).unwrap();
514 g.add_edge(3, 4).unwrap();
515 g.add_edge(4, 5).unwrap();
516 g.add_edge(5, 3).unwrap();
517 assert_eq!(count_triangles(&g).unwrap(), 2);
518 assert_eq!(transitivity_undirected(&g).unwrap(), Some(1.0));
520 }
521
522 #[test]
523 fn local_transitivity_triangle_is_all_ones() {
524 let mut g = Graph::with_vertices(3);
525 g.add_edge(0, 1).unwrap();
526 g.add_edge(1, 2).unwrap();
527 g.add_edge(2, 0).unwrap();
528 assert_eq!(
529 transitivity_local_undirected(&g).unwrap(),
530 vec![Some(1.0), Some(1.0), Some(1.0)]
531 );
532 }
533
534 #[test]
535 fn local_transitivity_star_centre_zero_leaves_none() {
536 let mut g = Graph::with_vertices(4);
537 for v in 1..4 {
538 g.add_edge(0, v).unwrap();
539 }
540 assert_eq!(
541 transitivity_local_undirected(&g).unwrap(),
542 vec![Some(0.0), None, None, None]
543 );
544 }
545
546 #[test]
547 fn local_transitivity_isolated_vertices_all_none() {
548 let g = Graph::with_vertices(3);
549 assert_eq!(
550 transitivity_local_undirected(&g).unwrap(),
551 vec![None, None, None]
552 );
553 }
554
555 #[test]
556 fn local_transitivity_diamond_per_vertex() {
557 let mut g = Graph::with_vertices(4);
561 g.add_edge(0, 1).unwrap();
562 g.add_edge(0, 2).unwrap();
563 g.add_edge(1, 2).unwrap();
564 g.add_edge(1, 3).unwrap();
565 g.add_edge(2, 3).unwrap();
566 let r = transitivity_local_undirected(&g).unwrap();
567 assert_eq!(r[0], Some(1.0));
568 assert_eq!(r[3], Some(1.0));
569 let two_thirds = 2.0_f64 / 3.0;
572 assert_eq!(r[1], Some(two_thirds));
573 assert_eq!(r[2], Some(two_thirds));
574 }
575
576 #[test]
577 fn barrat_unit_weights_match_unweighted_on_k4() {
578 let mut g = Graph::with_vertices(4);
581 for u in 0..4u32 {
582 for v in (u + 1)..4 {
583 g.add_edge(u, v).unwrap();
584 }
585 }
586 let r = transitivity_barrat(&g, &[1.0; 6]).unwrap();
587 assert_eq!(r, vec![Some(1.0); 4]);
588 }
589
590 #[test]
591 fn barrat_triangle_unit_weights_all_ones() {
592 let mut g = Graph::with_vertices(3);
593 g.add_edge(0, 1).unwrap();
594 g.add_edge(1, 2).unwrap();
595 g.add_edge(2, 0).unwrap();
596 let r = transitivity_barrat(&g, &[1.0; 3]).unwrap();
597 assert_eq!(r, vec![Some(1.0), Some(1.0), Some(1.0)]);
598 }
599
600 #[test]
601 fn barrat_triangle_unequal_weights_hand_check() {
602 let mut g = Graph::with_vertices(3);
610 g.add_edge(0, 1).unwrap();
611 g.add_edge(1, 2).unwrap();
612 g.add_edge(2, 0).unwrap();
613 let r = transitivity_barrat(&g, &[1.0, 2.0, 4.0]).unwrap();
614 assert_eq!(r, vec![Some(1.0), Some(1.0), Some(1.0)]);
615 }
616
617 #[test]
618 fn barrat_path_no_triangles_yields_zero_inner_none_endpoints() {
619 let mut g = Graph::with_vertices(3);
622 g.add_edge(0, 1).unwrap();
623 g.add_edge(1, 2).unwrap();
624 let r = transitivity_barrat(&g, &[1.0, 1.0]).unwrap();
625 assert_eq!(r, vec![None, Some(0.0), None]);
626 }
627
628 #[test]
629 fn barrat_isolated_vertex_yields_none() {
630 let mut g = Graph::with_vertices(3);
631 g.add_edge(0, 1).unwrap();
632 let r = transitivity_barrat(&g, &[1.0]).unwrap();
633 assert_eq!(r, vec![None, None, None]);
634 }
635
636 #[test]
637 fn barrat_empty_graph_empty_result() {
638 let g = Graph::with_vertices(0);
639 let r = transitivity_barrat(&g, &[]).unwrap();
640 assert!(r.is_empty());
641 }
642
643 #[test]
644 fn barrat_diamond_unit_weights() {
645 let mut g = Graph::with_vertices(4);
648 g.add_edge(0, 1).unwrap();
649 g.add_edge(0, 2).unwrap();
650 g.add_edge(1, 2).unwrap();
651 g.add_edge(1, 3).unwrap();
652 g.add_edge(2, 3).unwrap();
653 let r = transitivity_barrat(&g, &[1.0; 5]).unwrap();
654 let two_thirds = 2.0_f64 / 3.0;
655 assert_eq!(r[0], Some(1.0));
656 assert_eq!(r[3], Some(1.0));
657 match (r[1], r[2]) {
658 (Some(a), Some(b)) => {
659 assert!((a - two_thirds).abs() < 1e-12);
660 assert!((b - two_thirds).abs() < 1e-12);
661 }
662 _ => panic!("middle vertices must be Some"),
663 }
664 }
665
666 #[test]
667 fn barrat_invalid_weight_length_errors() {
668 let mut g = Graph::with_vertices(3);
669 g.add_edge(0, 1).unwrap();
670 g.add_edge(1, 2).unwrap();
671 assert!(transitivity_barrat(&g, &[1.0]).is_err());
672 }
673
674 #[test]
675 fn barrat_negative_weight_errors() {
676 let mut g = Graph::with_vertices(3);
677 g.add_edge(0, 1).unwrap();
678 g.add_edge(1, 2).unwrap();
679 assert!(transitivity_barrat(&g, &[1.0, -1.0]).is_err());
680 }
681
682 #[test]
683 fn barrat_self_loop_rejected() {
684 let mut g = Graph::with_vertices(2);
685 g.add_edge(0, 0).unwrap();
686 g.add_edge(0, 1).unwrap();
687 assert!(transitivity_barrat(&g, &[1.0, 1.0]).is_err());
688 }
689
690 #[test]
691 fn barrat_multi_edge_rejected() {
692 let mut g = Graph::with_vertices(2);
693 g.add_edge(0, 1).unwrap();
694 g.add_edge(0, 1).unwrap();
695 assert!(transitivity_barrat(&g, &[1.0, 1.0]).is_err());
696 }
697
698 #[test]
699 fn barrat_directed_rejected() {
700 let mut g = Graph::new(2, true).unwrap();
701 g.add_edge(0, 1).unwrap();
702 assert!(transitivity_barrat(&g, &[1.0]).is_err());
703 }
704
705 #[test]
708 fn adjacent_triangles_empty_graph() {
709 let g = Graph::with_vertices(0);
710 assert_eq!(count_adjacent_triangles(&g).unwrap(), Vec::<u64>::new());
711 }
712
713 #[test]
714 fn adjacent_triangles_isolated_vertices() {
715 let g = Graph::with_vertices(5);
716 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![0; 5]);
717 }
718
719 #[test]
720 fn adjacent_triangles_single_triangle() {
721 let mut g = Graph::with_vertices(3);
722 g.add_edge(0, 1).unwrap();
723 g.add_edge(1, 2).unwrap();
724 g.add_edge(2, 0).unwrap();
725 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![1, 1, 1]);
726 }
727
728 #[test]
729 fn adjacent_triangles_k4_each_vertex_sees_3() {
730 let mut g = Graph::with_vertices(4);
731 for u in 0..4u32 {
732 for v in (u + 1)..4 {
733 g.add_edge(u, v).unwrap();
734 }
735 }
736 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![3, 3, 3, 3]);
738 }
739
740 #[test]
741 fn adjacent_triangles_diamond_k4_minus_edge() {
742 let mut g = Graph::with_vertices(4);
745 g.add_edge(0, 1).unwrap();
746 g.add_edge(0, 2).unwrap();
747 g.add_edge(1, 2).unwrap();
748 g.add_edge(1, 3).unwrap();
749 g.add_edge(2, 3).unwrap();
750 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![1, 2, 2, 1]);
751 }
752
753 #[test]
754 fn adjacent_triangles_star_all_zero() {
755 let mut g = Graph::with_vertices(5);
756 for v in 1..5u32 {
757 g.add_edge(0, v).unwrap();
758 }
759 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![0; 5]);
760 }
761
762 #[test]
763 fn adjacent_triangles_self_loops_ignored() {
764 let mut g = Graph::with_vertices(3);
765 g.add_edge(0, 0).unwrap();
766 g.add_edge(1, 1).unwrap();
767 g.add_edge(0, 1).unwrap();
768 g.add_edge(1, 2).unwrap();
769 g.add_edge(2, 0).unwrap();
770 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![1, 1, 1]);
772 }
773
774 #[test]
775 fn adjacent_triangles_parallel_edges_ignored() {
776 let mut g = Graph::with_vertices(3);
777 g.add_edge(0, 1).unwrap();
778 g.add_edge(0, 1).unwrap();
779 g.add_edge(1, 2).unwrap();
780 g.add_edge(1, 2).unwrap();
781 g.add_edge(2, 0).unwrap();
782 assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![1, 1, 1]);
783 }
784
785 #[test]
786 fn adjacent_triangles_two_disjoint_triangles() {
787 let mut g = Graph::with_vertices(6);
788 g.add_edge(0, 1).unwrap();
789 g.add_edge(1, 2).unwrap();
790 g.add_edge(2, 0).unwrap();
791 g.add_edge(3, 4).unwrap();
792 g.add_edge(4, 5).unwrap();
793 g.add_edge(5, 3).unwrap();
794 assert_eq!(
795 count_adjacent_triangles(&g).unwrap(),
796 vec![1, 1, 1, 1, 1, 1]
797 );
798 }
799
800 #[test]
801 fn adjacent_triangles_sum_equals_three_times_count_triangles() {
802 let mut g = Graph::with_vertices(4);
804 for u in 0..4u32 {
805 for v in (u + 1)..4 {
806 g.add_edge(u, v).unwrap();
807 }
808 }
809 let per_vertex = count_adjacent_triangles(&g).unwrap();
810 let total = count_triangles(&g).unwrap();
811 assert_eq!(per_vertex.iter().sum::<u64>(), 3 * total);
812 }
813
814 #[test]
815 fn adjacent_triangles_consistent_with_local_transitivity() {
816 let mut g = Graph::with_vertices(3);
819 g.add_edge(0, 1).unwrap();
820 g.add_edge(1, 2).unwrap();
821 g.add_edge(2, 0).unwrap();
822 let adj = count_adjacent_triangles(&g).unwrap();
823 let local = transitivity_local_undirected(&g).unwrap();
824 for v in 0..3 {
825 assert_eq!(adj[v], 1);
826 assert_eq!(local[v], Some(1.0));
827 }
828 }
829
830 #[test]
831 fn diamond_k4_minus_edge_transitivity_below_one() {
832 let mut g = Graph::with_vertices(4);
836 g.add_edge(0, 1).unwrap();
837 g.add_edge(0, 2).unwrap();
838 g.add_edge(1, 2).unwrap();
839 g.add_edge(1, 3).unwrap();
840 g.add_edge(2, 3).unwrap();
841 assert_eq!(count_triangles(&g).unwrap(), 2);
842 assert_eq!(transitivity_undirected(&g).unwrap(), Some(0.75));
843 }
844}