use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
pub fn count_triangles(graph: &Graph) -> IgraphResult<u64> {
let (triangles, _) = triangles_and_triples(graph)?;
Ok(triangles)
}
pub fn count_adjacent_triangles(graph: &Graph) -> IgraphResult<Vec<u64>> {
let (per_vertex_triangles, _) = per_vertex_triangle_stats(graph)?;
Ok(per_vertex_triangles)
}
pub fn transitivity_undirected(graph: &Graph) -> IgraphResult<Option<f64>> {
let (triangles, triples) = triangles_and_triples(graph)?;
if triples == 0 {
return Ok(None);
}
#[allow(clippy::cast_precision_loss)]
let t = (triangles as f64) * 3.0 / (triples as f64);
Ok(Some(t))
}
pub fn transitivity_local_undirected(graph: &Graph) -> IgraphResult<Vec<Option<f64>>> {
let n = graph.vcount();
let n_us = n as usize;
let (per_vertex_triangles, simple_degrees) = per_vertex_triangle_stats(graph)?;
let mut out: Vec<Option<f64>> = Vec::with_capacity(n_us);
for v in 0..n_us {
let d = simple_degrees[v];
if d < 2 {
out.push(None);
continue;
}
let t = per_vertex_triangles[v];
#[allow(clippy::cast_precision_loss)]
let val = 2.0 * (t as f64) / ((d as f64) * ((d - 1) as f64));
out.push(Some(val));
}
Ok(out)
}
fn per_vertex_triangle_stats(graph: &Graph) -> IgraphResult<(Vec<u64>, Vec<u64>)> {
let n = graph.vcount();
let n_us = n as usize;
let mut adj: Vec<Vec<VertexId>> = Vec::with_capacity(n_us);
for v in 0..n {
let raw = graph.neighbors(v)?;
let mut simple: Vec<VertexId> = raw.into_iter().filter(|&u| u != v).collect();
simple.sort_unstable();
simple.dedup();
adj.push(simple);
}
let degrees: Vec<u64> = adj.iter().map(|nei| nei.len() as u64).collect();
let mut tri_counts: Vec<u64> = vec![0; n_us];
let mut mark: Vec<u32> = vec![0; n_us];
for v1 in 0..n {
let nei1 = &adj[v1 as usize];
if nei1.len() < 2 {
continue;
}
let v1_mark = v1
.checked_add(1)
.ok_or(IgraphError::Internal("vertex id overflow"))?;
for &v2 in nei1 {
if v2 >= v1 {
break;
}
mark[v2 as usize] = v1_mark;
}
for &v2 in nei1 {
if v2 >= v1 {
break;
}
let nei2 = &adj[v2 as usize];
for &v3 in nei2 {
if v3 >= v2 {
break;
}
if mark[v3 as usize] == v1_mark {
tri_counts[v1 as usize] += 1;
tri_counts[v2 as usize] += 1;
tri_counts[v3 as usize] += 1;
}
}
}
}
Ok((tri_counts, degrees))
}
pub fn transitivity_barrat(graph: &Graph, weights: &[f64]) -> IgraphResult<Vec<Option<f64>>> {
if graph.is_directed() {
return Err(IgraphError::Internal(
"transitivity_barrat: directed graphs not supported in Phase-1 minimal slice",
));
}
let n = graph.vcount();
let n_us = n as usize;
let m = graph.ecount();
if weights.len() != m {
return Err(IgraphError::Internal(
"weights length does not match ecount in transitivity_barrat",
));
}
for &w in weights {
if !w.is_finite() || w < 0.0 {
return Err(IgraphError::Internal(
"weights must be finite and non-negative in transitivity_barrat",
));
}
}
if !crate::algorithms::properties::is_simple::is_simple(graph)? {
return Err(IgraphError::Internal(
"transitivity_barrat requires a simple graph (no multi-edges, no self-loops)",
));
}
if n_us == 0 {
return Ok(Vec::new());
}
let mut nei_mark: Vec<u64> = vec![0; n_us];
let mut actw: Vec<f64> = vec![0.0; n_us];
let mut out: Vec<Option<f64>> = Vec::with_capacity(n_us);
for v in 0..n {
let i_mark = u64::from(v) + 1;
let inc_v = graph.incident(v)?;
let mut strength = 0.0_f64;
for &e in &inc_v {
let u = graph.edge_other(e, v)?;
nei_mark[u as usize] = i_mark;
actw[u as usize] = weights[e as usize];
strength += weights[e as usize];
}
let deg_v = inc_v.len();
if deg_v < 2 {
out.push(None);
continue;
}
#[allow(clippy::cast_precision_loss)]
let triples = strength * (deg_v as f64 - 1.0);
let mut triangles_w = 0.0_f64;
for &e1 in &inc_v {
let u = graph.edge_other(e1, v)?;
let w1 = weights[e1 as usize];
let inc_u = graph.incident(u)?;
for &e2 in &inc_u {
let u2 = graph.edge_other(e2, u)?;
if nei_mark[u2 as usize] == i_mark {
triangles_w += f64::midpoint(actw[u2 as usize], w1);
}
}
}
if triples > 0.0 {
out.push(Some(triangles_w / triples));
} else {
out.push(None);
}
}
Ok(out)
}
fn triangles_and_triples(graph: &Graph) -> IgraphResult<(u64, u64)> {
let n = graph.vcount();
let n_us = n as usize;
let mut adj: Vec<Vec<VertexId>> = Vec::with_capacity(n_us);
for v in 0..n {
let raw = graph.neighbors(v)?;
let mut simple: Vec<VertexId> = raw.into_iter().filter(|&u| u != v).collect();
simple.sort_unstable();
simple.dedup();
adj.push(simple);
}
let mut mark: Vec<u32> = vec![0; n_us];
let mut triangles: u64 = 0;
let mut triples: u64 = 0;
for v1 in 0..n {
let nei1 = &adj[v1 as usize];
let d1 = nei1.len();
if d1 < 2 {
continue;
}
triples = triples.saturating_add((d1 as u64) * ((d1 - 1) as u64) / 2);
let v1_mark = v1
.checked_add(1)
.ok_or(IgraphError::Internal("vertex id overflow"))?;
for &v2 in nei1 {
if v2 >= v1 {
break;
}
mark[v2 as usize] = v1_mark;
}
for &v2 in nei1 {
if v2 >= v1 {
break;
}
let nei2 = &adj[v2 as usize];
for &v3 in nei2 {
if v3 >= v2 {
break;
}
if mark[v3 as usize] == v1_mark {
triangles = triangles.saturating_add(1);
}
}
}
}
Ok((triangles, triples))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_graph_has_no_triangles_or_transitivity() {
let g = Graph::with_vertices(0);
assert_eq!(count_triangles(&g).unwrap(), 0);
assert_eq!(transitivity_undirected(&g).unwrap(), None);
}
#[test]
fn isolated_vertices_give_no_triples() {
let g = Graph::with_vertices(5);
assert_eq!(count_triangles(&g).unwrap(), 0);
assert_eq!(transitivity_undirected(&g).unwrap(), None);
}
#[test]
fn triangle_count_is_one_transitivity_is_one() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
assert_eq!(count_triangles(&g).unwrap(), 1);
assert_eq!(transitivity_undirected(&g).unwrap(), Some(1.0));
}
#[test]
fn k4_has_4_triangles_transitivity_1() {
let mut g = Graph::with_vertices(4);
for u in 0..4u32 {
for v in (u + 1)..4 {
g.add_edge(u, v).unwrap();
}
}
assert_eq!(count_triangles(&g).unwrap(), 4);
assert_eq!(transitivity_undirected(&g).unwrap(), Some(1.0));
}
#[test]
fn cycle_4_has_no_triangles_transitivity_zero() {
let mut g = Graph::with_vertices(4);
for i in 0..4u32 {
g.add_edge(i, (i + 1) % 4).unwrap();
}
assert_eq!(count_triangles(&g).unwrap(), 0);
assert_eq!(transitivity_undirected(&g).unwrap(), Some(0.0));
}
#[test]
fn star_has_no_triangles_transitivity_zero() {
let mut g = Graph::with_vertices(4);
for v in 1..4 {
g.add_edge(0, v).unwrap();
}
assert_eq!(count_triangles(&g).unwrap(), 0);
assert_eq!(transitivity_undirected(&g).unwrap(), Some(0.0));
}
#[test]
fn path_has_one_triple_no_triangle() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
assert_eq!(count_triangles(&g).unwrap(), 0);
assert_eq!(transitivity_undirected(&g).unwrap(), Some(0.0));
}
#[test]
fn self_loop_is_ignored() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 0).unwrap(); g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
assert_eq!(count_triangles(&g).unwrap(), 1);
assert_eq!(transitivity_undirected(&g).unwrap(), Some(1.0));
}
#[test]
fn parallel_edges_are_ignored() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 1).unwrap(); g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
assert_eq!(count_triangles(&g).unwrap(), 1);
assert_eq!(transitivity_undirected(&g).unwrap(), Some(1.0));
}
#[test]
fn two_disjoint_triangles_count_as_two() {
let mut g = Graph::with_vertices(6);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
g.add_edge(3, 4).unwrap();
g.add_edge(4, 5).unwrap();
g.add_edge(5, 3).unwrap();
assert_eq!(count_triangles(&g).unwrap(), 2);
assert_eq!(transitivity_undirected(&g).unwrap(), Some(1.0));
}
#[test]
fn local_transitivity_triangle_is_all_ones() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
assert_eq!(
transitivity_local_undirected(&g).unwrap(),
vec![Some(1.0), Some(1.0), Some(1.0)]
);
}
#[test]
fn local_transitivity_star_centre_zero_leaves_none() {
let mut g = Graph::with_vertices(4);
for v in 1..4 {
g.add_edge(0, v).unwrap();
}
assert_eq!(
transitivity_local_undirected(&g).unwrap(),
vec![Some(0.0), None, None, None]
);
}
#[test]
fn local_transitivity_isolated_vertices_all_none() {
let g = Graph::with_vertices(3);
assert_eq!(
transitivity_local_undirected(&g).unwrap(),
vec![None, None, None]
);
}
#[test]
fn local_transitivity_diamond_per_vertex() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 2).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(1, 3).unwrap();
g.add_edge(2, 3).unwrap();
let r = transitivity_local_undirected(&g).unwrap();
assert_eq!(r[0], Some(1.0));
assert_eq!(r[3], Some(1.0));
let two_thirds = 2.0_f64 / 3.0;
assert_eq!(r[1], Some(two_thirds));
assert_eq!(r[2], Some(two_thirds));
}
#[test]
fn barrat_unit_weights_match_unweighted_on_k4() {
let mut g = Graph::with_vertices(4);
for u in 0..4u32 {
for v in (u + 1)..4 {
g.add_edge(u, v).unwrap();
}
}
let r = transitivity_barrat(&g, &[1.0; 6]).unwrap();
assert_eq!(r, vec![Some(1.0); 4]);
}
#[test]
fn barrat_triangle_unit_weights_all_ones() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
let r = transitivity_barrat(&g, &[1.0; 3]).unwrap();
assert_eq!(r, vec![Some(1.0), Some(1.0), Some(1.0)]);
}
#[test]
fn barrat_triangle_unequal_weights_hand_check() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
let r = transitivity_barrat(&g, &[1.0, 2.0, 4.0]).unwrap();
assert_eq!(r, vec![Some(1.0), Some(1.0), Some(1.0)]);
}
#[test]
fn barrat_path_no_triangles_yields_zero_inner_none_endpoints() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let r = transitivity_barrat(&g, &[1.0, 1.0]).unwrap();
assert_eq!(r, vec![None, Some(0.0), None]);
}
#[test]
fn barrat_isolated_vertex_yields_none() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
let r = transitivity_barrat(&g, &[1.0]).unwrap();
assert_eq!(r, vec![None, None, None]);
}
#[test]
fn barrat_empty_graph_empty_result() {
let g = Graph::with_vertices(0);
let r = transitivity_barrat(&g, &[]).unwrap();
assert!(r.is_empty());
}
#[test]
fn barrat_diamond_unit_weights() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 2).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(1, 3).unwrap();
g.add_edge(2, 3).unwrap();
let r = transitivity_barrat(&g, &[1.0; 5]).unwrap();
let two_thirds = 2.0_f64 / 3.0;
assert_eq!(r[0], Some(1.0));
assert_eq!(r[3], Some(1.0));
match (r[1], r[2]) {
(Some(a), Some(b)) => {
assert!((a - two_thirds).abs() < 1e-12);
assert!((b - two_thirds).abs() < 1e-12);
}
_ => panic!("middle vertices must be Some"),
}
}
#[test]
fn barrat_invalid_weight_length_errors() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
assert!(transitivity_barrat(&g, &[1.0]).is_err());
}
#[test]
fn barrat_negative_weight_errors() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
assert!(transitivity_barrat(&g, &[1.0, -1.0]).is_err());
}
#[test]
fn barrat_self_loop_rejected() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 0).unwrap();
g.add_edge(0, 1).unwrap();
assert!(transitivity_barrat(&g, &[1.0, 1.0]).is_err());
}
#[test]
fn barrat_multi_edge_rejected() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 1).unwrap();
assert!(transitivity_barrat(&g, &[1.0, 1.0]).is_err());
}
#[test]
fn barrat_directed_rejected() {
let mut g = Graph::new(2, true).unwrap();
g.add_edge(0, 1).unwrap();
assert!(transitivity_barrat(&g, &[1.0]).is_err());
}
#[test]
fn adjacent_triangles_empty_graph() {
let g = Graph::with_vertices(0);
assert_eq!(count_adjacent_triangles(&g).unwrap(), Vec::<u64>::new());
}
#[test]
fn adjacent_triangles_isolated_vertices() {
let g = Graph::with_vertices(5);
assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![0; 5]);
}
#[test]
fn adjacent_triangles_single_triangle() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![1, 1, 1]);
}
#[test]
fn adjacent_triangles_k4_each_vertex_sees_3() {
let mut g = Graph::with_vertices(4);
for u in 0..4u32 {
for v in (u + 1)..4 {
g.add_edge(u, v).unwrap();
}
}
assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![3, 3, 3, 3]);
}
#[test]
fn adjacent_triangles_diamond_k4_minus_edge() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 2).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(1, 3).unwrap();
g.add_edge(2, 3).unwrap();
assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![1, 2, 2, 1]);
}
#[test]
fn adjacent_triangles_star_all_zero() {
let mut g = Graph::with_vertices(5);
for v in 1..5u32 {
g.add_edge(0, v).unwrap();
}
assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![0; 5]);
}
#[test]
fn adjacent_triangles_self_loops_ignored() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 0).unwrap();
g.add_edge(1, 1).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![1, 1, 1]);
}
#[test]
fn adjacent_triangles_parallel_edges_ignored() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
assert_eq!(count_adjacent_triangles(&g).unwrap(), vec![1, 1, 1]);
}
#[test]
fn adjacent_triangles_two_disjoint_triangles() {
let mut g = Graph::with_vertices(6);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
g.add_edge(3, 4).unwrap();
g.add_edge(4, 5).unwrap();
g.add_edge(5, 3).unwrap();
assert_eq!(
count_adjacent_triangles(&g).unwrap(),
vec![1, 1, 1, 1, 1, 1]
);
}
#[test]
fn adjacent_triangles_sum_equals_three_times_count_triangles() {
let mut g = Graph::with_vertices(4);
for u in 0..4u32 {
for v in (u + 1)..4 {
g.add_edge(u, v).unwrap();
}
}
let per_vertex = count_adjacent_triangles(&g).unwrap();
let total = count_triangles(&g).unwrap();
assert_eq!(per_vertex.iter().sum::<u64>(), 3 * total);
}
#[test]
fn adjacent_triangles_consistent_with_local_transitivity() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
let adj = count_adjacent_triangles(&g).unwrap();
let local = transitivity_local_undirected(&g).unwrap();
for v in 0..3 {
assert_eq!(adj[v], 1);
assert_eq!(local[v], Some(1.0));
}
}
#[test]
fn diamond_k4_minus_edge_transitivity_below_one() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 2).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(1, 3).unwrap();
g.add_edge(2, 3).unwrap();
assert_eq!(count_triangles(&g).unwrap(), 2);
assert_eq!(transitivity_undirected(&g).unwrap(), Some(0.75));
}
}