use std::cmp::Ordering;
use std::collections::BinaryHeap;
use crate::core::graph::EdgeId;
use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
#[derive(Copy, Clone)]
struct Frontier(f64, VertexId);
impl PartialEq for Frontier {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0 && self.1 == other.1
}
}
impl Eq for Frontier {}
impl Ord for Frontier {
fn cmp(&self, other: &Self) -> Ordering {
other.0.total_cmp(&self.0).then(other.1.cmp(&self.1))
}
}
impl PartialOrd for Frontier {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
pub fn edge_betweenness_weighted(graph: &Graph, weights: &[f64]) -> IgraphResult<Vec<f64>> {
let n = graph.vcount();
let n_us = n as usize;
let m = graph.ecount();
let mut bc = vec![0.0_f64; m];
if n == 0 || m == 0 {
return Ok(bc);
}
if weights.len() != m {
return Err(IgraphError::InvalidArgument(format!(
"weights vector size ({}) differs from edge count ({})",
weights.len(),
m
)));
}
for (e, &w) in weights.iter().enumerate() {
if w.is_nan() || w < 0.0 || !w.is_finite() {
return Err(IgraphError::InvalidArgument(format!(
"weight at edge {e} must be non-negative and finite (got {w})"
)));
}
}
let mut sigma = vec![0.0_f64; n_us];
let mut dist = vec![f64::INFINITY; n_us];
let mut visited = vec![false; 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];
for s in 0..n {
sigma.fill(0.0);
dist.fill(f64::INFINITY);
visited.fill(false);
for slot in &mut pred {
slot.clear();
}
stack.clear();
delta_v.fill(0.0);
sigma[s as usize] = 1.0;
dist[s as usize] = 0.0;
let mut heap: BinaryHeap<Frontier> = BinaryHeap::new();
heap.push(Frontier(0.0, s));
while let Some(Frontier(d, v)) = heap.pop() {
let v_us = v as usize;
if visited[v_us] {
continue;
}
visited[v_us] = true;
stack.push(v);
for eid in graph.incident(v)? {
let w_edge = weights[eid as usize];
let other = graph.edge_other(eid as EdgeId, v)?;
let other_us = other as usize;
let nd = d + w_edge;
match nd.partial_cmp(&dist[other_us]) {
Some(Ordering::Less) => {
dist[other_us] = nd;
sigma[other_us] = sigma[v_us];
pred[other_us].clear();
pred[other_us].push((v, eid as EdgeId));
heap.push(Frontier(nd, other));
}
Some(Ordering::Equal) => {
sigma[other_us] += sigma[v_us];
pred[other_us].push((v, eid as EdgeId));
}
_ => {}
}
}
}
while let Some(w) = stack.pop() {
let w_us = w as usize;
for &(v, e) in &pred[w_us] {
let c = (sigma[v as usize] / sigma[w_us]) * (1.0 + delta_v[w_us]);
delta_v[v as usize] += c;
bc[e as usize] += c;
}
}
}
if !graph.is_directed() {
for slot in &mut bc {
*slot /= 2.0;
}
}
Ok(bc)
}
#[cfg(test)]
mod tests {
use super::*;
fn close(actual: &[f64], expected: &[f64], tol: f64) {
assert_eq!(actual.len(), expected.len(), "length mismatch");
for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
assert!((a - e).abs() < tol, "edge {i}: actual={a} expected={e}");
}
}
#[test]
fn empty_graph_yields_empty() {
let g = Graph::with_vertices(0);
assert!(edge_betweenness_weighted(&g, &[]).unwrap().is_empty());
}
#[test]
fn no_edges_yields_empty() {
let g = Graph::with_vertices(5);
assert!(edge_betweenness_weighted(&g, &[]).unwrap().is_empty());
}
#[test]
fn unit_weights_match_unweighted_path_4() {
let mut g = Graph::with_vertices(4);
for i in 0..3u32 {
g.add_edge(i, i + 1).unwrap();
}
let eb = edge_betweenness_weighted(&g, &[1.0, 1.0, 1.0]).unwrap();
close(&eb, &[3.0, 4.0, 3.0], 1e-12);
}
#[test]
fn weighted_triangle_swaps_route() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(0, 2).unwrap();
let eb = edge_betweenness_weighted(&g, &[1.0, 1.0, 5.0]).unwrap();
close(&eb, &[2.0, 2.0, 0.0], 1e-12);
}
#[test]
fn weighted_triangle_keeps_direct() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(0, 2).unwrap();
let eb = edge_betweenness_weighted(&g, &[5.0, 5.0, 1.0]).unwrap();
close(&eb, &[1.0, 1.0, 1.0], 1e-12);
}
#[test]
fn directed_unit_weights_match_unweighted_path_3() {
let mut g = Graph::new(3, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let eb = edge_betweenness_weighted(&g, &[1.0, 1.0]).unwrap();
close(&eb, &[2.0, 2.0], 1e-12);
}
#[test]
fn k4_unit_weights_each_edge_one() {
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 eb = edge_betweenness_weighted(&g, &[1.0; 6]).unwrap();
close(&eb, &[1.0; 6], 1e-12);
}
#[test]
fn weights_size_mismatch_errors() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
assert!(edge_betweenness_weighted(&g, &[]).is_err());
}
#[test]
fn negative_weight_errors() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
assert!(edge_betweenness_weighted(&g, &[-1.0]).is_err());
}
#[test]
fn nan_weight_errors() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
assert!(edge_betweenness_weighted(&g, &[f64::NAN]).is_err());
}
}