use crate::node::NodeIndex;
use crate::plugins::algorithm::{
AlgorithmData, AlgorithmResult, GraphAlgorithm, PluginContext, PluginInfo,
};
use crate::vgi::{Capability, GraphType, VgiResult, VirtualGraph};
use std::any::Any;
pub struct PageRankPlugin {
damping: f64,
max_iterations: usize,
tolerance: f64,
}
impl PageRankPlugin {
pub fn new(damping: f64, max_iterations: usize, tolerance: f64) -> Self {
Self {
damping: damping.clamp(0.0, 1.0),
max_iterations,
tolerance,
}
}
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 incoming_edges: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
for edge in graph.edges() {
let src = edge.source();
let tgt = edge.target();
let src_pos = node_id_to_pos[src.index()];
let tgt_pos = node_id_to_pos[tgt.index()];
if src_pos != usize::MAX && tgt_pos != usize::MAX {
if let Ok(out_degree) = graph.out_degree(src) {
if out_degree > 0 {
incoming_edges[tgt_pos].push((src_pos, 1.0 / out_degree as f64));
}
}
}
}
let mut scores: Vec<f64> = vec![1.0 / n as f64; n];
let teleport = (1.0 - self.damping) / n as f64;
for _iteration in 0..self.max_iterations {
let mut new_scores: Vec<f64> = vec![0.0; n];
let mut max_diff = 0.0;
for (pos, _idx) in node_indices.iter().enumerate() {
let mut new_score = teleport;
for &(src_pos, inv_degree) in &incoming_edges[pos] {
new_score += self.damping * scores[src_pos] * inv_degree;
}
let diff = (new_score - scores[pos]).abs();
if diff > max_diff {
max_diff = diff;
}
new_scores[pos] = new_score;
}
scores = new_scores;
if max_diff < self.tolerance {
break;
}
}
Ok(node_indices
.iter()
.zip(scores.iter())
.map(|(idx, &score)| (idx.index(), score))
.collect())
}
}
impl GraphAlgorithm for PageRankPlugin {
fn info(&self) -> PluginInfo {
PluginInfo::new("pagerank", "1.0.0", "PageRank 中心性算法")
.with_author("God-Graph Team")
.with_required_capabilities(&[Capability::IncrementalUpdate])
.with_supported_graph_types(&[GraphType::Directed, GraphType::Undirected])
.with_tags(&["centrality", "ranking", "iterative"])
}
fn execute<G>(&self, ctx: &mut PluginContext<G>) -> VgiResult<AlgorithmResult>
where
G: VirtualGraph + ?Sized,
{
let damping = ctx.get_config_as("damping", self.damping);
let max_iter = ctx.get_config_as("max_iter", self.max_iterations);
let tolerance = ctx.get_config_as("tolerance", self.tolerance);
let plugin = PageRankPlugin::new(damping, max_iter, tolerance);
ctx.report_progress(0.1);
let scores = plugin.compute(ctx.graph)?;
ctx.report_progress(1.0);
let result = AlgorithmResult::new("pagerank", AlgorithmData::NodeValues(scores.into_iter().collect()))
.with_metadata("damping", damping.to_string())
.with_metadata("iterations", max_iter.to_string());
Ok(result)
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl Default for PageRankPlugin {
fn default() -> Self {
Self::new(0.85, 100, 1e-6)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::Graph;
use crate::graph::traits::GraphOps;
#[test]
fn test_pagerank_basic() {
let mut graph = Graph::<String, f64>::directed();
graph.add_node("A".to_string()).unwrap();
graph.add_node("B".to_string()).unwrap();
graph.add_node("C".to_string()).unwrap();
let plugin = PageRankPlugin::default();
let scores = plugin.compute(&graph).unwrap();
assert_eq!(scores.len(), 3);
assert!(scores.iter().all(|(_, v)| *v > 0.0));
}
#[test]
fn test_pagerank_empty_graph() {
let graph = Graph::<String, f64>::directed();
let plugin = PageRankPlugin::default();
let scores = plugin.compute(&graph).unwrap();
assert!(scores.is_empty());
}
#[test]
fn test_pagerank_plugin_info() {
let plugin = PageRankPlugin::default();
let info = plugin.info();
assert_eq!(info.name, "pagerank");
assert_eq!(info.version, "1.0.0");
assert!(info.tags.contains(&"centrality".to_string()));
}
}