use crate::KnowledgeGraph;
use petgraph::graph::NodeIndex;
use std::collections::{HashMap, VecDeque};
#[derive(Debug, Clone, Copy)]
pub struct ClosenessConfig {
pub normalized: bool,
pub undirected: bool,
pub harmonic: bool,
}
impl Default for ClosenessConfig {
fn default() -> Self {
Self {
normalized: true,
undirected: false,
harmonic: true, }
}
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn closeness_centrality(kg: &KnowledgeGraph, config: ClosenessConfig) -> 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 result = HashMap::with_capacity(n);
for source in graph.node_indices() {
let distances = bfs_distances(graph, source, config.undirected);
let closeness = if config.harmonic {
let sum: f64 = distances
.iter()
.enumerate()
.filter(|(i, &d)| *i != source.index() && d > 0)
.map(|(_, &d)| 1.0 / d as f64)
.sum();
sum
} else {
let reachable: Vec<_> = distances
.iter()
.enumerate()
.filter(|(i, &d)| *i != source.index() && d > 0)
.collect();
if reachable.is_empty() {
0.0
} else {
let total_dist: i32 = reachable.iter().map(|(_, &d)| d).sum();
(reachable.len() as f64) / (total_dist as f64)
}
};
let normalized_closeness = if config.normalized {
closeness / (n - 1) as f64
} else {
closeness
};
let entity = &graph[source];
result.insert(entity.id.as_str().to_owned(), normalized_closeness);
}
result
}
fn bfs_distances(
graph: &petgraph::Graph<crate::Entity, crate::Relation>,
source: NodeIndex,
undirected: bool,
) -> Vec<i32> {
let n = graph.node_count();
let mut dist = vec![-1_i32; n];
dist[source.index()] = 0;
let mut queue = VecDeque::new();
queue.push_back(source);
while let Some(v) = queue.pop_front() {
let v_dist = dist[v.index()];
let neighbors: Vec<NodeIndex> = if undirected {
graph.neighbors_undirected(v).collect()
} else {
graph
.neighbors_directed(v, petgraph::Direction::Outgoing)
.collect()
};
for w in neighbors {
if dist[w.index()] < 0 {
dist[w.index()] = v_dist + 1;
queue.push_back(w);
}
}
}
dist
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Triple;
#[test]
fn test_closeness_star_directed() {
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 = ClosenessConfig::default();
let scores = closeness_centrality(&kg, config);
let hub = *scores.get("Hub").unwrap();
let a = *scores.get("A").unwrap();
assert!(hub > 0.0, "Hub should have positive closeness: {hub}");
assert_eq!(a, 0.0, "A can't reach anyone: {a}");
}
#[test]
fn test_closeness_line() {
let mut kg = KnowledgeGraph::new();
kg.add_triple(Triple::new("A", "rel", "B"));
kg.add_triple(Triple::new("B", "rel", "C"));
let config = ClosenessConfig {
normalized: false,
undirected: true, harmonic: true,
};
let scores = closeness_centrality(&kg, config);
let a = *scores.get("A").unwrap();
let b = *scores.get("B").unwrap();
let c = *scores.get("C").unwrap();
assert!(b > a, "B should be more central than A: B={b}, A={a}");
assert!(
(a - c).abs() < 1e-6,
"A and C should be symmetric: A={a}, C={c}"
);
}
#[test]
fn test_closeness_disconnected() {
let mut kg = KnowledgeGraph::new();
kg.add_triple(Triple::new("A", "rel", "B"));
kg.add_triple(Triple::new("C", "rel", "D"));
let config = ClosenessConfig::default();
let scores = closeness_centrality(&kg, config);
let a = *scores.get("A").unwrap();
assert!(a > 0.0, "A should have some closeness: {a}");
}
#[test]
fn test_closeness_normalized() {
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", "B"));
kg.add_triple(Triple::new("A", "rel", "C"));
kg.add_triple(Triple::new("C", "rel", "A"));
let config = ClosenessConfig {
normalized: true,
undirected: false,
harmonic: true,
};
let scores = closeness_centrality(&kg, config);
for (name, score) in scores {
assert!(
(score - 1.0).abs() < 1e-6,
"Complete graph should have max closeness: {name}={score}"
);
}
}
}