use crate::KnowledgeGraph;
use petgraph::graph::NodeIndex;
use std::collections::{HashMap, VecDeque};
#[derive(Debug, Clone, Copy)]
pub struct BetweennessConfig {
pub normalized: bool,
pub undirected: bool,
}
impl Default for BetweennessConfig {
fn default() -> Self {
Self {
normalized: true,
undirected: false,
}
}
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn betweenness_centrality(
kg: &KnowledgeGraph,
config: BetweennessConfig,
) -> HashMap<String, f64> {
let graph = kg.as_petgraph();
let n = graph.node_count();
if n < 2 {
return graph
.node_indices()
.map(|idx| (graph[idx].id.as_str().to_owned(), 0.0))
.collect();
}
let mut betweenness = vec![0.0_f64; n];
for s in graph.node_indices() {
let (sigma, predecessors, order) = bfs_shortest_paths(graph, s, config.undirected);
let mut delta = vec![0.0_f64; n];
for &w in order.iter().rev() {
let w_idx = w.index();
for &v in &predecessors[w_idx] {
let v_idx = v.index();
let coeff = sigma[v_idx] / sigma[w_idx];
delta[v_idx] += coeff * (1.0 + delta[w_idx]);
}
if w != s {
betweenness[w_idx] += delta[w_idx];
}
}
}
if config.undirected {
for b in &mut betweenness {
*b /= 2.0;
}
}
if config.normalized && n > 2 {
let norm = ((n - 1) * (n - 2)) as f64;
for b in &mut betweenness {
*b /= norm;
}
}
graph
.node_indices()
.map(|idx| (graph[idx].id.as_str().to_owned(), betweenness[idx.index()]))
.collect()
}
#[allow(clippy::cast_precision_loss)]
fn bfs_shortest_paths(
graph: &petgraph::Graph<crate::Entity, crate::Relation>,
source: NodeIndex,
undirected: bool,
) -> (Vec<f64>, Vec<Vec<NodeIndex>>, Vec<NodeIndex>) {
let n = graph.node_count();
let mut sigma = vec![0.0_f64; n]; let mut dist = vec![-1_i32; n]; let mut predecessors: Vec<Vec<NodeIndex>> = vec![Vec::new(); n];
let mut order = Vec::with_capacity(n);
sigma[source.index()] = 1.0;
dist[source.index()] = 0;
let mut queue = VecDeque::new();
queue.push_back(source);
while let Some(v) = queue.pop_front() {
order.push(v);
let v_idx = v.index();
let v_dist = dist[v_idx];
let neighbors: Vec<NodeIndex> = if undirected {
graph.neighbors_undirected(v).collect()
} else {
graph
.neighbors_directed(v, petgraph::Direction::Outgoing)
.collect()
};
for w in neighbors {
let w_idx = w.index();
if dist[w_idx] < 0 {
dist[w_idx] = v_dist + 1;
queue.push_back(w);
}
if dist[w_idx] == v_dist + 1 {
sigma[w_idx] += sigma[v_idx];
predecessors[w_idx].push(v);
}
}
}
(sigma, predecessors, order)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Triple;
#[test]
fn test_betweenness_line() {
let mut kg = KnowledgeGraph::new();
kg.add_triple(Triple::new("A", "rel", "B"));
kg.add_triple(Triple::new("B", "rel", "C"));
kg.add_triple(Triple::new("C", "rel", "D"));
let config = BetweennessConfig {
normalized: false,
undirected: false,
};
let scores = betweenness_centrality(&kg, config);
assert_eq!(*scores.get("A").unwrap(), 0.0);
assert_eq!(*scores.get("D").unwrap(), 0.0);
assert!(*scores.get("B").unwrap() > 0.0);
assert!(*scores.get("C").unwrap() > 0.0);
}
#[test]
fn test_betweenness_star() {
let mut kg = KnowledgeGraph::new();
kg.add_triple(Triple::new("Hub", "rel", "A"));
kg.add_triple(Triple::new("Hub", "rel", "B"));
kg.add_triple(Triple::new("Hub", "rel", "C"));
let config = BetweennessConfig::default();
let scores = betweenness_centrality(&kg, config);
for score in scores.values() {
assert_eq!(
*score, 0.0,
"Star nodes have no betweenness in directed graph"
);
}
}
#[test]
fn test_betweenness_bridge() {
let mut kg = KnowledgeGraph::new();
kg.add_triple(Triple::new("A", "rel", "B"));
kg.add_triple(Triple::new("B", "rel", "A"));
kg.add_triple(Triple::new("B", "rel", "C"));
kg.add_triple(Triple::new("C", "rel", "D"));
kg.add_triple(Triple::new("D", "rel", "C"));
let config = BetweennessConfig {
normalized: false,
undirected: false,
};
let scores = betweenness_centrality(&kg, config);
let b = *scores.get("B").unwrap();
let c = *scores.get("C").unwrap();
let a = *scores.get("A").unwrap();
let d = *scores.get("D").unwrap();
assert!(
b > a && c > d,
"Bridge nodes should have higher betweenness: B={b}, C={c}, A={a}, D={d}"
);
}
#[test]
fn test_betweenness_undirected() {
let mut kg = KnowledgeGraph::new();
kg.add_triple(Triple::new("A", "rel", "B"));
kg.add_triple(Triple::new("B", "rel", "C"));
let config = BetweennessConfig {
normalized: false,
undirected: true,
};
let scores = betweenness_centrality(&kg, config);
let b = *scores.get("B").unwrap();
assert!(b > 0.0, "B should be on shortest paths: {b}");
}
}