use rayon::prelude::*;
use scribe_core::{error::ScribeError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::graph::{DependencyGraph, GraphStatistics, InternalNodeId, NodeId};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PageRankResults {
pub scores: HashMap<NodeId, f64>,
pub iterations_converged: usize,
pub convergence_epsilon: f64,
pub graph_stats: GraphStatistics,
pub parameters: PageRankConfig,
pub performance_metrics: PerformanceMetrics,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PageRankConfig {
pub damping_factor: f64,
pub max_iterations: usize,
pub epsilon: f64,
pub use_parallel: bool,
pub min_score_threshold: f64,
}
impl Default for PageRankConfig {
fn default() -> Self {
Self {
damping_factor: 0.85, max_iterations: 50, epsilon: 1e-6, use_parallel: true, min_score_threshold: 1e-8, }
}
}
impl PageRankConfig {
pub fn for_code_analysis() -> Self {
Self {
damping_factor: 0.85, max_iterations: 30, epsilon: 1e-5, use_parallel: true,
min_score_threshold: 1e-6, }
}
pub fn for_large_codebases() -> Self {
Self {
damping_factor: 0.85,
max_iterations: 20, epsilon: 1e-4, use_parallel: true,
min_score_threshold: 1e-5, }
}
pub fn for_research() -> Self {
Self {
damping_factor: 0.85,
max_iterations: 100, epsilon: 1e-8, use_parallel: true,
min_score_threshold: 0.0, }
}
pub fn validate(&self) -> Result<()> {
if self.damping_factor < 0.0 || self.damping_factor >= 1.0 {
return Err(ScribeError::invalid_operation(
"Damping factor must be in range [0, 1)".to_string(),
"pagerank_config_validation".to_string(),
));
}
if self.max_iterations == 0 {
return Err(ScribeError::invalid_operation(
"Max iterations must be greater than 0".to_string(),
"pagerank_config_validation".to_string(),
));
}
if self.epsilon <= 0.0 {
return Err(ScribeError::invalid_operation(
"Epsilon must be positive".to_string(),
"pagerank_config_validation".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PerformanceMetrics {
pub total_time_ms: u64,
pub avg_iteration_time_ms: f64,
pub peak_memory_mb: f64,
pub nodes_processed: usize,
pub convergence_rate: f64,
pub used_parallel: bool,
}
impl Default for PerformanceMetrics {
fn default() -> Self {
Self {
total_time_ms: 0,
avg_iteration_time_ms: 0.0,
peak_memory_mb: 0.0,
nodes_processed: 0,
convergence_rate: 0.0,
used_parallel: false,
}
}
}
#[derive(Debug)]
pub struct PageRankComputer {
config: PageRankConfig,
}
impl PageRankComputer {
pub fn new() -> Self {
Self {
config: PageRankConfig::default(),
}
}
pub fn with_config(config: PageRankConfig) -> Result<Self> {
config.validate()?;
Ok(Self { config })
}
pub fn for_code_analysis() -> Self {
Self {
config: PageRankConfig::for_code_analysis(),
}
}
pub fn for_large_codebases() -> Self {
Self {
config: PageRankConfig::for_large_codebases(),
}
}
pub fn compute(&self, graph: &DependencyGraph) -> Result<PageRankResults> {
let start_time = std::time::Instant::now();
if graph.node_count() == 0 {
return Ok(PageRankResults {
scores: HashMap::new(),
iterations_converged: 0,
convergence_epsilon: 0.0,
graph_stats: GraphStatistics::empty(),
parameters: self.config.clone(),
performance_metrics: PerformanceMetrics::default(),
});
}
let num_nodes = graph.internal_node_count();
let internal_nodes: Vec<(InternalNodeId, &NodeId)> = graph.internal_nodes().collect();
let initial_score = 1.0 / num_nodes as f64;
let mut current_scores = vec![initial_score; num_nodes];
let mut previous_scores = current_scores.clone();
let mut convergence_history = Vec::new();
let mut iterations = 0;
let mut total_convergence_diff = 0.0;
for iteration in 0..self.config.max_iterations {
iterations = iteration + 1;
if self.config.use_parallel {
self.compute_iteration_parallel_optimized(
graph,
&internal_nodes,
&previous_scores,
&mut current_scores,
)?;
} else {
self.compute_iteration_sequential_optimized(
graph,
&internal_nodes,
&previous_scores,
&mut current_scores,
)?;
}
total_convergence_diff =
self.compute_convergence_diff_vec(¤t_scores, &previous_scores);
convergence_history.push(total_convergence_diff);
if total_convergence_diff < self.config.epsilon {
break;
}
std::mem::swap(&mut current_scores, &mut previous_scores);
}
let mut final_scores = HashMap::new();
for (internal_id, &score) in current_scores.iter().enumerate() {
if score >= self.config.min_score_threshold {
if let Some(node_path) = graph.get_path(internal_id) {
final_scores.insert(node_path.clone(), score);
}
}
}
let total_time = start_time.elapsed();
let convergence_rate = if convergence_history.len() > 1 {
let first = convergence_history[0];
let last = convergence_history.last().unwrap();
if first > 0.0 {
(first - last) / first
} else {
0.0
}
} else {
0.0
};
let performance_metrics = PerformanceMetrics {
total_time_ms: total_time.as_millis() as u64,
avg_iteration_time_ms: total_time.as_millis() as f64 / iterations as f64,
peak_memory_mb: self.estimate_memory_usage(num_nodes),
nodes_processed: num_nodes,
convergence_rate,
used_parallel: self.config.use_parallel,
};
Ok(PageRankResults {
scores: final_scores,
iterations_converged: iterations,
convergence_epsilon: total_convergence_diff,
graph_stats: self.compute_graph_stats(graph),
parameters: self.config.clone(),
performance_metrics,
})
}
fn compute_iteration_sequential(
&self,
graph: &DependencyGraph,
nodes: &[NodeId],
previous_scores: &HashMap<NodeId, f64>,
current_scores: &mut HashMap<NodeId, f64>,
) -> Result<()> {
let num_nodes = nodes.len() as f64;
let teleport_prob = (1.0 - self.config.damping_factor) / num_nodes;
for node in nodes {
let mut new_score = teleport_prob;
if let Some(incoming_neighbors) = graph.incoming_neighbors(node) {
for linking_node in incoming_neighbors {
if let Some(&linking_score) = previous_scores.get(linking_node) {
let linking_out_degree = graph.out_degree(linking_node).max(1) as f64;
new_score +=
self.config.damping_factor * (linking_score / linking_out_degree);
}
}
}
current_scores.insert(node.clone(), new_score);
}
Ok(())
}
fn compute_iteration_sequential_optimized(
&self,
graph: &DependencyGraph,
internal_nodes: &[(InternalNodeId, &NodeId)],
previous_scores: &[f64],
current_scores: &mut [f64],
) -> Result<()> {
let num_nodes = internal_nodes.len() as f64;
let teleport_prob = (1.0 - self.config.damping_factor) / num_nodes;
let mut dangling_sum = 0.0;
for &(internal_id, _) in internal_nodes {
if graph.out_degree_by_id(internal_id) == 0 {
dangling_sum += previous_scores[internal_id];
}
}
let dangling_bonus = self.config.damping_factor * dangling_sum / num_nodes;
for &(internal_id, _) in internal_nodes {
let mut new_score = teleport_prob + dangling_bonus;
if let Some(incoming_neighbors) = graph.incoming_neighbors_by_id(internal_id) {
for &linking_id in incoming_neighbors {
let linking_score = previous_scores[linking_id];
let linking_out_degree = graph.out_degree_by_id(linking_id) as f64;
if linking_out_degree > 0.0 {
new_score +=
self.config.damping_factor * (linking_score / linking_out_degree);
}
}
}
current_scores[internal_id] = new_score;
}
Ok(())
}
fn compute_iteration_parallel(
&self,
graph: &DependencyGraph,
nodes: &[NodeId],
previous_scores: &HashMap<NodeId, f64>,
current_scores: &mut HashMap<NodeId, f64>,
) -> Result<()> {
let num_nodes = nodes.len() as f64;
let teleport_prob = (1.0 - self.config.damping_factor) / num_nodes;
let new_scores: Vec<(NodeId, f64)> = nodes
.par_iter()
.map(|node| {
let mut new_score = teleport_prob;
if let Some(incoming_neighbors) = graph.incoming_neighbors(node) {
for linking_node in incoming_neighbors {
if let Some(&linking_score) = previous_scores.get(linking_node) {
let linking_out_degree = graph.out_degree(linking_node).max(1) as f64;
new_score +=
self.config.damping_factor * (linking_score / linking_out_degree);
}
}
}
(node.clone(), new_score)
})
.collect();
for (node, score) in new_scores {
current_scores.insert(node, score);
}
Ok(())
}
fn compute_iteration_parallel_optimized(
&self,
graph: &DependencyGraph,
internal_nodes: &[(InternalNodeId, &NodeId)],
previous_scores: &[f64],
current_scores: &mut [f64],
) -> Result<()> {
let num_nodes = internal_nodes.len() as f64;
let teleport_prob = (1.0 - self.config.damping_factor) / num_nodes;
let mut dangling_sum = 0.0;
for &(internal_id, _) in internal_nodes {
if graph.out_degree_by_id(internal_id) == 0 {
dangling_sum += previous_scores[internal_id];
}
}
let dangling_bonus = self.config.damping_factor * dangling_sum / num_nodes;
let new_scores: Vec<(InternalNodeId, f64)> = internal_nodes
.par_iter()
.map(|&(internal_id, _)| {
let mut new_score = teleport_prob + dangling_bonus;
if let Some(incoming_neighbors) = graph.incoming_neighbors_by_id(internal_id) {
for &linking_id in incoming_neighbors {
let linking_score = previous_scores[linking_id];
let linking_out_degree = graph.out_degree_by_id(linking_id) as f64;
if linking_out_degree > 0.0 {
new_score +=
self.config.damping_factor * (linking_score / linking_out_degree);
}
}
}
(internal_id, new_score)
})
.collect();
for (internal_id, score) in new_scores {
current_scores[internal_id] = score;
}
Ok(())
}
fn compute_convergence_diff(
&self,
current: &HashMap<NodeId, f64>,
previous: &HashMap<NodeId, f64>,
) -> f64 {
current
.iter()
.map(|(node, ¤t_score)| {
let previous_score = previous.get(node).copied().unwrap_or(0.0);
(current_score - previous_score).abs()
})
.sum()
}
fn compute_convergence_diff_vec(&self, current: &[f64], previous: &[f64]) -> f64 {
current
.iter()
.zip(previous.iter())
.map(|(&curr, &prev)| (curr - prev).abs())
.sum()
}
fn estimate_memory_usage(&self, num_nodes: usize) -> f64 {
let score_map_size =
num_nodes * (std::mem::size_of::<String>() + std::mem::size_of::<f64>());
let total_bytes = score_map_size * 2; total_bytes as f64 / (1024.0 * 1024.0) }
fn compute_graph_stats(&self, graph: &DependencyGraph) -> GraphStatistics {
let total_nodes = graph.node_count();
let total_edges = graph.edge_count();
if total_nodes == 0 {
return GraphStatistics::empty();
}
let mut in_degrees = Vec::with_capacity(total_nodes);
let mut out_degrees = Vec::with_capacity(total_nodes);
for (_, node_path) in graph.internal_nodes() {
in_degrees.push(graph.in_degree(node_path));
out_degrees.push(graph.out_degree(node_path));
}
let in_degree_avg = in_degrees.iter().sum::<usize>() as f64 / total_nodes as f64;
let in_degree_max = *in_degrees.iter().max().unwrap_or(&0);
let out_degree_avg = out_degrees.iter().sum::<usize>() as f64 / total_nodes as f64;
let out_degree_max = *out_degrees.iter().max().unwrap_or(&0);
let isolated_nodes = in_degrees
.iter()
.zip(out_degrees.iter())
.filter(|(&in_deg, &out_deg)| in_deg == 0 && out_deg == 0)
.count();
let dangling_nodes = out_degrees.iter().filter(|&&out_deg| out_deg == 0).count();
let max_possible_edges = total_nodes * (total_nodes - 1);
let graph_density = if max_possible_edges > 0 {
total_edges as f64 / max_possible_edges as f64
} else {
0.0
};
GraphStatistics {
total_nodes,
total_edges,
in_degree_avg,
in_degree_max,
out_degree_avg,
out_degree_max,
strongly_connected_components: graph.estimate_scc_count(),
graph_density,
isolated_nodes,
dangling_nodes,
}
}
}
impl Default for PageRankComputer {
fn default() -> Self {
Self::new()
}
}
impl PageRankResults {
pub fn top_nodes(&self, k: usize) -> Vec<(NodeId, f64)> {
let mut sorted_scores: Vec<_> = self
.scores
.iter()
.map(|(node, &score)| (node.clone(), score))
.collect();
sorted_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
sorted_scores.into_iter().take(k).collect()
}
pub fn nodes_above_threshold(&self, threshold: f64) -> Vec<(NodeId, f64)> {
self.scores
.iter()
.filter_map(|(node, &score)| {
if score >= threshold {
Some((node.clone(), score))
} else {
None
}
})
.collect()
}
pub fn node_score(&self, node_id: &NodeId) -> Option<f64> {
self.scores.get(node_id).copied()
}
pub fn score_statistics(&self) -> ScoreStatistics {
if self.scores.is_empty() {
return ScoreStatistics::default();
}
let scores: Vec<f64> = self.scores.values().copied().collect();
let sum: f64 = scores.iter().sum();
let mean = sum / scores.len() as f64;
let min_score = scores.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max_score = scores.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let variance = scores
.iter()
.map(|&score| (score - mean).powi(2))
.sum::<f64>()
/ scores.len() as f64;
let std_dev = variance.sqrt();
let mut sorted_scores = scores;
sorted_scores.sort_by(|a, b| a.partial_cmp(b).unwrap());
let median = if sorted_scores.len() % 2 == 0 {
let mid = sorted_scores.len() / 2;
(sorted_scores[mid - 1] + sorted_scores[mid]) / 2.0
} else {
sorted_scores[sorted_scores.len() / 2]
};
ScoreStatistics {
mean,
median,
std_dev,
min_score,
max_score,
total_nodes: self.scores.len(),
}
}
pub fn converged(&self) -> bool {
self.convergence_epsilon < self.parameters.epsilon
}
pub fn summary(&self) -> String {
let stats = self.score_statistics();
format!(
"PageRank Results Summary:\n\
- Nodes: {} (converged in {} iterations)\n\
- Score range: [{:.6}, {:.6}] (mean: {:.6})\n\
- Graph: {} nodes, {} edges (density: {:.4})\n\
- Performance: {:.1}ms total, {:.2}ms/iter, {:.1}MB peak memory",
self.scores.len(),
self.iterations_converged,
stats.min_score,
stats.max_score,
stats.mean,
self.graph_stats.total_nodes,
self.graph_stats.total_edges,
self.graph_stats.graph_density,
self.performance_metrics.total_time_ms,
self.performance_metrics.avg_iteration_time_ms,
self.performance_metrics.peak_memory_mb,
)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ScoreStatistics {
pub mean: f64,
pub median: f64,
pub std_dev: f64,
pub min_score: f64,
pub max_score: f64,
pub total_nodes: usize,
}
impl Default for ScoreStatistics {
fn default() -> Self {
Self {
mean: 0.0,
median: 0.0,
std_dev: 0.0,
min_score: 0.0,
max_score: 0.0,
total_nodes: 0,
}
}
}
pub struct SpecializedPageRank;
impl SpecializedPageRank {
pub fn personalized_pagerank(
graph: &DependencyGraph,
_personalization: &HashMap<NodeId, f64>,
config: PageRankConfig,
) -> Result<PageRankResults> {
let computer = PageRankComputer::with_config(config)?;
computer.compute(graph)
}
pub fn entrypoint_focused_pagerank(
graph: &DependencyGraph,
config: PageRankConfig,
) -> Result<PageRankResults> {
let entrypoints = graph.entrypoint_nodes();
if entrypoints.is_empty() {
return PageRankComputer::with_config(config)?.compute(graph);
}
let mut personalization = HashMap::new();
let entrypoint_weight = 1.0 / entrypoints.len() as f64;
for node in graph.nodes() {
if entrypoints.contains(&node) {
personalization.insert(node.clone(), entrypoint_weight);
} else {
personalization.insert(node.clone(), 0.0);
}
}
Self::personalized_pagerank(graph, &personalization, config)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::DependencyGraph;
fn create_test_graph() -> DependencyGraph {
let mut graph = DependencyGraph::new();
graph.add_edge("A".to_string(), "B".to_string()).unwrap();
graph.add_edge("B".to_string(), "C".to_string()).unwrap();
graph.add_edge("C".to_string(), "A".to_string()).unwrap();
graph
}
#[test]
fn test_pagerank_config() {
let config = PageRankConfig::default();
assert_eq!(config.damping_factor, 0.85);
assert_eq!(config.max_iterations, 50);
assert!(config.use_parallel);
assert!(config.validate().is_ok());
let invalid_config = PageRankConfig {
damping_factor: 1.5, ..config
};
assert!(invalid_config.validate().is_err());
}
#[test]
fn test_pagerank_computation() {
let graph = create_test_graph();
let computer = PageRankComputer::new();
let results = computer.compute(&graph).unwrap();
assert_eq!(results.scores.len(), 3);
assert!(results.converged());
assert!(results.iterations_converged > 0);
assert!(results.iterations_converged <= 50);
let total_score: f64 = results.scores.values().sum();
println!(
"Total score: {}, Number of nodes: {}",
total_score,
results.scores.len()
);
assert!((total_score - 1.0).abs() < 1e-3);
for (node, score) in &results.scores {
assert!(*score > 0.0);
assert!(*score < 2.0); println!("Node {}: score = {:.6}", node, score);
}
}
#[test]
fn test_pagerank_empty_graph() {
let graph = DependencyGraph::new();
let computer = PageRankComputer::new();
let results = computer.compute(&graph).unwrap();
assert!(results.scores.is_empty());
assert_eq!(results.iterations_converged, 0);
}
#[test]
fn test_pagerank_single_node() {
let mut graph = DependencyGraph::new();
graph.add_node("A".to_string()).unwrap();
let computer = PageRankComputer::new();
let results = computer.compute(&graph).unwrap();
assert_eq!(results.scores.len(), 1);
let actual_score = results.scores["A"];
println!("Single node score: {}, Expected: 1.0", actual_score);
assert!(actual_score > 0.0);
assert!(actual_score <= 1.0);
}
#[test]
fn test_pagerank_linear_chain() {
let mut graph = DependencyGraph::new();
graph.add_edge("A".to_string(), "B".to_string()).unwrap();
graph.add_edge("B".to_string(), "C".to_string()).unwrap();
graph.add_edge("C".to_string(), "D".to_string()).unwrap();
let computer = PageRankComputer::new();
let results = computer.compute(&graph).unwrap();
assert_eq!(results.scores.len(), 4);
let score_a = results.scores["A"];
let score_d = results.scores["D"];
assert!(score_d > score_a);
println!("Linear chain scores:");
for node in ["A", "B", "C", "D"] {
println!(" {}: {:.6}", node, results.scores[node]);
}
}
#[test]
fn test_pagerank_hub_and_authority() {
let mut graph = DependencyGraph::new();
graph.add_edge("A".to_string(), "B".to_string()).unwrap();
graph.add_edge("A".to_string(), "C".to_string()).unwrap();
graph.add_edge("A".to_string(), "D".to_string()).unwrap();
graph.add_edge("E".to_string(), "H".to_string()).unwrap();
graph.add_edge("F".to_string(), "H".to_string()).unwrap();
graph.add_edge("G".to_string(), "H".to_string()).unwrap();
let computer = PageRankComputer::new();
let results = computer.compute(&graph).unwrap();
let score_a = results.scores["A"];
let score_h = results.scores["H"];
assert!(score_h > score_a);
println!("Hub and Authority scores:");
for node in ["A", "B", "C", "D", "E", "F", "G", "H"] {
println!(" {}: {:.6}", node, results.scores[node]);
}
}
#[test]
fn test_pagerank_parallel_vs_sequential() {
let graph = create_test_graph();
let sequential_config = PageRankConfig {
use_parallel: false,
epsilon: 1e-8,
..PageRankConfig::default()
};
let sequential_computer = PageRankComputer::with_config(sequential_config).unwrap();
let sequential_results = sequential_computer.compute(&graph).unwrap();
let parallel_config = PageRankConfig {
use_parallel: true,
epsilon: 1e-8,
..PageRankConfig::default()
};
let parallel_computer = PageRankComputer::with_config(parallel_config).unwrap();
let parallel_results = parallel_computer.compute(&graph).unwrap();
for node in graph.nodes() {
let seq_score = sequential_results.scores[node];
let par_score = parallel_results.scores[node];
let diff = (seq_score - par_score).abs();
assert!(
diff < 1e-6,
"Scores differ too much for node {}: seq={:.8}, par={:.8}",
node,
seq_score,
par_score
);
}
assert!(!sequential_results.performance_metrics.used_parallel);
assert!(parallel_results.performance_metrics.used_parallel);
}
#[test]
fn test_score_statistics() {
let graph = create_test_graph();
let computer = PageRankComputer::new();
let results = computer.compute(&graph).unwrap();
let stats = results.score_statistics();
assert_eq!(stats.total_nodes, 3);
assert!(stats.mean > 0.0);
assert!(stats.std_dev >= 0.0);
assert!(stats.min_score <= stats.max_score);
assert!(stats.median > 0.0);
println!("Score statistics: {:#?}", stats);
}
#[test]
fn test_top_nodes() {
let graph = create_test_graph();
let computer = PageRankComputer::new();
let results = computer.compute(&graph).unwrap();
let top_2 = results.top_nodes(2);
assert_eq!(top_2.len(), 2);
assert!(top_2[0].1 >= top_2[1].1);
println!("Top 2 nodes: {:#?}", top_2);
}
#[test]
fn test_configuration_variants() {
let graph = create_test_graph();
let configs = vec![
PageRankConfig::for_code_analysis(),
PageRankConfig::for_large_codebases(),
PageRankConfig::for_research(),
];
for config in configs {
let computer = PageRankComputer::with_config(config.clone()).unwrap();
let results = computer.compute(&graph).unwrap();
assert!(!results.scores.is_empty());
assert!(results.iterations_converged > 0);
println!(
"Config {:?}: converged in {} iterations",
config.damping_factor, results.iterations_converged
);
}
}
#[test]
fn test_pagerank_summary() {
let graph = create_test_graph();
let computer = PageRankComputer::new();
let results = computer.compute(&graph).unwrap();
let summary = results.summary();
assert!(summary.contains("PageRank Results Summary"));
assert!(summary.contains("Nodes:"));
assert!(summary.contains("converged"));
assert!(summary.contains("Performance:"));
println!("Summary:\n{}", summary);
}
}