#[cfg(feature = "petgraph")]
use petgraph::prelude::*;
#[cfg(feature = "petgraph")]
pub fn betweenness_centrality<N, E, Ix>(graph: &petgraph::Graph<N, E, Directed, Ix>) -> Vec<f64>
where
Ix: petgraph::graph::IndexType,
{
let n = graph.node_count();
if n <= 2 {
return vec![0.0; n];
}
let mut betweenness = vec![0.0; n];
for s in graph.node_indices() {
let mut stack: Vec<NodeIndex<Ix>> = Vec::new();
let mut pred: Vec<Vec<NodeIndex<Ix>>> = vec![vec![]; n];
let mut sigma = vec![0.0f64; n];
let mut dist: Vec<i32> = vec![-1; n];
sigma[s.index()] = 1.0;
dist[s.index()] = 0;
let mut queue: std::collections::VecDeque<NodeIndex<Ix>> =
std::collections::VecDeque::new();
queue.push_back(s);
while let Some(v) = queue.pop_front() {
stack.push(v);
for w in graph.neighbors_directed(v, Direction::Outgoing) {
if dist[w.index()] < 0 {
dist[w.index()] = dist[v.index()] + 1;
queue.push_back(w);
}
if dist[w.index()] == dist[v.index()] + 1 {
sigma[w.index()] += sigma[v.index()];
pred[w.index()].push(v);
}
}
}
let mut delta = vec![0.0f64; n];
while let Some(w) = stack.pop() {
for &v in &pred[w.index()] {
let sigma_w = sigma[w.index()];
if sigma_w > 0.0 {
delta[v.index()] += (sigma[v.index()] / sigma_w) * (1.0 + delta[w.index()]);
}
}
if w != s {
betweenness[w.index()] += delta[w.index()];
}
}
}
let norm = 1.0 / ((n - 1) * (n - 2)) as f64;
for b in &mut betweenness {
*b *= norm;
}
betweenness
}
use crate::graph::GraphRef;
use crate::{Error, Result};
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NewmanBetweennessConfig {
pub n_sources: usize,
pub seed: u64,
pub max_iter: usize,
pub tolerance: f64,
}
impl Default for NewmanBetweennessConfig {
fn default() -> Self {
Self {
n_sources: usize::MAX,
seed: 42,
max_iter: 200,
tolerance: 1e-6,
}
}
}
impl NewmanBetweennessConfig {
pub fn validate(&self) -> Result<()> {
if self.n_sources == 0 {
return Err(Error::InvalidParameter("n_sources must be > 0".to_string()));
}
if self.max_iter == 0 {
return Err(Error::InvalidParameter("max_iter must be > 0".to_string()));
}
if !self.tolerance.is_finite() || self.tolerance <= 0.0 {
return Err(Error::InvalidParameter(
"tolerance must be finite and > 0".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NewmanBetweennessRun {
pub scores: Vec<f64>,
pub iterations: usize,
pub converged: bool,
}
pub fn newman_betweenness<G: GraphRef>(graph: &G, config: NewmanBetweennessConfig) -> Vec<f64> {
newman_betweenness_run(graph, config).scores
}
pub fn newman_betweenness_run<G: GraphRef>(
graph: &G,
config: NewmanBetweennessConfig,
) -> NewmanBetweennessRun {
let n = graph.node_count();
if n < 3 {
return NewmanBetweennessRun {
scores: vec![0.0; n],
iterations: 0,
converged: true,
};
}
let neighbors: Vec<&[usize]> = (0..n).map(|u| graph.neighbors_ref(u)).collect();
let degree: Vec<usize> = (0..n).map(|u| neighbors[u].len()).collect();
let sources: Vec<usize> = if config.n_sources >= n {
(0..n).collect()
} else {
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
let mut rng = ChaCha8Rng::seed_from_u64(config.seed);
reservoir_sample_nodes(n, config.n_sources, &mut rng)
};
let n_sources_actual = sources.len();
let mut scores = vec![0.0f64; n];
let mut total_iters = 0usize;
let mut all_converged = true;
let pinned = n - 1;
let mut v = vec![0.0f64; n]; let mut v_new = vec![0.0f64; n];
for &s in &sources {
for t in 0..n {
if t == s {
continue;
}
v.fill(0.0);
let mut solve_iters = 0usize;
let mut solve_converged = false;
for _ in 0..config.max_iter {
solve_iters += 1;
v_new.fill(0.0);
for i in 0..n {
if i == pinned || degree[i] == 0 {
continue;
}
let b_i: f64 = if i == s {
1.0
} else if i == t {
-1.0
} else {
0.0
};
let neighbor_sum: f64 = neighbors[i].iter().map(|&j| v[j]).sum();
v_new[i] = (b_i + neighbor_sum) / degree[i] as f64;
}
#[cfg(feature = "simd")]
let residual: f64 = innr::dense_f64::l1_distance_f64(&v, &v_new);
#[cfg(not(feature = "simd"))]
let residual: f64 = v
.iter()
.zip(v_new.iter())
.map(|(old, new)| (old - new).abs())
.sum();
std::mem::swap(&mut v, &mut v_new);
if residual < config.tolerance {
solve_converged = true;
break;
}
}
total_iters += solve_iters;
if !solve_converged {
all_converged = false;
}
for k in 0..n {
let mut flow_k = 0.0f64;
for &j in neighbors[k] {
flow_k += (v[k] - v[j]).abs();
}
scores[k] += 0.5 * flow_k;
}
}
}
let scale = if config.n_sources >= n {
1.0 / ((n - 1) * (n - 2)) as f64
} else {
(n as f64 / n_sources_actual as f64) / ((n - 1) * (n - 2)) as f64
};
for s in &mut scores {
*s *= scale;
}
NewmanBetweennessRun {
scores,
iterations: total_iters,
converged: all_converged,
}
}
pub fn newman_betweenness_checked<G: GraphRef>(
graph: &G,
config: NewmanBetweennessConfig,
) -> Result<Vec<f64>> {
config.validate()?;
Ok(newman_betweenness(graph, config))
}
fn reservoir_sample_nodes<R: rand::Rng>(n: usize, k: usize, rng: &mut R) -> Vec<usize> {
let k = k.min(n);
let mut reservoir: Vec<usize> = (0..k).collect();
for i in k..n {
let j = rng.random_range(0..=i);
if j < k {
reservoir[j] = i;
}
}
reservoir
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::GraphRef;
struct VG(Vec<Vec<usize>>);
impl GraphRef for VG {
fn node_count(&self) -> usize {
self.0.len()
}
fn neighbors_ref(&self, node: usize) -> &[usize] {
&self.0[node]
}
}
#[cfg(feature = "petgraph")]
#[test]
fn line_graph_middle_is_highest() {
use petgraph::prelude::*;
let mut g: DiGraph<(), ()> = DiGraph::new();
let a = g.add_node(());
let b = g.add_node(());
let c = g.add_node(());
let d = g.add_node(());
g.add_edge(a, b, ());
g.add_edge(b, c, ());
g.add_edge(c, d, ());
let bc = betweenness_centrality(&g);
assert_eq!(bc[a.index()], 0.0);
assert_eq!(bc[d.index()], 0.0);
assert!(bc[b.index()] > 0.0, "b={}", bc[b.index()]);
assert!(bc[c.index()] > 0.0, "c={}", bc[c.index()]);
}
#[test]
fn newman_line_4_nodes_inner_outscores_outer() {
let g = VG(vec![
vec![1], vec![0, 2], vec![1, 3], vec![2], ]);
let scores = newman_betweenness(&g, NewmanBetweennessConfig::default());
assert_eq!(scores.len(), 4);
assert!(
scores[1] > scores[0],
"scores[1]={} should exceed scores[0]={}",
scores[1],
scores[0]
);
assert!(
scores[2] > scores[3],
"scores[2]={} should exceed scores[3]={}",
scores[2],
scores[3]
);
}
#[test]
fn newman_star_5_nodes_center_highest() {
let g = VG(vec![
vec![1, 2, 3, 4, 5], vec![0], vec![0], vec![0], vec![0], vec![0], ]);
let scores = newman_betweenness(&g, NewmanBetweennessConfig::default());
assert_eq!(scores.len(), 6);
for i in 1..6 {
assert!(
scores[0] > scores[i],
"center scores[0]={} should exceed leaf scores[{}]={}",
scores[0],
i,
scores[i]
);
}
}
#[test]
fn newman_n_sources_zero_rejected_by_checked() {
let g = VG(vec![vec![1], vec![0], vec![1, 3], vec![2]]);
let cfg = NewmanBetweennessConfig {
n_sources: 0,
..NewmanBetweennessConfig::default()
};
assert!(
newman_betweenness_checked(&g, cfg).is_err(),
"n_sources=0 should return Err"
);
}
mod proptest_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn proptest_scores_finite_nonneg(
n in 1usize..10,
edges in proptest::collection::vec((0usize..10, 0usize..10), 0..30),
) {
let mut adj = vec![vec![]; n];
for (u, v) in edges {
if u < n && v < n && u != v {
adj[u].push(v);
adj[v].push(u);
}
}
for row in &mut adj {
row.sort_unstable();
row.dedup();
}
let g = VG(adj);
let scores = newman_betweenness(&g, NewmanBetweennessConfig::default());
prop_assert_eq!(scores.len(), n);
for &s in &scores {
prop_assert!(s.is_finite(), "score is not finite: {s}");
prop_assert!(s >= 0.0, "score is negative: {s}");
}
}
}
}
}