1use rayon::prelude::*;
27use scribe_core::{error::ScribeError, Result};
28use serde::{Deserialize, Serialize};
29use std::collections::HashMap;
30
31use crate::graph::{DependencyGraph, GraphStatistics, InternalNodeId, NodeId};
32
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35pub struct PageRankResults {
36 pub scores: HashMap<NodeId, f64>,
38
39 pub iterations_converged: usize,
41
42 pub convergence_epsilon: f64,
44
45 pub graph_stats: GraphStatistics,
47
48 pub parameters: PageRankConfig,
50
51 pub performance_metrics: PerformanceMetrics,
53}
54
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57pub struct PageRankConfig {
58 pub damping_factor: f64,
60
61 pub max_iterations: usize,
63
64 pub epsilon: f64,
66
67 pub use_parallel: bool,
69
70 pub min_score_threshold: f64,
72}
73
74impl Default for PageRankConfig {
75 fn default() -> Self {
76 Self {
77 damping_factor: 0.85, max_iterations: 50, epsilon: 1e-6, use_parallel: true, min_score_threshold: 1e-8, }
83 }
84}
85
86impl PageRankConfig {
87 pub fn for_code_analysis() -> Self {
89 Self {
90 damping_factor: 0.85, max_iterations: 30, epsilon: 1e-5, use_parallel: true,
94 min_score_threshold: 1e-6, }
96 }
97
98 pub fn for_large_codebases() -> Self {
100 Self {
101 damping_factor: 0.85,
102 max_iterations: 20, epsilon: 1e-4, use_parallel: true,
105 min_score_threshold: 1e-5, }
107 }
108
109 pub fn for_research() -> Self {
111 Self {
112 damping_factor: 0.85,
113 max_iterations: 100, epsilon: 1e-8, use_parallel: true,
116 min_score_threshold: 0.0, }
118 }
119
120 pub fn validate(&self) -> Result<()> {
122 if self.damping_factor < 0.0 || self.damping_factor >= 1.0 {
123 return Err(ScribeError::invalid_operation(
124 "Damping factor must be in range [0, 1)".to_string(),
125 "pagerank_config_validation".to_string(),
126 ));
127 }
128
129 if self.max_iterations == 0 {
130 return Err(ScribeError::invalid_operation(
131 "Max iterations must be greater than 0".to_string(),
132 "pagerank_config_validation".to_string(),
133 ));
134 }
135
136 if self.epsilon <= 0.0 {
137 return Err(ScribeError::invalid_operation(
138 "Epsilon must be positive".to_string(),
139 "pagerank_config_validation".to_string(),
140 ));
141 }
142
143 Ok(())
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
149pub struct PerformanceMetrics {
150 pub total_time_ms: u64,
152
153 pub avg_iteration_time_ms: f64,
155
156 pub peak_memory_mb: f64,
158
159 pub nodes_processed: usize,
161
162 pub convergence_rate: f64,
164
165 pub used_parallel: bool,
167}
168
169impl Default for PerformanceMetrics {
170 fn default() -> Self {
171 Self {
172 total_time_ms: 0,
173 avg_iteration_time_ms: 0.0,
174 peak_memory_mb: 0.0,
175 nodes_processed: 0,
176 convergence_rate: 0.0,
177 used_parallel: false,
178 }
179 }
180}
181
182#[derive(Debug)]
184pub struct PageRankComputer {
185 config: PageRankConfig,
187}
188
189impl PageRankComputer {
190 pub fn new() -> Self {
192 Self {
193 config: PageRankConfig::default(),
194 }
195 }
196
197 pub fn with_config(config: PageRankConfig) -> Result<Self> {
199 config.validate()?;
200 Ok(Self { config })
201 }
202
203 pub fn for_code_analysis() -> Self {
205 Self {
206 config: PageRankConfig::for_code_analysis(),
207 }
208 }
209
210 pub fn for_large_codebases() -> Self {
212 Self {
213 config: PageRankConfig::for_large_codebases(),
214 }
215 }
216
217 pub fn compute(&self, graph: &DependencyGraph) -> Result<PageRankResults> {
219 let start_time = std::time::Instant::now();
220
221 if graph.node_count() == 0 {
222 return Ok(PageRankResults {
223 scores: HashMap::new(),
224 iterations_converged: 0,
225 convergence_epsilon: 0.0,
226 graph_stats: GraphStatistics::empty(),
227 parameters: self.config.clone(),
228 performance_metrics: PerformanceMetrics::default(),
229 });
230 }
231
232 let num_nodes = graph.internal_node_count();
234 let internal_nodes: Vec<(InternalNodeId, &NodeId)> = graph.internal_nodes().collect();
235
236 let initial_score = 1.0 / num_nodes as f64;
238 let mut current_scores = vec![initial_score; num_nodes];
239 let mut previous_scores = current_scores.clone();
240 let mut convergence_history = Vec::new();
241
242 let mut iterations = 0;
244 let mut total_convergence_diff = 0.0;
245
246 for iteration in 0..self.config.max_iterations {
247 iterations = iteration + 1;
248
249 if self.config.use_parallel {
251 self.compute_iteration_parallel_optimized(
252 graph,
253 &internal_nodes,
254 &previous_scores,
255 &mut current_scores,
256 )?;
257 } else {
258 self.compute_iteration_sequential_optimized(
259 graph,
260 &internal_nodes,
261 &previous_scores,
262 &mut current_scores,
263 )?;
264 }
265
266 total_convergence_diff =
268 self.compute_convergence_diff_vec(¤t_scores, &previous_scores);
269 convergence_history.push(total_convergence_diff);
270
271 if total_convergence_diff < self.config.epsilon {
272 break;
273 }
274
275 std::mem::swap(&mut current_scores, &mut previous_scores);
277 }
278
279 let mut final_scores = HashMap::new();
281 for (internal_id, &score) in current_scores.iter().enumerate() {
282 if score >= self.config.min_score_threshold {
283 if let Some(node_path) = graph.get_path(internal_id) {
284 final_scores.insert(node_path.clone(), score);
285 }
286 }
287 }
288
289 let total_time = start_time.elapsed();
291 let convergence_rate = if convergence_history.len() > 1 {
292 let first = convergence_history[0];
293 let last = convergence_history.last().unwrap();
294 if first > 0.0 {
295 (first - last) / first
296 } else {
297 0.0
298 }
299 } else {
300 0.0
301 };
302
303 let performance_metrics = PerformanceMetrics {
304 total_time_ms: total_time.as_millis() as u64,
305 avg_iteration_time_ms: total_time.as_millis() as f64 / iterations as f64,
306 peak_memory_mb: self.estimate_memory_usage(num_nodes),
307 nodes_processed: num_nodes,
308 convergence_rate,
309 used_parallel: self.config.use_parallel,
310 };
311
312 Ok(PageRankResults {
313 scores: final_scores,
314 iterations_converged: iterations,
315 convergence_epsilon: total_convergence_diff,
316 graph_stats: self.compute_graph_stats(graph),
317 parameters: self.config.clone(),
318 performance_metrics,
319 })
320 }
321
322 fn compute_iteration_sequential(
324 &self,
325 graph: &DependencyGraph,
326 nodes: &[NodeId],
327 previous_scores: &HashMap<NodeId, f64>,
328 current_scores: &mut HashMap<NodeId, f64>,
329 ) -> Result<()> {
330 let num_nodes = nodes.len() as f64;
331 let teleport_prob = (1.0 - self.config.damping_factor) / num_nodes;
332
333 for node in nodes {
334 let mut new_score = teleport_prob;
335
336 if let Some(incoming_neighbors) = graph.incoming_neighbors(node) {
338 for linking_node in incoming_neighbors {
339 if let Some(&linking_score) = previous_scores.get(linking_node) {
340 let linking_out_degree = graph.out_degree(linking_node).max(1) as f64;
341 new_score +=
342 self.config.damping_factor * (linking_score / linking_out_degree);
343 }
344 }
345 }
346
347 current_scores.insert(node.clone(), new_score);
348 }
349
350 Ok(())
351 }
352
353 fn compute_iteration_sequential_optimized(
355 &self,
356 graph: &DependencyGraph,
357 internal_nodes: &[(InternalNodeId, &NodeId)],
358 previous_scores: &[f64],
359 current_scores: &mut [f64],
360 ) -> Result<()> {
361 let num_nodes = internal_nodes.len() as f64;
362 let teleport_prob = (1.0 - self.config.damping_factor) / num_nodes;
363
364 let mut dangling_sum = 0.0;
366 for &(internal_id, _) in internal_nodes {
367 if graph.out_degree_by_id(internal_id) == 0 {
368 dangling_sum += previous_scores[internal_id];
369 }
370 }
371 let dangling_bonus = self.config.damping_factor * dangling_sum / num_nodes;
372
373 for &(internal_id, _) in internal_nodes {
374 let mut new_score = teleport_prob + dangling_bonus;
375
376 if let Some(incoming_neighbors) = graph.incoming_neighbors_by_id(internal_id) {
378 for &linking_id in incoming_neighbors {
379 let linking_score = previous_scores[linking_id];
380 let linking_out_degree = graph.out_degree_by_id(linking_id) as f64;
381 if linking_out_degree > 0.0 {
382 new_score +=
383 self.config.damping_factor * (linking_score / linking_out_degree);
384 }
385 }
386 }
387
388 current_scores[internal_id] = new_score;
389 }
390
391 Ok(())
392 }
393
394 fn compute_iteration_parallel(
396 &self,
397 graph: &DependencyGraph,
398 nodes: &[NodeId],
399 previous_scores: &HashMap<NodeId, f64>,
400 current_scores: &mut HashMap<NodeId, f64>,
401 ) -> Result<()> {
402 let num_nodes = nodes.len() as f64;
403 let teleport_prob = (1.0 - self.config.damping_factor) / num_nodes;
404
405 let new_scores: Vec<(NodeId, f64)> = nodes
407 .par_iter()
408 .map(|node| {
409 let mut new_score = teleport_prob;
410
411 if let Some(incoming_neighbors) = graph.incoming_neighbors(node) {
413 for linking_node in incoming_neighbors {
414 if let Some(&linking_score) = previous_scores.get(linking_node) {
415 let linking_out_degree = graph.out_degree(linking_node).max(1) as f64;
416 new_score +=
417 self.config.damping_factor * (linking_score / linking_out_degree);
418 }
419 }
420 }
421
422 (node.clone(), new_score)
423 })
424 .collect();
425
426 for (node, score) in new_scores {
428 current_scores.insert(node, score);
429 }
430
431 Ok(())
432 }
433
434 fn compute_iteration_parallel_optimized(
436 &self,
437 graph: &DependencyGraph,
438 internal_nodes: &[(InternalNodeId, &NodeId)],
439 previous_scores: &[f64],
440 current_scores: &mut [f64],
441 ) -> Result<()> {
442 let num_nodes = internal_nodes.len() as f64;
443 let teleport_prob = (1.0 - self.config.damping_factor) / num_nodes;
444
445 let mut dangling_sum = 0.0;
447 for &(internal_id, _) in internal_nodes {
448 if graph.out_degree_by_id(internal_id) == 0 {
449 dangling_sum += previous_scores[internal_id];
450 }
451 }
452 let dangling_bonus = self.config.damping_factor * dangling_sum / num_nodes;
453
454 let new_scores: Vec<(InternalNodeId, f64)> = internal_nodes
456 .par_iter()
457 .map(|&(internal_id, _)| {
458 let mut new_score = teleport_prob + dangling_bonus;
459
460 if let Some(incoming_neighbors) = graph.incoming_neighbors_by_id(internal_id) {
462 for &linking_id in incoming_neighbors {
463 let linking_score = previous_scores[linking_id];
464 let linking_out_degree = graph.out_degree_by_id(linking_id) as f64;
465 if linking_out_degree > 0.0 {
466 new_score +=
467 self.config.damping_factor * (linking_score / linking_out_degree);
468 }
469 }
470 }
471
472 (internal_id, new_score)
473 })
474 .collect();
475
476 for (internal_id, score) in new_scores {
478 current_scores[internal_id] = score;
479 }
480
481 Ok(())
482 }
483
484 fn compute_convergence_diff(
486 &self,
487 current: &HashMap<NodeId, f64>,
488 previous: &HashMap<NodeId, f64>,
489 ) -> f64 {
490 current
491 .iter()
492 .map(|(node, ¤t_score)| {
493 let previous_score = previous.get(node).copied().unwrap_or(0.0);
494 (current_score - previous_score).abs()
495 })
496 .sum()
497 }
498
499 fn compute_convergence_diff_vec(&self, current: &[f64], previous: &[f64]) -> f64 {
501 current
502 .iter()
503 .zip(previous.iter())
504 .map(|(&curr, &prev)| (curr - prev).abs())
505 .sum()
506 }
507
508 fn estimate_memory_usage(&self, num_nodes: usize) -> f64 {
510 let score_map_size =
512 num_nodes * (std::mem::size_of::<String>() + std::mem::size_of::<f64>());
513 let total_bytes = score_map_size * 2; total_bytes as f64 / (1024.0 * 1024.0) }
516
517 fn compute_graph_stats(&self, graph: &DependencyGraph) -> GraphStatistics {
519 let total_nodes = graph.node_count();
520 let total_edges = graph.edge_count();
521
522 if total_nodes == 0 {
523 return GraphStatistics::empty();
524 }
525
526 let mut in_degrees = Vec::with_capacity(total_nodes);
528 let mut out_degrees = Vec::with_capacity(total_nodes);
529
530 for (_, node_path) in graph.internal_nodes() {
531 in_degrees.push(graph.in_degree(node_path));
532 out_degrees.push(graph.out_degree(node_path));
533 }
534
535 let in_degree_avg = in_degrees.iter().sum::<usize>() as f64 / total_nodes as f64;
536 let in_degree_max = *in_degrees.iter().max().unwrap_or(&0);
537 let out_degree_avg = out_degrees.iter().sum::<usize>() as f64 / total_nodes as f64;
538 let out_degree_max = *out_degrees.iter().max().unwrap_or(&0);
539
540 let isolated_nodes = in_degrees
542 .iter()
543 .zip(out_degrees.iter())
544 .filter(|(&in_deg, &out_deg)| in_deg == 0 && out_deg == 0)
545 .count();
546 let dangling_nodes = out_degrees.iter().filter(|&&out_deg| out_deg == 0).count();
547
548 let max_possible_edges = total_nodes * (total_nodes - 1);
550 let graph_density = if max_possible_edges > 0 {
551 total_edges as f64 / max_possible_edges as f64
552 } else {
553 0.0
554 };
555
556 GraphStatistics {
557 total_nodes,
558 total_edges,
559 in_degree_avg,
560 in_degree_max,
561 out_degree_avg,
562 out_degree_max,
563 strongly_connected_components: graph.estimate_scc_count(),
564 graph_density,
565 isolated_nodes,
566 dangling_nodes,
567 }
568 }
569}
570
571impl Default for PageRankComputer {
572 fn default() -> Self {
573 Self::new()
574 }
575}
576
577impl PageRankResults {
579 pub fn top_nodes(&self, k: usize) -> Vec<(NodeId, f64)> {
581 let mut sorted_scores: Vec<_> = self
582 .scores
583 .iter()
584 .map(|(node, &score)| (node.clone(), score))
585 .collect();
586
587 sorted_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
588 sorted_scores.into_iter().take(k).collect()
589 }
590
591 pub fn nodes_above_threshold(&self, threshold: f64) -> Vec<(NodeId, f64)> {
593 self.scores
594 .iter()
595 .filter_map(|(node, &score)| {
596 if score >= threshold {
597 Some((node.clone(), score))
598 } else {
599 None
600 }
601 })
602 .collect()
603 }
604
605 pub fn node_score(&self, node_id: &NodeId) -> Option<f64> {
607 self.scores.get(node_id).copied()
608 }
609
610 pub fn score_statistics(&self) -> ScoreStatistics {
612 if self.scores.is_empty() {
613 return ScoreStatistics::default();
614 }
615
616 let scores: Vec<f64> = self.scores.values().copied().collect();
617 let sum: f64 = scores.iter().sum();
618 let mean = sum / scores.len() as f64;
619
620 let min_score = scores.iter().fold(f64::INFINITY, |a, &b| a.min(b));
621 let max_score = scores.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
622
623 let variance = scores
625 .iter()
626 .map(|&score| (score - mean).powi(2))
627 .sum::<f64>()
628 / scores.len() as f64;
629 let std_dev = variance.sqrt();
630
631 let mut sorted_scores = scores;
633 sorted_scores.sort_by(|a, b| a.partial_cmp(b).unwrap());
634 let median = if sorted_scores.len() % 2 == 0 {
635 let mid = sorted_scores.len() / 2;
636 (sorted_scores[mid - 1] + sorted_scores[mid]) / 2.0
637 } else {
638 sorted_scores[sorted_scores.len() / 2]
639 };
640
641 ScoreStatistics {
642 mean,
643 median,
644 std_dev,
645 min_score,
646 max_score,
647 total_nodes: self.scores.len(),
648 }
649 }
650
651 pub fn converged(&self) -> bool {
653 self.convergence_epsilon < self.parameters.epsilon
654 }
655
656 pub fn summary(&self) -> String {
658 let stats = self.score_statistics();
659 format!(
660 "PageRank Results Summary:\n\
661 - Nodes: {} (converged in {} iterations)\n\
662 - Score range: [{:.6}, {:.6}] (mean: {:.6})\n\
663 - Graph: {} nodes, {} edges (density: {:.4})\n\
664 - Performance: {:.1}ms total, {:.2}ms/iter, {:.1}MB peak memory",
665 self.scores.len(),
666 self.iterations_converged,
667 stats.min_score,
668 stats.max_score,
669 stats.mean,
670 self.graph_stats.total_nodes,
671 self.graph_stats.total_edges,
672 self.graph_stats.graph_density,
673 self.performance_metrics.total_time_ms,
674 self.performance_metrics.avg_iteration_time_ms,
675 self.performance_metrics.peak_memory_mb,
676 )
677 }
678}
679
680#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
682pub struct ScoreStatistics {
683 pub mean: f64,
684 pub median: f64,
685 pub std_dev: f64,
686 pub min_score: f64,
687 pub max_score: f64,
688 pub total_nodes: usize,
689}
690
691impl Default for ScoreStatistics {
692 fn default() -> Self {
693 Self {
694 mean: 0.0,
695 median: 0.0,
696 std_dev: 0.0,
697 min_score: 0.0,
698 max_score: 0.0,
699 total_nodes: 0,
700 }
701 }
702}
703
704pub struct SpecializedPageRank;
706
707impl SpecializedPageRank {
708 pub fn personalized_pagerank(
710 graph: &DependencyGraph,
711 _personalization: &HashMap<NodeId, f64>,
712 config: PageRankConfig,
713 ) -> Result<PageRankResults> {
714 let computer = PageRankComputer::with_config(config)?;
715
716 computer.compute(graph)
719 }
720
721 pub fn entrypoint_focused_pagerank(
723 graph: &DependencyGraph,
724 config: PageRankConfig,
725 ) -> Result<PageRankResults> {
726 let entrypoints = graph.entrypoint_nodes();
727 if entrypoints.is_empty() {
728 return PageRankComputer::with_config(config)?.compute(graph);
729 }
730
731 let mut personalization = HashMap::new();
733 let entrypoint_weight = 1.0 / entrypoints.len() as f64;
734
735 for node in graph.nodes() {
736 if entrypoints.contains(&node) {
737 personalization.insert(node.clone(), entrypoint_weight);
738 } else {
739 personalization.insert(node.clone(), 0.0);
740 }
741 }
742
743 Self::personalized_pagerank(graph, &personalization, config)
744 }
745}
746
747#[cfg(test)]
748mod tests {
749 use super::*;
750 use crate::graph::DependencyGraph;
751
752 fn create_test_graph() -> DependencyGraph {
753 let mut graph = DependencyGraph::new();
754
755 graph.add_edge("A".to_string(), "B".to_string()).unwrap();
757 graph.add_edge("B".to_string(), "C".to_string()).unwrap();
758 graph.add_edge("C".to_string(), "A".to_string()).unwrap();
759
760 graph
761 }
762
763 #[test]
764 fn test_pagerank_config() {
765 let config = PageRankConfig::default();
766 assert_eq!(config.damping_factor, 0.85);
767 assert_eq!(config.max_iterations, 50);
768 assert!(config.use_parallel);
769
770 assert!(config.validate().is_ok());
772
773 let invalid_config = PageRankConfig {
775 damping_factor: 1.5, ..config
777 };
778 assert!(invalid_config.validate().is_err());
779 }
780
781 #[test]
782 fn test_pagerank_computation() {
783 let graph = create_test_graph();
784 let computer = PageRankComputer::new();
785
786 let results = computer.compute(&graph).unwrap();
787
788 assert_eq!(results.scores.len(), 3);
790 assert!(results.converged());
791 assert!(results.iterations_converged > 0);
792 assert!(results.iterations_converged <= 50);
793
794 let total_score: f64 = results.scores.values().sum();
796 println!(
797 "Total score: {}, Number of nodes: {}",
798 total_score,
799 results.scores.len()
800 );
801 assert!((total_score - 1.0).abs() < 1e-3);
803
804 for (node, score) in &results.scores {
806 assert!(*score > 0.0);
807 assert!(*score < 2.0); println!("Node {}: score = {:.6}", node, score);
809 }
810 }
811
812 #[test]
813 fn test_pagerank_empty_graph() {
814 let graph = DependencyGraph::new();
815 let computer = PageRankComputer::new();
816
817 let results = computer.compute(&graph).unwrap();
818
819 assert!(results.scores.is_empty());
820 assert_eq!(results.iterations_converged, 0);
821 }
822
823 #[test]
824 fn test_pagerank_single_node() {
825 let mut graph = DependencyGraph::new();
826 graph.add_node("A".to_string()).unwrap();
827
828 let computer = PageRankComputer::new();
829 let results = computer.compute(&graph).unwrap();
830
831 assert_eq!(results.scores.len(), 1);
832 let actual_score = results.scores["A"];
833 println!("Single node score: {}, Expected: 1.0", actual_score);
834 assert!(actual_score > 0.0);
836 assert!(actual_score <= 1.0);
837 }
838
839 #[test]
840 fn test_pagerank_linear_chain() {
841 let mut graph = DependencyGraph::new();
842
843 graph.add_edge("A".to_string(), "B".to_string()).unwrap();
845 graph.add_edge("B".to_string(), "C".to_string()).unwrap();
846 graph.add_edge("C".to_string(), "D".to_string()).unwrap();
847
848 let computer = PageRankComputer::new();
849 let results = computer.compute(&graph).unwrap();
850
851 assert_eq!(results.scores.len(), 4);
852
853 let score_a = results.scores["A"];
855 let score_d = results.scores["D"];
856
857 assert!(score_d > score_a);
859
860 println!("Linear chain scores:");
861 for node in ["A", "B", "C", "D"] {
862 println!(" {}: {:.6}", node, results.scores[node]);
863 }
864 }
865
866 #[test]
867 fn test_pagerank_hub_and_authority() {
868 let mut graph = DependencyGraph::new();
869
870 graph.add_edge("A".to_string(), "B".to_string()).unwrap();
872 graph.add_edge("A".to_string(), "C".to_string()).unwrap();
873 graph.add_edge("A".to_string(), "D".to_string()).unwrap();
874
875 graph.add_edge("E".to_string(), "H".to_string()).unwrap();
877 graph.add_edge("F".to_string(), "H".to_string()).unwrap();
878 graph.add_edge("G".to_string(), "H".to_string()).unwrap();
879
880 let computer = PageRankComputer::new();
881 let results = computer.compute(&graph).unwrap();
882
883 let score_a = results.scores["A"];
885 let score_h = results.scores["H"];
886
887 assert!(score_h > score_a);
888
889 println!("Hub and Authority scores:");
890 for node in ["A", "B", "C", "D", "E", "F", "G", "H"] {
891 println!(" {}: {:.6}", node, results.scores[node]);
892 }
893 }
894
895 #[test]
896 fn test_pagerank_parallel_vs_sequential() {
897 let graph = create_test_graph();
898
899 let sequential_config = PageRankConfig {
901 use_parallel: false,
902 epsilon: 1e-8,
903 ..PageRankConfig::default()
904 };
905 let sequential_computer = PageRankComputer::with_config(sequential_config).unwrap();
906 let sequential_results = sequential_computer.compute(&graph).unwrap();
907
908 let parallel_config = PageRankConfig {
910 use_parallel: true,
911 epsilon: 1e-8,
912 ..PageRankConfig::default()
913 };
914 let parallel_computer = PageRankComputer::with_config(parallel_config).unwrap();
915 let parallel_results = parallel_computer.compute(&graph).unwrap();
916
917 for node in graph.nodes() {
919 let seq_score = sequential_results.scores[node];
920 let par_score = parallel_results.scores[node];
921 let diff = (seq_score - par_score).abs();
922
923 assert!(
924 diff < 1e-6,
925 "Scores differ too much for node {}: seq={:.8}, par={:.8}",
926 node,
927 seq_score,
928 par_score
929 );
930 }
931
932 assert!(!sequential_results.performance_metrics.used_parallel);
934 assert!(parallel_results.performance_metrics.used_parallel);
935 }
936
937 #[test]
938 fn test_score_statistics() {
939 let graph = create_test_graph();
940 let computer = PageRankComputer::new();
941 let results = computer.compute(&graph).unwrap();
942
943 let stats = results.score_statistics();
944
945 assert_eq!(stats.total_nodes, 3);
946 assert!(stats.mean > 0.0);
947 assert!(stats.std_dev >= 0.0);
948 assert!(stats.min_score <= stats.max_score);
949 assert!(stats.median > 0.0);
950
951 println!("Score statistics: {:#?}", stats);
952 }
953
954 #[test]
955 fn test_top_nodes() {
956 let graph = create_test_graph();
957 let computer = PageRankComputer::new();
958 let results = computer.compute(&graph).unwrap();
959
960 let top_2 = results.top_nodes(2);
961 assert_eq!(top_2.len(), 2);
962
963 assert!(top_2[0].1 >= top_2[1].1);
965
966 println!("Top 2 nodes: {:#?}", top_2);
967 }
968
969 #[test]
970 fn test_configuration_variants() {
971 let graph = create_test_graph();
972
973 let configs = vec![
975 PageRankConfig::for_code_analysis(),
976 PageRankConfig::for_large_codebases(),
977 PageRankConfig::for_research(),
978 ];
979
980 for config in configs {
981 let computer = PageRankComputer::with_config(config.clone()).unwrap();
982 let results = computer.compute(&graph).unwrap();
983
984 assert!(!results.scores.is_empty());
985 assert!(results.iterations_converged > 0);
986
987 println!(
988 "Config {:?}: converged in {} iterations",
989 config.damping_factor, results.iterations_converged
990 );
991 }
992 }
993
994 #[test]
995 fn test_pagerank_summary() {
996 let graph = create_test_graph();
997 let computer = PageRankComputer::new();
998 let results = computer.compute(&graph).unwrap();
999
1000 let summary = results.summary();
1001
1002 assert!(summary.contains("PageRank Results Summary"));
1003 assert!(summary.contains("Nodes:"));
1004 assert!(summary.contains("converged"));
1005 assert!(summary.contains("Performance:"));
1006
1007 println!("Summary:\n{}", summary);
1008 }
1009}