use crate::node::NodeIndex;
use crate::plugins::algorithm::{
AlgorithmData, AlgorithmResult, GraphAlgorithm, PluginContext, PluginInfo,
};
use crate::vgi::{Capability, GraphType, VgiResult, VirtualGraph};
use std::any::Any;
use std::collections::VecDeque;
pub struct ClosenessCentralityPlugin {
improved: bool,
}
impl ClosenessCentralityPlugin {
pub fn new(improved: bool) -> Self {
Self { improved }
}
pub fn improved() -> Self {
Self { improved: true }
}
pub fn standard() -> Self {
Self { improved: false }
}
pub fn compute<G>(&self, graph: &G) -> VgiResult<Vec<(usize, f64)>>
where
G: VirtualGraph + ?Sized,
{
let n = graph.node_count();
if n == 0 {
return Ok(Vec::new());
}
let node_indices: Vec<NodeIndex> = graph.nodes().map(|n| n.index()).collect();
let mut node_id_to_pos: Vec<usize> = vec![usize::MAX; n];
for (pos, idx) in node_indices.iter().enumerate() {
node_id_to_pos[idx.index()] = pos;
}
let mut centrality: Vec<(usize, f64)> = Vec::with_capacity(n);
for (source_pos, source_idx) in node_indices.iter().enumerate() {
let source = source_idx.index();
let distances = self.bfs_distances(graph, source, &node_id_to_pos, n)?;
let mut total_distance = 0.0;
let mut reachable_count = 0usize;
for (target_pos, &dist) in distances.iter().enumerate() {
if target_pos != source_pos && dist.is_finite() && dist > 0.0 {
total_distance += dist;
reachable_count += 1;
}
}
let closeness = if total_distance > 0.0 {
if self.improved && reachable_count < n - 1 {
let reachability_ratio = reachable_count as f64 / (n - 1) as f64;
reachability_ratio * (reachable_count as f64 / total_distance)
} else {
(n - 1) as f64 / total_distance
}
} else {
0.0 };
centrality.push((source, closeness));
}
Ok(centrality)
}
fn bfs_distances<G>(
&self,
graph: &G,
source: usize,
node_id_to_pos: &[usize],
n: usize,
) -> VgiResult<Vec<f64>>
where
G: VirtualGraph + ?Sized,
{
let mut distances: Vec<f64> = vec![f64::INFINITY; n];
let mut queue: VecDeque<usize> = VecDeque::new();
let source_pos = node_id_to_pos[source];
if source_pos == usize::MAX {
return Ok(distances);
}
distances[source_pos] = 0.0;
queue.push_back(source_pos);
while let Some(pos) = queue.pop_front() {
let node_id = node_id_to_pos[pos];
let v_dist = distances[pos];
let v_idx = NodeIndex::new_public(node_id);
for w_idx in graph.neighbors(v_idx) {
let w = w_idx.index();
let w_pos = node_id_to_pos[w];
if w_pos != usize::MAX {
let edge_weight = 1.0;
if v_dist + edge_weight < distances[w_pos] {
distances[w_pos] = v_dist + edge_weight;
queue.push_back(w_pos);
}
}
}
}
Ok(distances)
}
pub fn top_k<G>(&self, graph: &G, k: usize) -> VgiResult<Vec<(usize, f64)>>
where
G: VirtualGraph + ?Sized,
{
let centrality = self.compute(graph)?;
let mut nodes: Vec<(usize, f64)> = centrality;
if k >= nodes.len() {
nodes.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
return Ok(nodes);
}
nodes.select_nth_unstable_by(k, |a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
nodes.truncate(k);
Ok(nodes)
}
}
impl Default for ClosenessCentralityPlugin {
fn default() -> Self {
Self::improved()
}
}
impl GraphAlgorithm for ClosenessCentralityPlugin {
fn info(&self) -> PluginInfo {
PluginInfo::new("closeness-centrality", "1.0.0", "接近中心性算法")
.with_author("God-Graph Team")
.with_required_capabilities(&[Capability::IncrementalUpdate])
.with_supported_graph_types(&[GraphType::Directed, GraphType::Undirected])
.with_tags(&["centrality", "closeness", "distance", "importance"])
}
fn execute<G>(&self, ctx: &mut PluginContext<G>) -> VgiResult<AlgorithmResult>
where
G: VirtualGraph + ?Sized,
{
let improved = ctx.get_config_or("improved", "true") == "true";
let top_k = ctx.get_config_as("top_k", 0usize);
let plugin = if improved {
ClosenessCentralityPlugin::improved()
} else {
ClosenessCentralityPlugin::standard()
};
ctx.report_progress(0.1);
let result = if top_k > 0 {
let top_nodes = plugin.top_k(ctx.graph, top_k)?;
let nodes: Vec<usize> = top_nodes.iter().map(|(id, _)| *id).collect();
let scores: Vec<(usize, f64)> = top_nodes;
AlgorithmResult::new("closeness_top_k", AlgorithmData::NodeList(nodes))
.with_metadata("top_k", top_k.to_string())
.with_metadata("scores", format!("{:?}", scores))
} else {
let centrality = plugin.compute(ctx.graph)?;
let centrality_map: Vec<(usize, f64)> = centrality;
AlgorithmResult::new(
"closeness_centrality",
AlgorithmData::NodeValues(centrality_map.into_iter().collect()),
)
}
.with_metadata("improved", improved.to_string())
.with_metadata("algorithm", "closeness-centrality")
.with_metadata("node_count", ctx.graph.node_count().to_string());
ctx.report_progress(1.0);
Ok(result)
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::Graph;
use crate::graph::traits::GraphOps;
fn create_star_graph() -> Graph<String, ()> {
let mut graph = Graph::<String, ()>::undirected();
let center = graph.add_node("Center".to_string()).unwrap();
let leaves: Vec<NodeIndex> = (0..5)
.map(|i| graph.add_node(format!("Leaf{}", i)).unwrap())
.collect();
for leaf in leaves {
graph.add_edge(center, leaf, ()).unwrap();
graph.add_edge(leaf, center, ()).unwrap();
}
graph
}
fn create_line_graph() -> Graph<String, ()> {
let mut graph = Graph::<String, ()>::undirected();
let a = graph.add_node("A".to_string()).unwrap();
let b = graph.add_node("B".to_string()).unwrap();
let c = graph.add_node("C".to_string()).unwrap();
let d = graph.add_node("D".to_string()).unwrap();
let e = graph.add_node("E".to_string()).unwrap();
graph.add_edge(a, b, ()).unwrap();
graph.add_edge(b, a, ()).unwrap();
graph.add_edge(b, c, ()).unwrap();
graph.add_edge(c, b, ()).unwrap();
graph.add_edge(c, d, ()).unwrap();
graph.add_edge(d, c, ()).unwrap();
graph.add_edge(d, e, ()).unwrap();
graph.add_edge(e, d, ()).unwrap();
graph
}
fn create_disconnected_graph() -> Graph<String, ()> {
let mut graph = Graph::<String, ()>::undirected();
let a = graph.add_node("A".to_string()).unwrap();
let b = graph.add_node("B".to_string()).unwrap();
graph.add_edge(a, b, ()).unwrap();
graph.add_edge(b, a, ()).unwrap();
let c = graph.add_node("C".to_string()).unwrap();
let d = graph.add_node("D".to_string()).unwrap();
graph.add_edge(c, d, ()).unwrap();
graph.add_edge(d, c, ()).unwrap();
graph
}
#[test]
fn test_closeness_star_graph() {
let graph = create_star_graph();
let plugin = ClosenessCentralityPlugin::standard();
let centrality = plugin.compute(&graph).unwrap();
let center_centrality = centrality.iter().find(|(id, _)| *id == 0).map(|(_, v)| *v).unwrap_or(0.0);
for i in 1..=5 {
let leaf_centrality = centrality.iter().find(|(id, _)| *id == i).map(|(_, v)| *v).unwrap_or(0.0);
assert!(leaf_centrality < center_centrality);
}
assert!(center_centrality > 0.8);
}
#[test]
fn test_closeness_line_graph() {
let graph = create_line_graph();
let plugin = ClosenessCentralityPlugin::standard();
let centrality = plugin.compute(&graph).unwrap();
let c_centrality = centrality.iter().find(|(id, _)| *id == 2).map(|(_, v)| *v).unwrap_or(0.0);
let a_centrality = centrality.iter().find(|(id, _)| *id == 0).map(|(_, v)| *v).unwrap_or(0.0);
let e_centrality = centrality.iter().find(|(id, _)| *id == 4).map(|(_, v)| *v).unwrap_or(0.0);
assert!(c_centrality > a_centrality);
assert!(c_centrality > e_centrality);
assert!((a_centrality - e_centrality).abs() < 1e-10);
}
#[test]
fn test_closeness_disconnected_graph_standard() {
let graph = create_disconnected_graph();
let plugin = ClosenessCentralityPlugin::standard();
let centrality = plugin.compute(&graph).unwrap();
assert_eq!(centrality.len(), 4);
}
#[test]
fn test_closeness_disconnected_graph_improved() {
let graph = create_disconnected_graph();
let plugin = ClosenessCentralityPlugin::improved();
let centrality = plugin.compute(&graph).unwrap();
assert_eq!(centrality.len(), 4);
for (_, value) in ¢rality {
assert!((0.0..=1.0).contains(value));
}
}
#[test]
fn test_closeness_empty_graph() {
let graph = Graph::<String, ()>::undirected();
let plugin = ClosenessCentralityPlugin::default();
let centrality = plugin.compute(&graph).unwrap();
assert!(centrality.is_empty());
}
#[test]
fn test_closeness_top_k() {
let graph = create_line_graph();
let plugin = ClosenessCentralityPlugin::standard();
let top_nodes = plugin.top_k(&graph, 1).unwrap();
assert_eq!(top_nodes.len(), 1);
let (top_id, _) = top_nodes[0];
assert_eq!(top_id, 2);
}
#[test]
fn test_closeness_plugin_info() {
let plugin = ClosenessCentralityPlugin::default();
let info = plugin.info();
assert_eq!(info.name, "closeness-centrality");
assert_eq!(info.version, "1.0.0");
assert!(info.tags.contains(&"centrality".to_string()));
}
}