use std::collections::VecDeque;
use crate::algorithms::paths::distances::distances;
use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
pub fn global_efficiency(graph: &Graph) -> IgraphResult<Option<f64>> {
let n = graph.vcount();
if n < 2 {
return Ok(None);
}
let mut sum_inv: f64 = 0.0;
for v in 0..n {
let d = distances(graph, v)?;
let v_us = v as usize;
for (target, &val) in d.iter().enumerate() {
if target == v_us {
continue;
}
if let Some(dist) = val {
if dist > 0 {
sum_inv += 1.0 / f64::from(dist);
}
}
}
}
let n_f = f64::from(n);
let denom = n_f * (n_f - 1.0);
Ok(Some(sum_inv / denom))
}
pub fn local_efficiency(graph: &Graph) -> IgraphResult<Vec<f64>> {
let n = graph.vcount();
let n_us = n as usize;
let mut result = vec![0.0_f64; n_us];
if n < 3 {
return Ok(result);
}
let mut nei_mask = vec![false; n_us];
for v in 0..n {
let raw = graph.neighbors(v)?;
let mut neighbors: Vec<VertexId> = raw.into_iter().filter(|&u| u != v).collect();
neighbors.sort_unstable();
neighbors.dedup();
let k = neighbors.len();
if k < 2 {
continue;
}
for &u in &neighbors {
nei_mask[u as usize] = true;
}
let mut sum_inv: f64 = 0.0;
for &source in &neighbors {
let mut dist: Vec<Option<u32>> = vec![None; n_us];
dist[source as usize] = Some(0);
let mut queue: VecDeque<VertexId> = VecDeque::new();
queue.push_back(source);
let mut reached = 0_usize;
'bfs: while let Some(node) = queue.pop_front() {
let cur = dist[node as usize].ok_or(IgraphError::Internal(
"dequeued unvisited vertex in local_efficiency BFS",
))?;
if node != source && nei_mask[node as usize] && cur > 0 {
sum_inv += 1.0 / f64::from(cur);
reached += 1;
if reached + 1 == k {
break 'bfs;
}
}
let next = cur.checked_add(1).ok_or(IgraphError::Internal(
"distance overflow in local_efficiency",
))?;
for w in graph.neighbors(node)? {
if w == v {
continue;
}
if dist[w as usize].is_none() {
dist[w as usize] = Some(next);
queue.push_back(w);
}
}
}
}
for &u in &neighbors {
nei_mask[u as usize] = false;
}
let k_u32 = u32::try_from(k)
.map_err(|_| IgraphError::Internal("neighbour count exceeds u32::MAX"))?;
let k_f = f64::from(k_u32);
result[v as usize] = sum_inv / (k_f * (k_f - 1.0));
}
Ok(result)
}
pub fn average_local_efficiency(graph: &Graph) -> IgraphResult<f64> {
let n = graph.vcount();
if n < 3 {
return Ok(0.0);
}
let local = local_efficiency(graph)?;
let n_f = f64::from(n);
Ok(local.iter().sum::<f64>() / n_f)
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: f64, b: f64, tol: f64) {
assert!((a - b).abs() < tol, "{a} vs {b}");
}
#[test]
fn empty_graph_returns_none() {
let g = Graph::with_vertices(0);
assert_eq!(global_efficiency(&g).unwrap(), None);
}
#[test]
fn singleton_returns_none() {
let g = Graph::with_vertices(1);
assert_eq!(global_efficiency(&g).unwrap(), None);
}
#[test]
fn no_edges_two_vertices_zero() {
let g = Graph::with_vertices(2);
assert_eq!(global_efficiency(&g).unwrap(), Some(0.0));
}
#[test]
fn complete_graph_one() {
for n in 2..=5u32 {
let mut g = Graph::with_vertices(n);
for u in 0..n {
for v in (u + 1)..n {
g.add_edge(u, v).unwrap();
}
}
close(global_efficiency(&g).unwrap().unwrap(), 1.0, 1e-12);
}
}
#[test]
fn path_3_two_thirds() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let e = global_efficiency(&g).unwrap().unwrap();
close(e, 5.0 / 6.0, 1e-12);
}
#[test]
fn path_4_thirteen_eighteenths() {
let mut g = Graph::with_vertices(4);
for i in 0..3u32 {
g.add_edge(i, i + 1).unwrap();
}
let e = global_efficiency(&g).unwrap().unwrap();
close(e, 13.0 / 18.0, 1e-12);
}
#[test]
fn isolated_vertices_zero() {
let g = Graph::with_vertices(3);
assert_eq!(global_efficiency(&g).unwrap(), Some(0.0));
}
#[test]
fn disconnected_two_components() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
let e = global_efficiency(&g).unwrap().unwrap();
close(e, 1.0 / 3.0, 1e-12);
}
#[test]
fn directed_path_uses_out_edges() {
let mut g = Graph::new(3, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let e = global_efficiency(&g).unwrap().unwrap();
close(e, 5.0 / 12.0, 1e-12);
}
#[test]
fn star_efficiency() {
let mut g = Graph::with_vertices(4);
for v in 1..4u32 {
g.add_edge(0, v).unwrap();
}
let e = global_efficiency(&g).unwrap().unwrap();
close(e, 0.75, 1e-12);
}
#[test]
fn matches_harmonic_centrality_average() {
let mut g = Graph::with_vertices(5);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 2).unwrap();
g.add_edge(2, 3).unwrap();
g.add_edge(3, 4).unwrap();
let e = global_efficiency(&g).unwrap().unwrap();
let h = crate::algorithms::properties::harmonic::harmonic_centrality(&g).unwrap();
let avg: f64 = h.iter().sum::<f64>() / f64::from(u32::try_from(h.len()).unwrap());
close(e, avg, 1e-12);
}
#[test]
fn efficiency_in_range() {
let mut g = Graph::with_vertices(6);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 3).unwrap();
g.add_edge(3, 4).unwrap();
g.add_edge(4, 5).unwrap();
g.add_edge(0, 5).unwrap(); let e = global_efficiency(&g).unwrap().unwrap();
assert!((0.0..=1.0).contains(&e), "{e}");
}
#[test]
fn local_eff_empty_graph_empty_vec() {
let g = Graph::with_vertices(0);
assert!(local_efficiency(&g).unwrap().is_empty());
}
#[test]
fn local_eff_singleton_zero() {
let g = Graph::with_vertices(1);
assert_eq!(local_efficiency(&g).unwrap(), vec![0.0]);
}
#[test]
fn local_eff_two_vertices_zero() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
assert_eq!(local_efficiency(&g).unwrap(), vec![0.0, 0.0]);
}
#[test]
fn local_eff_isolated_three_vertices_zero() {
let g = Graph::with_vertices(3);
assert_eq!(local_efficiency(&g).unwrap(), vec![0.0, 0.0, 0.0]);
}
#[test]
fn local_eff_path_three_zero() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
assert_eq!(local_efficiency(&g).unwrap(), vec![0.0, 0.0, 0.0]);
}
#[test]
fn local_eff_triangle_zero() {
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 local = local_efficiency(&g).unwrap();
for v in &local {
close(*v, 1.0, 1e-12);
}
}
#[test]
fn local_eff_k4_all_one() {
let mut g = Graph::with_vertices(4);
for i in 0..4u32 {
for j in (i + 1)..4u32 {
g.add_edge(i, j).unwrap();
}
}
let local = local_efficiency(&g).unwrap();
for v in &local {
close(*v, 1.0, 1e-12);
}
}
#[test]
fn local_eff_star_zero() {
let mut g = Graph::with_vertices(4);
for v in 1..4u32 {
g.add_edge(0, v).unwrap();
}
assert_eq!(local_efficiency(&g).unwrap(), vec![0.0, 0.0, 0.0, 0.0]);
}
#[test]
fn local_eff_diamond() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 2).unwrap();
g.add_edge(0, 3).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 3).unwrap();
let local = local_efficiency(&g).unwrap();
close(local[0], 5.0 / 6.0, 1e-12);
close(local[1], 1.0, 1e-12);
close(local[2], 5.0 / 6.0, 1e-12);
close(local[3], 1.0, 1e-12);
}
#[test]
fn local_eff_ignores_self_loops_and_parallel_edges() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 0).unwrap(); g.add_edge(0, 1).unwrap();
g.add_edge(0, 1).unwrap(); g.add_edge(0, 2).unwrap();
g.add_edge(1, 2).unwrap();
let local = local_efficiency(&g).unwrap();
for v in &local {
close(*v, 1.0, 1e-12);
}
}
#[test]
fn average_local_eff_empty_zero() {
let g = Graph::with_vertices(0);
close(average_local_efficiency(&g).unwrap(), 0.0, 1e-12);
}
#[test]
fn average_local_eff_lt_three_zero() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
close(average_local_efficiency(&g).unwrap(), 0.0, 1e-12);
}
#[test]
fn average_local_eff_k4_one() {
let mut g = Graph::with_vertices(4);
for i in 0..4u32 {
for j in (i + 1)..4u32 {
g.add_edge(i, j).unwrap();
}
}
close(average_local_efficiency(&g).unwrap(), 1.0, 1e-12);
}
#[test]
fn average_local_eff_diamond() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 2).unwrap();
g.add_edge(0, 3).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 3).unwrap();
close(average_local_efficiency(&g).unwrap(), 11.0 / 12.0, 1e-12);
}
#[test]
fn average_local_eff_path_zero() {
let mut g = Graph::with_vertices(4);
for i in 0..3u32 {
g.add_edge(i, i + 1).unwrap();
}
close(average_local_efficiency(&g).unwrap(), 0.0, 1e-12);
}
}