1use crate::messages::{CentralityInput, CentralityOutput, CentralityParams};
12use crate::ring_messages::{
13 K2KBarrier, K2KBarrierRelease, K2KIterationSync, K2KIterationSyncResponse,
14 PageRankConvergeResponse, PageRankConvergeRing, PageRankIterateResponse, PageRankIterateRing,
15 PageRankQueryResponse, PageRankQueryRing, from_fixed_point, to_fixed_point,
16};
17use crate::types::{CentralityResult, CsrGraph, NodeScore};
18use async_trait::async_trait;
19use ringkernel_core::RingContext;
20use rustkernel_core::{
21 domain::Domain,
22 error::Result,
23 k2k::IterativeState,
24 kernel::KernelMetadata,
25 traits::{BatchKernel, GpuKernel, RingKernelHandler},
26};
27use std::collections::VecDeque;
28use std::time::Instant;
29
30#[derive(Debug, Clone, Default)]
36pub struct PageRankState {
37 pub scores: Vec<f64>,
39 pub prev_scores: Vec<f64>,
41 pub graph: Option<CsrGraph>,
43 pub damping: f32,
45 pub iteration: u32,
47 pub converged: bool,
49}
50
51#[derive(Debug)]
56pub struct PageRank {
57 metadata: KernelMetadata,
58 state: std::sync::RwLock<PageRankState>,
60}
61
62impl Clone for PageRank {
63 fn clone(&self) -> Self {
64 Self {
65 metadata: self.metadata.clone(),
66 state: std::sync::RwLock::new(self.state.read().unwrap().clone()),
67 }
68 }
69}
70
71impl PageRank {
72 #[must_use]
74 pub fn new() -> Self {
75 Self {
76 metadata: KernelMetadata::ring("graph/pagerank", Domain::GraphAnalytics)
77 .with_description("PageRank centrality via power iteration")
78 .with_throughput(100_000)
79 .with_latency_us(1.0)
80 .with_gpu_native(true),
81 state: std::sync::RwLock::new(PageRankState::default()),
82 }
83 }
84
85 pub fn initialize(&self, graph: CsrGraph, damping: f32) {
87 let mut state = self.state.write().unwrap();
88 *state = Self::initialize_state(graph, damping);
89 }
90
91 pub fn query_score(&self, node_id: u64) -> Option<f64> {
93 let state = self.state.read().unwrap();
94 state.scores.get(node_id as usize).copied()
95 }
96
97 pub fn current_iteration(&self) -> u32 {
99 self.state.read().unwrap().iteration
100 }
101
102 pub fn is_converged(&self) -> bool {
104 self.state.read().unwrap().converged
105 }
106
107 pub fn iterate(&self) -> f64 {
109 let mut state = self.state.write().unwrap();
110 Self::iterate_step(&mut state)
111 }
112
113 pub fn iterate_step(state: &mut PageRankState) -> f64 {
115 let Some(ref graph) = state.graph else {
116 return 0.0;
117 };
118
119 let n = graph.num_nodes;
120 if n == 0 {
121 return 0.0;
122 }
123
124 let d = state.damping as f64;
125 let teleport = (1.0 - d) / n as f64;
126
127 std::mem::swap(&mut state.scores, &mut state.prev_scores);
129
130 let mut max_diff = 0.0f64;
132
133 for i in 0..n {
134 let mut rank_sum = 0.0f64;
135
136 for &neighbor in graph.neighbors(i as u64) {
138 let out_degree = graph.out_degree(neighbor) as f64;
139 if out_degree > 0.0 {
140 rank_sum += state.prev_scores[neighbor as usize] / out_degree;
141 }
142 }
143
144 let new_score = teleport + d * rank_sum;
145 state.scores[i] = new_score;
146
147 let diff = (new_score - state.prev_scores[i]).abs();
148 if diff > max_diff {
149 max_diff = diff;
150 }
151 }
152
153 state.iteration += 1;
154 max_diff
155 }
156
157 pub fn initialize_state(graph: CsrGraph, damping: f32) -> PageRankState {
159 let n = graph.num_nodes;
160 PageRankState {
161 scores: vec![1.0 / n as f64; n],
162 prev_scores: vec![0.0; n],
163 graph: Some(graph),
164 damping,
165 iteration: 0,
166 converged: false,
167 }
168 }
169
170 pub fn run_to_convergence(
172 graph: CsrGraph,
173 damping: f32,
174 max_iterations: u32,
175 threshold: f64,
176 ) -> Result<CentralityResult> {
177 let mut state = Self::initialize_state(graph, damping);
178
179 for _ in 0..max_iterations {
180 let diff = Self::iterate_step(&mut state);
181 if diff < threshold {
182 state.converged = true;
183 break;
184 }
185 }
186
187 Ok(CentralityResult {
188 scores: state
189 .scores
190 .iter()
191 .enumerate()
192 .map(|(i, &score)| NodeScore {
193 node_id: i as u64,
194 score,
195 })
196 .collect(),
197 iterations: Some(state.iteration),
198 converged: state.converged,
199 })
200 }
201}
202
203impl Default for PageRank {
204 fn default() -> Self {
205 Self::new()
206 }
207}
208
209impl GpuKernel for PageRank {
210 fn metadata(&self) -> &KernelMetadata {
211 &self.metadata
212 }
213}
214
215#[async_trait]
223impl RingKernelHandler<PageRankQueryRing, PageRankQueryResponse> for PageRank {
224 async fn handle(
225 &self,
226 _ctx: &mut RingContext,
227 msg: PageRankQueryRing,
228 ) -> Result<PageRankQueryResponse> {
229 let state = self.state.read().unwrap();
230 let score = state
231 .scores
232 .get(msg.node_id as usize)
233 .copied()
234 .unwrap_or(0.0);
235
236 Ok(PageRankQueryResponse {
237 request_id: msg.id.0,
238 node_id: msg.node_id,
239 score_fp: to_fixed_point(score),
240 iteration: state.iteration,
241 converged: state.converged,
242 })
243 }
244}
245
246#[async_trait]
250impl RingKernelHandler<PageRankIterateRing, PageRankIterateResponse> for PageRank {
251 async fn handle(
252 &self,
253 _ctx: &mut RingContext,
254 msg: PageRankIterateRing,
255 ) -> Result<PageRankIterateResponse> {
256 let max_delta = self.iterate();
258
259 let state = self.state.read().unwrap();
261 let converged = max_delta < 1e-6;
262
263 Ok(PageRankIterateResponse {
264 request_id: msg.id.0,
265 iteration: state.iteration,
266 max_delta_fp: to_fixed_point(max_delta),
267 converged,
268 })
269 }
270}
271
272#[async_trait]
276impl RingKernelHandler<PageRankConvergeRing, PageRankConvergeResponse> for PageRank {
277 async fn handle(
278 &self,
279 _ctx: &mut RingContext,
280 msg: PageRankConvergeRing,
281 ) -> Result<PageRankConvergeResponse> {
282 let threshold = from_fixed_point(msg.threshold_fp);
283 let max_iterations = msg.max_iterations as u64;
284
285 let mut iterative_state = IterativeState::new(threshold, max_iterations);
287
288 while iterative_state.should_continue() {
290 let max_delta = self.iterate();
291 iterative_state.update(max_delta);
292 }
293
294 {
296 let mut state = self.state.write().unwrap();
297 state.converged = iterative_state.summary().converged;
298 }
299
300 let summary = iterative_state.summary();
301
302 Ok(PageRankConvergeResponse {
303 request_id: msg.id.0,
304 iterations: summary.iterations as u32,
305 final_delta_fp: to_fixed_point(summary.final_delta),
306 converged: summary.converged,
307 })
308 }
309}
310
311#[async_trait]
317impl RingKernelHandler<K2KIterationSync, K2KIterationSyncResponse> for PageRank {
318 async fn handle(
319 &self,
320 _ctx: &mut RingContext,
321 msg: K2KIterationSync,
322 ) -> Result<K2KIterationSyncResponse> {
323 let state = self.state.read().unwrap();
324
325 let current_iteration = state.iteration as u64;
328 let all_synced = msg.iteration <= current_iteration;
329
330 let local_delta = from_fixed_point(msg.local_delta_fp);
333 let global_converged = local_delta < 1e-6 || state.converged;
334
335 Ok(K2KIterationSyncResponse {
336 request_id: msg.id.0,
337 iteration: msg.iteration,
338 all_synced,
339 global_delta_fp: msg.local_delta_fp,
340 global_converged,
341 })
342 }
343}
344
345#[async_trait]
349impl RingKernelHandler<K2KBarrier, K2KBarrierRelease> for PageRank {
350 async fn handle(&self, _ctx: &mut RingContext, msg: K2KBarrier) -> Result<K2KBarrierRelease> {
351 let all_ready = msg.ready_count >= msg.total_workers;
356
357 Ok(K2KBarrierRelease {
358 barrier_id: msg.barrier_id,
359 all_ready,
360 next_iteration: msg.barrier_id + 1,
361 })
362 }
363}
364
365#[derive(Debug, Clone)]
373pub struct DegreeCentrality {
374 metadata: KernelMetadata,
375}
376
377impl DegreeCentrality {
378 #[must_use]
380 pub fn new() -> Self {
381 Self {
382 metadata: KernelMetadata::ring("graph/degree-centrality", Domain::GraphAnalytics)
383 .with_description("Degree centrality (O(1) lookup)")
384 .with_throughput(1_000_000)
385 .with_latency_us(0.1),
386 }
387 }
388
389 pub fn compute(graph: &CsrGraph) -> CentralityResult {
393 let n = graph.num_nodes;
394 let normalizer = if n > 1 { (n - 1) as f64 } else { 1.0 };
395
396 let scores: Vec<NodeScore> = (0..n)
397 .map(|i| NodeScore {
398 node_id: i as u64,
399 score: graph.out_degree(i as u64) as f64 / normalizer,
400 })
401 .collect();
402
403 CentralityResult {
404 scores,
405 iterations: None,
406 converged: true,
407 }
408 }
409}
410
411impl Default for DegreeCentrality {
412 fn default() -> Self {
413 Self::new()
414 }
415}
416
417impl GpuKernel for DegreeCentrality {
418 fn metadata(&self) -> &KernelMetadata {
419 &self.metadata
420 }
421}
422
423#[derive(Debug, Clone)]
431pub struct BetweennessCentrality {
432 metadata: KernelMetadata,
433}
434
435impl BetweennessCentrality {
436 #[must_use]
438 pub fn new() -> Self {
439 Self {
440 metadata: KernelMetadata::batch("graph/betweenness-centrality", Domain::GraphAnalytics)
441 .with_description("Betweenness centrality (Brandes algorithm)")
442 .with_throughput(10_000)
443 .with_latency_us(100.0),
444 }
445 }
446
447 pub fn compute(graph: &CsrGraph, normalized: bool) -> CentralityResult {
452 let n = graph.num_nodes;
453 let mut centrality = vec![0.0f64; n];
454
455 for s in 0..n {
457 let mut stack: Vec<usize> = Vec::with_capacity(n);
459 let mut predecessors: Vec<Vec<usize>> = vec![Vec::new(); n];
460 let mut sigma = vec![0.0f64; n]; let mut dist = vec![-1i64; n]; sigma[s] = 1.0;
464 dist[s] = 0;
465
466 let mut queue = VecDeque::new();
467 queue.push_back(s);
468
469 while let Some(v) = queue.pop_front() {
471 stack.push(v);
472
473 for &w in graph.neighbors(v as u64) {
474 let w = w as usize;
475
476 if dist[w] < 0 {
478 dist[w] = dist[v] + 1;
479 queue.push_back(w);
480 }
481
482 if dist[w] == dist[v] + 1 {
484 sigma[w] += sigma[v];
485 predecessors[w].push(v);
486 }
487 }
488 }
489
490 let mut delta = vec![0.0f64; n];
492
493 while let Some(w) = stack.pop() {
494 for &v in &predecessors[w] {
495 let contribution = (sigma[v] / sigma[w]) * (1.0 + delta[w]);
496 delta[v] += contribution;
497 }
498
499 if w != s {
500 centrality[w] += delta[w];
501 }
502 }
503 }
504
505 if normalized && n > 2 {
507 let scale = 1.0 / ((n - 1) * (n - 2)) as f64;
508 for c in &mut centrality {
509 *c *= scale;
510 }
511 }
512
513 CentralityResult {
514 scores: centrality
515 .into_iter()
516 .enumerate()
517 .map(|(i, score)| NodeScore {
518 node_id: i as u64,
519 score,
520 })
521 .collect(),
522 iterations: None,
523 converged: true,
524 }
525 }
526}
527
528impl Default for BetweennessCentrality {
529 fn default() -> Self {
530 Self::new()
531 }
532}
533
534impl GpuKernel for BetweennessCentrality {
535 fn metadata(&self) -> &KernelMetadata {
536 &self.metadata
537 }
538}
539
540#[derive(Debug, Clone)]
549pub struct ClosenessCentrality {
550 metadata: KernelMetadata,
551}
552
553impl ClosenessCentrality {
554 #[must_use]
556 pub fn new() -> Self {
557 Self {
558 metadata: KernelMetadata::batch("graph/closeness-centrality", Domain::GraphAnalytics)
559 .with_description("Closeness centrality (BFS-based)")
560 .with_throughput(10_000)
561 .with_latency_us(100.0),
562 }
563 }
564
565 pub fn compute(graph: &CsrGraph, harmonic: bool) -> CentralityResult {
569 let n = graph.num_nodes;
570 let mut centrality = vec![0.0f64; n];
571
572 for source in 0..n {
573 let distances = Self::bfs_distances(graph, source);
574
575 if harmonic {
576 let sum: f64 = distances
578 .iter()
579 .enumerate()
580 .filter(|(i, d)| *i != source && **d > 0)
581 .map(|(_, d)| 1.0 / *d as f64)
582 .sum();
583 centrality[source] = sum / (n - 1) as f64;
584 } else {
585 let sum: i64 = distances.iter().sum();
587 let reachable: usize = distances.iter().filter(|&&d| d > 0).count();
588
589 if sum > 0 && reachable > 0 {
590 centrality[source] = reachable as f64 / sum as f64;
591 }
592 }
593 }
594
595 CentralityResult {
596 scores: centrality
597 .into_iter()
598 .enumerate()
599 .map(|(i, score)| NodeScore {
600 node_id: i as u64,
601 score,
602 })
603 .collect(),
604 iterations: None,
605 converged: true,
606 }
607 }
608
609 fn bfs_distances(graph: &CsrGraph, source: usize) -> Vec<i64> {
611 let n = graph.num_nodes;
612 let mut distances = vec![0i64; n];
613 let mut visited = vec![false; n];
614
615 let mut queue = VecDeque::new();
616 queue.push_back(source);
617 visited[source] = true;
618
619 while let Some(v) = queue.pop_front() {
620 for &w in graph.neighbors(v as u64) {
621 let w = w as usize;
622 if !visited[w] {
623 visited[w] = true;
624 distances[w] = distances[v] + 1;
625 queue.push_back(w);
626 }
627 }
628 }
629
630 distances
631 }
632}
633
634impl Default for ClosenessCentrality {
635 fn default() -> Self {
636 Self::new()
637 }
638}
639
640impl GpuKernel for ClosenessCentrality {
641 fn metadata(&self) -> &KernelMetadata {
642 &self.metadata
643 }
644}
645
646#[derive(Debug, Clone)]
655pub struct EigenvectorCentrality {
656 metadata: KernelMetadata,
657}
658
659impl EigenvectorCentrality {
660 #[must_use]
662 pub fn new() -> Self {
663 Self {
664 metadata: KernelMetadata::batch("graph/eigenvector-centrality", Domain::GraphAnalytics)
665 .with_description("Eigenvector centrality (power iteration)")
666 .with_throughput(50_000)
667 .with_latency_us(10.0),
668 }
669 }
670
671 pub fn compute(graph: &CsrGraph, max_iterations: u32, tolerance: f64) -> CentralityResult {
673 let n = graph.num_nodes;
674 if n == 0 {
675 return CentralityResult {
676 scores: Vec::new(),
677 iterations: Some(0),
678 converged: true,
679 };
680 }
681
682 let mut scores = vec![1.0 / (n as f64).sqrt(); n];
684 let mut new_scores = vec![0.0f64; n];
685 let mut converged = false;
686 let mut iterations = 0u32;
687
688 for iter in 0..max_iterations {
689 iterations = iter + 1;
690
691 for i in 0..n {
693 let mut sum = 0.0f64;
694 for &j in graph.neighbors(i as u64) {
695 sum += scores[j as usize];
696 }
697 new_scores[i] = sum;
698 }
699
700 let norm: f64 = new_scores.iter().map(|x| x * x).sum::<f64>().sqrt();
702 if norm > 0.0 {
703 for x in &mut new_scores {
704 *x /= norm;
705 }
706 }
707
708 let diff: f64 = scores
710 .iter()
711 .zip(new_scores.iter())
712 .map(|(a, b)| (a - b).abs())
713 .fold(0.0f64, |acc, x| acc.max(x));
714
715 std::mem::swap(&mut scores, &mut new_scores);
716
717 if diff < tolerance {
718 converged = true;
719 break;
720 }
721 }
722
723 CentralityResult {
724 scores: scores
725 .into_iter()
726 .enumerate()
727 .map(|(i, score)| NodeScore {
728 node_id: i as u64,
729 score,
730 })
731 .collect(),
732 iterations: Some(iterations),
733 converged,
734 }
735 }
736}
737
738impl Default for EigenvectorCentrality {
739 fn default() -> Self {
740 Self::new()
741 }
742}
743
744impl GpuKernel for EigenvectorCentrality {
745 fn metadata(&self) -> &KernelMetadata {
746 &self.metadata
747 }
748}
749
750#[derive(Debug, Clone)]
759pub struct KatzCentrality {
760 metadata: KernelMetadata,
761}
762
763impl KatzCentrality {
764 #[must_use]
766 pub fn new() -> Self {
767 Self {
768 metadata: KernelMetadata::batch("graph/katz-centrality", Domain::GraphAnalytics)
769 .with_description("Katz centrality (attenuated paths)")
770 .with_throughput(50_000)
771 .with_latency_us(10.0),
772 }
773 }
774
775 pub fn compute(
784 graph: &CsrGraph,
785 alpha: f64,
786 beta: f64,
787 max_iterations: u32,
788 tolerance: f64,
789 ) -> CentralityResult {
790 let n = graph.num_nodes;
791 if n == 0 {
792 return CentralityResult {
793 scores: Vec::new(),
794 iterations: Some(0),
795 converged: true,
796 };
797 }
798
799 let mut scores = vec![0.0f64; n];
801 let mut new_scores = vec![0.0f64; n];
802 let mut converged = false;
803 let mut iterations = 0u32;
804
805 for iter in 0..max_iterations {
807 iterations = iter + 1;
808
809 for i in 0..n {
810 let mut sum = 0.0f64;
811 for &j in graph.neighbors(i as u64) {
812 sum += scores[j as usize];
813 }
814 new_scores[i] = alpha * sum + beta;
815 }
816
817 let diff: f64 = scores
819 .iter()
820 .zip(new_scores.iter())
821 .map(|(a, b)| (a - b).abs())
822 .fold(0.0f64, |acc, x| acc.max(x));
823
824 std::mem::swap(&mut scores, &mut new_scores);
825
826 if diff < tolerance {
827 converged = true;
828 break;
829 }
830 }
831
832 let max_score = scores.iter().cloned().fold(0.0f64, f64::max);
834 if max_score > 0.0 {
835 for s in &mut scores {
836 *s /= max_score;
837 }
838 }
839
840 CentralityResult {
841 scores: scores
842 .into_iter()
843 .enumerate()
844 .map(|(i, score)| NodeScore {
845 node_id: i as u64,
846 score,
847 })
848 .collect(),
849 iterations: Some(iterations),
850 converged,
851 }
852 }
853}
854
855impl Default for KatzCentrality {
856 fn default() -> Self {
857 Self::new()
858 }
859}
860
861impl GpuKernel for KatzCentrality {
862 fn metadata(&self) -> &KernelMetadata {
863 &self.metadata
864 }
865}
866
867#[async_trait]
877impl BatchKernel<CentralityInput, CentralityOutput> for BetweennessCentrality {
878 async fn execute(&self, input: CentralityInput) -> Result<CentralityOutput> {
879 let start = Instant::now();
880 let normalized = input.normalize;
881 let result = Self::compute(&input.graph, normalized);
882 let compute_time_us = start.elapsed().as_micros() as u64;
883
884 Ok(CentralityOutput {
885 result,
886 compute_time_us,
887 })
888 }
889}
890
891#[async_trait]
892impl BatchKernel<CentralityInput, CentralityOutput> for ClosenessCentrality {
893 async fn execute(&self, input: CentralityInput) -> Result<CentralityOutput> {
894 let start = Instant::now();
895 let harmonic = match input.params {
896 CentralityParams::Closeness { harmonic } => harmonic,
897 _ => false,
898 };
899 let result = Self::compute(&input.graph, harmonic);
900 let compute_time_us = start.elapsed().as_micros() as u64;
901
902 Ok(CentralityOutput {
903 result,
904 compute_time_us,
905 })
906 }
907}
908
909#[async_trait]
910impl BatchKernel<CentralityInput, CentralityOutput> for EigenvectorCentrality {
911 async fn execute(&self, input: CentralityInput) -> Result<CentralityOutput> {
912 let start = Instant::now();
913 let max_iterations = input.max_iterations.unwrap_or(1000);
914 let tolerance = input.tolerance.unwrap_or(1e-6);
915 let result = Self::compute(&input.graph, max_iterations, tolerance);
916 let compute_time_us = start.elapsed().as_micros() as u64;
917
918 Ok(CentralityOutput {
919 result,
920 compute_time_us,
921 })
922 }
923}
924
925#[async_trait]
926impl BatchKernel<CentralityInput, CentralityOutput> for KatzCentrality {
927 async fn execute(&self, input: CentralityInput) -> Result<CentralityOutput> {
928 let start = Instant::now();
929 let (alpha, beta) = match input.params {
930 CentralityParams::Katz { alpha, beta } => (alpha, beta),
931 _ => (0.1, 1.0),
932 };
933 let max_iterations = input.max_iterations.unwrap_or(100);
934 let tolerance = input.tolerance.unwrap_or(1e-6);
935 let result = Self::compute(&input.graph, alpha, beta, max_iterations, tolerance);
936 let compute_time_us = start.elapsed().as_micros() as u64;
937
938 Ok(CentralityOutput {
939 result,
940 compute_time_us,
941 })
942 }
943}
944
945impl PageRank {
948 pub async fn compute_batch(
952 &self,
953 graph: CsrGraph,
954 damping: f32,
955 max_iterations: u32,
956 threshold: f64,
957 ) -> Result<CentralityResult> {
958 Self::run_to_convergence(graph, damping, max_iterations, threshold)
959 }
960}
961
962#[async_trait]
963impl BatchKernel<CentralityInput, CentralityOutput> for PageRank {
964 async fn execute(&self, input: CentralityInput) -> Result<CentralityOutput> {
965 let start = Instant::now();
966 let damping = match input.params {
967 CentralityParams::PageRank { damping } => damping,
968 _ => 0.85,
969 };
970 let max_iterations = input.max_iterations.unwrap_or(100);
971 let tolerance = input.tolerance.unwrap_or(1e-6);
972 let result = Self::run_to_convergence(input.graph, damping, max_iterations, tolerance)?;
973 let compute_time_us = start.elapsed().as_micros() as u64;
974
975 Ok(CentralityOutput {
976 result,
977 compute_time_us,
978 })
979 }
980}
981
982#[async_trait]
984impl BatchKernel<CentralityInput, CentralityOutput> for DegreeCentrality {
985 async fn execute(&self, input: CentralityInput) -> Result<CentralityOutput> {
986 let start = Instant::now();
987 let result = Self::compute(&input.graph);
988 let compute_time_us = start.elapsed().as_micros() as u64;
989
990 Ok(CentralityOutput {
991 result,
992 compute_time_us,
993 })
994 }
995}
996
997#[cfg(test)]
998mod tests {
999 use super::*;
1000
1001 fn create_test_graph() -> CsrGraph {
1002 CsrGraph::from_edges(4, &[(0, 1), (1, 2), (2, 3), (3, 0)])
1004 }
1005
1006 fn create_star_graph() -> CsrGraph {
1007 CsrGraph::from_edges(
1009 5,
1010 &[
1011 (0, 1),
1012 (0, 2),
1013 (0, 3),
1014 (0, 4),
1015 (1, 0),
1016 (2, 0),
1017 (3, 0),
1018 (4, 0),
1019 ],
1020 )
1021 }
1022
1023 #[test]
1024 fn test_pagerank_metadata() {
1025 let kernel = PageRank::new();
1026 assert_eq!(kernel.metadata().id, "graph/pagerank");
1027 assert_eq!(kernel.metadata().domain, Domain::GraphAnalytics);
1028 }
1029
1030 #[test]
1031 fn test_pagerank_iteration() {
1032 let graph = create_test_graph();
1033 let mut state = PageRank::initialize_state(graph, 0.85);
1034
1035 let diff = PageRank::iterate_step(&mut state);
1036 assert!(diff >= 0.0);
1037 assert_eq!(state.iteration, 1);
1038 }
1039
1040 #[test]
1041 fn test_pagerank_convergence() {
1042 let graph = create_test_graph();
1043 let result = PageRank::run_to_convergence(graph, 0.85, 100, 1e-6).unwrap();
1044
1045 assert!(result.converged);
1046 assert_eq!(result.scores.len(), 4);
1047
1048 let first_score = result.scores[0].score;
1050 for score in &result.scores {
1051 assert!((score.score - first_score).abs() < 0.01);
1052 }
1053 }
1054
1055 #[test]
1056 fn test_degree_centrality() {
1057 let graph = create_star_graph();
1058 let result = DegreeCentrality::compute(&graph);
1059
1060 assert_eq!(result.scores.len(), 5);
1061
1062 let center_score = result.scores[0].score;
1064 for score in &result.scores[1..] {
1065 assert!(center_score > score.score);
1066 }
1067 }
1068
1069 #[test]
1070 fn test_betweenness_centrality() {
1071 let graph = CsrGraph::from_edges(4, &[(0, 1), (1, 0), (1, 2), (2, 1), (2, 3), (3, 2)]);
1073
1074 let result = BetweennessCentrality::compute(&graph, false);
1075
1076 assert_eq!(result.scores.len(), 4);
1077
1078 let node_1_score = result.scores[1].score;
1080 let node_0_score = result.scores[0].score;
1081 assert!(node_1_score > node_0_score);
1082 }
1083
1084 #[test]
1085 fn test_closeness_centrality() {
1086 let graph = create_star_graph();
1087 let result = ClosenessCentrality::compute(&graph, false);
1088
1089 assert_eq!(result.scores.len(), 5);
1090
1091 let center_score = result.scores[0].score;
1093 for score in &result.scores[1..] {
1094 assert!(center_score >= score.score);
1095 }
1096 }
1097
1098 #[test]
1099 fn test_eigenvector_centrality() {
1100 let graph = create_star_graph();
1101 let result = EigenvectorCentrality::compute(&graph, 1000, 1e-4);
1102
1103 assert_eq!(result.scores.len(), 5);
1105
1106 let center_score = result.scores[0].score;
1109 assert!(center_score > 0.0);
1110 }
1111
1112 #[test]
1113 fn test_katz_centrality() {
1114 let graph = create_star_graph();
1115 let result = KatzCentrality::compute(&graph, 0.1, 1.0, 100, 1e-6);
1116
1117 assert!(result.converged);
1118 assert_eq!(result.scores.len(), 5);
1119 }
1120}