#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::float_cmp,
clippy::items_after_statements,
clippy::many_single_char_names,
clippy::needless_range_loop,
clippy::too_many_lines
)]
use std::collections::VecDeque;
use crate::algorithms::community::modularity::{modularity, modularity_directed};
use crate::core::graph::EdgeId;
use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
#[derive(Debug, Clone)]
pub struct EdgeBetweennessResult {
pub membership: Vec<u32>,
pub nb_clusters: u32,
pub removed_edges: Vec<EdgeId>,
pub edge_betweenness: Vec<f64>,
pub merges: Vec<[u32; 2]>,
pub bridges: Vec<u32>,
pub modularity: Vec<f64>,
}
pub fn edge_betweenness_community(graph: &Graph) -> IgraphResult<EdgeBetweennessResult> {
let directed = graph.is_directed();
let n = graph.vcount();
let m_us = graph.ecount();
let n_us = n as usize;
if n == 0 {
return Ok(EdgeBetweennessResult {
membership: Vec::new(),
nb_clusters: 0,
removed_edges: Vec::new(),
edge_betweenness: Vec::new(),
merges: Vec::new(),
bridges: Vec::new(),
modularity: Vec::new(),
});
}
if m_us == 0 {
return Ok(EdgeBetweennessResult {
membership: (0..n).collect(),
nb_clusters: n,
removed_edges: Vec::new(),
edge_betweenness: Vec::new(),
merges: Vec::new(),
bridges: Vec::new(),
modularity: vec![0.0],
});
}
let mut inc_out: Vec<Vec<EdgeId>> = (0..n)
.map(|v| graph.incident(v))
.collect::<IgraphResult<Vec<_>>>()?;
let mut inc_in: Vec<Vec<EdgeId>> = if directed {
(0..n)
.map(|v| graph.incident_in(v))
.collect::<IgraphResult<Vec<_>>>()?
} else {
Vec::new()
};
let mut passive: Vec<bool> = vec![false; m_us];
let mut removed_edges: Vec<EdgeId> = Vec::with_capacity(m_us);
let mut edge_betweenness_history: Vec<f64> = Vec::with_capacity(m_us);
let mut sigma = vec![0.0_f64; n_us];
let mut dist = vec![-1_i64; n_us];
let mut pred: Vec<Vec<(VertexId, EdgeId)>> = vec![Vec::new(); n_us];
let mut stack: Vec<VertexId> = Vec::with_capacity(n_us);
let mut delta_v = vec![0.0_f64; n_us];
let mut eb_now = vec![0.0_f64; m_us];
let mut queue: VecDeque<VertexId> = VecDeque::with_capacity(n_us);
for _ in 0..m_us {
eb_now.fill(0.0);
for s in 0..n {
sigma.fill(0.0);
dist.fill(-1);
for slot in &mut pred {
slot.clear();
}
stack.clear();
delta_v.fill(0.0);
queue.clear();
sigma[s as usize] = 1.0;
dist[s as usize] = 0;
queue.push_back(s);
while let Some(v) = queue.pop_front() {
stack.push(v);
for &e in &inc_out[v as usize] {
let w = if directed {
let (_from, to) = graph.edge(e)?;
to
} else {
graph.edge_other(e, v)?
};
if dist[w as usize] < 0 {
queue.push_back(w);
dist[w as usize] = dist[v as usize] + 1;
}
if dist[w as usize] == dist[v as usize] + 1 {
sigma[w as usize] += sigma[v as usize];
pred[w as usize].push((v, e));
}
}
}
while let Some(w) = stack.pop() {
for &(v, e) in &pred[w as usize] {
let c = (sigma[v as usize] / sigma[w as usize]) * (1.0 + delta_v[w as usize]);
delta_v[v as usize] += c;
eb_now[e as usize] += c;
}
}
}
let mut max_eid: Option<EdgeId> = None;
let mut max_val = f64::NEG_INFINITY;
for e in 0..m_us {
if passive[e] {
continue;
}
let val = eb_now[e];
if val > max_val {
max_val = val;
max_eid = Some(e as EdgeId);
}
}
let eid = max_eid.ok_or(IgraphError::Internal(
"edge_betweenness_community: no active edge to remove",
))?;
removed_edges.push(eid);
edge_betweenness_history.push(if directed { max_val } else { max_val / 2.0 });
passive[eid as usize] = true;
let (from, to) = graph.edge(eid)?;
if directed {
inc_out[from as usize].retain(|&e| e != eid);
inc_in[to as usize].retain(|&e| e != eid);
} else {
for endpoint in [from, to] {
inc_out[endpoint as usize].retain(|&e| e != eid);
}
}
}
let mut membership_now: Vec<u32> = (0..n).collect();
let mut merges: Vec<[u32; 2]> = Vec::new();
let mut bridges: Vec<u32> = Vec::new();
let mut modularity_levels: Vec<f64> = Vec::new();
let level_q = |mem: &[u32]| -> IgraphResult<f64> {
let opt = if directed {
modularity_directed(graph, mem, 1.0)?
} else {
modularity(graph, mem, 1.0)?
};
Ok(opt.unwrap_or(0.0))
};
let q0 = level_q(&membership_now)?;
modularity_levels.push(q0);
let mut max_mod = q0;
let mut best_membership: Vec<u32> = membership_now.clone();
for (step, &eid) in removed_edges.iter().enumerate().rev() {
let (from, to) = graph.edge(eid)?;
let c1 = membership_now[from as usize];
let c2 = membership_now[to as usize];
if c1 == c2 {
continue;
}
let merge_index = merges.len();
let new_cluster = n
.checked_add(merge_index as u32)
.ok_or(IgraphError::Internal(
"edge_betweenness_community: merge index overflow",
))?;
merges.push([c1, c2]);
bridges.push(step as u32);
for slot in &mut membership_now {
if *slot == c1 || *slot == c2 {
*slot = new_cluster;
}
}
let q = level_q(&membership_now)?;
modularity_levels.push(q);
if q > max_mod {
max_mod = q;
best_membership.clone_from(&membership_now);
}
}
let (membership_dense, nb_clusters) = densify_labels(&best_membership);
Ok(EdgeBetweennessResult {
membership: membership_dense,
nb_clusters,
removed_edges,
edge_betweenness: edge_betweenness_history,
merges,
bridges,
modularity: modularity_levels,
})
}
fn densify_labels(labels: &[u32]) -> (Vec<u32>, u32) {
let mut remap: Vec<(u32, u32)> = Vec::new();
let mut out: Vec<u32> = Vec::with_capacity(labels.len());
for &lbl in labels {
let dense = if let Some(&(_, d)) = remap.iter().find(|(orig, _)| *orig == lbl) {
d
} else {
let d = remap.len() as u32;
remap.push((lbl, d));
d
};
out.push(dense);
}
let n_clusters = remap.len() as u32;
(out, n_clusters)
}
#[cfg(test)]
mod tests {
use super::*;
fn well_formed(r: &EdgeBetweennessResult, n: u32, m: usize) {
assert_eq!(r.membership.len() as u32, n, "membership length");
assert_eq!(r.removed_edges.len(), m, "removed_edges length");
assert_eq!(r.edge_betweenness.len(), m, "history length");
assert_eq!(r.merges.len(), r.bridges.len(), "merges/bridges");
assert_eq!(
r.modularity.len(),
r.merges.len() + 1,
"modularity = merges + 1"
);
for &lbl in &r.membership {
assert!(lbl < r.nb_clusters, "dense label in range");
}
}
#[test]
fn empty_graph_returns_empty_result() {
let g = Graph::with_vertices(0);
let r = edge_betweenness_community(&g).unwrap();
assert_eq!(r.nb_clusters, 0);
assert!(r.removed_edges.is_empty());
assert!(r.modularity.is_empty());
}
#[test]
fn edgeless_graph_returns_singletons() {
let g = Graph::with_vertices(4);
let r = edge_betweenness_community(&g).unwrap();
assert_eq!(r.nb_clusters, 4);
for v in 0..4 {
assert_eq!(r.membership[v as usize], v);
}
assert!(r.removed_edges.is_empty());
assert_eq!(r.modularity, vec![0.0]);
}
#[test]
fn two_triangles_bridge_splits_into_two() {
let mut g = Graph::with_vertices(6);
for &(u, v) in &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)] {
g.add_edge(u, v).unwrap();
}
let r = edge_betweenness_community(&g).unwrap();
well_formed(&r, 6, 7);
assert_eq!(r.nb_clusters, 2);
assert_eq!(r.membership[0], r.membership[1]);
assert_eq!(r.membership[1], r.membership[2]);
assert_eq!(r.membership[3], r.membership[4]);
assert_eq!(r.membership[4], r.membership[5]);
assert_ne!(r.membership[0], r.membership[3]);
let (from0, to0) = g.edge(r.removed_edges[0]).unwrap();
assert!(
(from0, to0) == (2, 3) || (from0, to0) == (3, 2),
"first removed must be the bridge, got ({from0}, {to0})"
);
}
#[test]
fn path_4_splits_at_middle_edge() {
let mut g = Graph::with_vertices(4);
for i in 0..3u32 {
g.add_edge(i, i + 1).unwrap();
}
let r = edge_betweenness_community(&g).unwrap();
well_formed(&r, 4, 3);
let (from0, to0) = g.edge(r.removed_edges[0]).unwrap();
assert!((from0, to0) == (1, 2) || (from0, to0) == (2, 1));
assert_eq!(r.membership[0], r.membership[1]);
assert_eq!(r.membership[2], r.membership[3]);
assert_ne!(r.membership[0], r.membership[2]);
}
#[test]
fn triangle_modularity_levels_are_in_range() {
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 = edge_betweenness_community(&g).unwrap();
well_formed(&r, 3, 3);
for &q in &r.modularity {
assert!((-0.501..=0.001).contains(&q), "modularity {q} out of range");
}
}
#[test]
fn two_components_already_split_no_bridges_between_them() {
let mut g = Graph::with_vertices(5);
g.add_edge(0, 1).unwrap();
g.add_edge(2, 3).unwrap();
g.add_edge(3, 4).unwrap();
let r = edge_betweenness_community(&g).unwrap();
well_formed(&r, 5, 3);
assert!(r.nb_clusters >= 2);
assert_eq!(r.membership[0], r.membership[1]);
assert_eq!(r.membership[2], r.membership[3]);
assert_eq!(r.membership[3], r.membership[4]);
assert_ne!(r.membership[0], r.membership[2]);
}
#[test]
fn dendrogram_merges_at_most_n_minus_components() {
let mut g = Graph::with_vertices(4);
for i in 0..4u32 {
g.add_edge(i, (i + 1) % 4).unwrap();
}
let r = edge_betweenness_community(&g).unwrap();
assert!(r.merges.len() <= 3);
well_formed(&r, 4, 4);
}
#[test]
fn directed_path_6_middle_edge_first_split() {
let mut g = Graph::new(6, true).unwrap();
for i in 0..5u32 {
g.add_edge(i, i + 1).unwrap();
}
let r = edge_betweenness_community(&g).unwrap();
well_formed(&r, 6, 5);
let (from0, to0) = g.edge(r.removed_edges[0]).unwrap();
assert_eq!((from0, to0), (2, 3), "bridge edge must be removed first");
assert_eq!(r.nb_clusters, 2);
assert_eq!(r.membership[0], r.membership[1]);
assert_eq!(r.membership[1], r.membership[2]);
assert_eq!(r.membership[3], r.membership[4]);
assert_eq!(r.membership[4], r.membership[5]);
assert_ne!(r.membership[0], r.membership[3]);
let best = r
.modularity
.iter()
.copied()
.fold(f64::NEG_INFINITY, f64::max);
assert!(
(best - 8.0 / 25.0).abs() < 1e-9,
"expected best directed Q ≈ 8/25, got {best}"
);
}
#[test]
fn directed_two_triangles_bridge_runs_cleanly() {
let mut g = Graph::new(6, true).unwrap();
for &(u, v) in &[(0, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3), (2, 3)] {
g.add_edge(u, v).unwrap();
}
let r = edge_betweenness_community(&g).unwrap();
well_formed(&r, 6, 7);
for &q in &r.modularity {
assert!(q.is_finite(), "directed modularity is finite");
assert!((-1.0..=1.0).contains(&q), "directed Q in plausible range");
}
}
#[test]
fn directed_betweenness_is_not_halved() {
let mut g = Graph::new(4, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 3).unwrap();
let r = edge_betweenness_community(&g).unwrap();
well_formed(&r, 4, 3);
let (from0, to0) = g.edge(r.removed_edges[0]).unwrap();
assert_eq!((from0, to0), (1, 2));
assert!(
(r.edge_betweenness[0] - 4.0).abs() < 1e-9,
"expected unhalved eb=4.0, got {}",
r.edge_betweenness[0]
);
}
#[test]
fn directed_disconnected_components_yield_singletons_or_components() {
let mut g = Graph::new(5, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(2, 3).unwrap();
g.add_edge(3, 4).unwrap();
let r = edge_betweenness_community(&g).unwrap();
well_formed(&r, 5, 3);
assert!(r.nb_clusters >= 2);
assert_ne!(r.membership[0], r.membership[2]);
}
#[test]
fn densify_labels_preserves_first_appearance_order() {
let (dense, n) = densify_labels(&[7, 7, 4, 4, 7, 9, 4, 9]);
assert_eq!(n, 3);
assert_eq!(dense, vec![0, 0, 1, 1, 0, 2, 1, 2]);
}
}