Skip to main content

fso_graph/
page_rank.rs

1use crate::{prelude::*, DEFAULT_PARALLELISM};
2
3use atomic_float::AtomicF64;
4use fso_graph_builder::SharedMut;
5use log::info;
6use rayon::prelude::*;
7
8use std::sync::atomic::Ordering;
9use std::thread::available_parallelism;
10use std::time::Instant;
11
12const CHUNK_SIZE: usize = 16384;
13
14#[derive(Copy, Clone, Debug)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16#[cfg_attr(feature = "clap", derive(clap::Args))]
17pub struct PageRankConfig {
18    /// The maximum number of page rank iterations.
19    #[cfg_attr(feature = "clap", clap(long, default_value_t = PageRankConfig::DEFAULT_MAX_ITERATIONS))]
20    pub max_iterations: usize,
21
22    /// If the sum of page rank deltas per iteration is
23    /// below the tolerance value, the computation stop.
24    #[cfg_attr(feature = "clap", clap(long, default_value_t = PageRankConfig::DEFAULT_TOLERANCE))]
25    pub tolerance: f64,
26
27    /// Imagining a random surfer clicking links, the
28    /// damping factor defines the probability if the
29    /// surfer will continue at any step.
30    #[cfg_attr(feature = "clap", clap(long, default_value_t = PageRankConfig::DEFAULT_DAMPING_FACTOR))]
31    pub damping_factor: f32,
32}
33
34impl Default for PageRankConfig {
35    fn default() -> Self {
36        Self {
37            max_iterations: Self::DEFAULT_MAX_ITERATIONS,
38            tolerance: Self::DEFAULT_TOLERANCE,
39            damping_factor: Self::DEFAULT_DAMPING_FACTOR,
40        }
41    }
42}
43
44impl PageRankConfig {
45    pub const DEFAULT_MAX_ITERATIONS: usize = 20;
46    pub const DEFAULT_TOLERANCE: f64 = 1E-4;
47    pub const DEFAULT_DAMPING_FACTOR: f32 = 0.85;
48
49    pub fn new(max_iterations: usize, tolerance: f64, damping_factor: f32) -> Self {
50        Self {
51            max_iterations,
52            tolerance,
53            damping_factor,
54        }
55    }
56}
57
58pub fn page_rank<NI, G>(graph: &G, config: PageRankConfig) -> (Vec<f32>, usize, f64)
59where
60    NI: Idx,
61    G: Graph<NI> + DirectedDegrees<NI> + DirectedNeighbors<NI> + Sync,
62{
63    let PageRankConfig {
64        max_iterations,
65        tolerance,
66        damping_factor,
67    } = config;
68
69    let node_count = graph.node_count().index();
70    let init_score = 1_f32 / node_count as f32;
71    let base_score = (1.0_f32 - damping_factor) / node_count as f32;
72
73    let mut out_scores = Vec::with_capacity(node_count);
74
75    (0..node_count)
76        .into_par_iter()
77        .map(NI::new)
78        .map(|node| init_score / graph.out_degree(node).index() as f32)
79        .collect_into_vec(&mut out_scores);
80
81    let mut scores = vec![init_score; node_count];
82
83    let scores_ptr = SharedMut::new(scores.as_mut_ptr());
84    let out_scores_ptr = SharedMut::new(out_scores.as_mut_ptr());
85
86    let mut iteration = 0;
87
88    loop {
89        let start = Instant::now();
90        let error = page_rank_iteration(
91            graph,
92            base_score,
93            damping_factor,
94            &out_scores_ptr,
95            &scores_ptr,
96        );
97
98        info!(
99            "Finished iteration {} with an error of {:.6} in {:?}",
100            iteration,
101            error,
102            start.elapsed()
103        );
104
105        iteration += 1;
106
107        if error < tolerance || iteration == max_iterations {
108            return (scores, iteration, error);
109        }
110    }
111}
112
113fn page_rank_iteration<NI, G>(
114    graph: &G,
115    base_score: f32,
116    damping_factor: f32,
117    out_scores: &SharedMut<f32>,
118    scores: &SharedMut<f32>,
119) -> f64
120where
121    NI: Idx,
122    G: Graph<NI> + DirectedDegrees<NI> + DirectedNeighbors<NI> + Sync,
123{
124    let next_chunk = Atomic::new(NI::zero());
125    let total_error = AtomicF64::new(0_f64);
126
127    std::thread::scope(|s| {
128        let num_threads = available_parallelism().map_or(DEFAULT_PARALLELISM, |p| p.get());
129
130        for _ in 0..num_threads {
131            s.spawn(|| {
132                let mut error = 0_f64;
133
134                loop {
135                    let start = NI::fetch_add(&next_chunk, NI::new(CHUNK_SIZE), Ordering::AcqRel);
136                    if start >= graph.node_count() {
137                        break;
138                    }
139
140                    let end = (start + NI::new(CHUNK_SIZE)).min(graph.node_count());
141
142                    for u in start.range(end) {
143                        let incoming_total = graph
144                            .in_neighbors(u)
145                            .map(|v| unsafe { out_scores.add(v.index()).read() })
146                            .sum::<f32>();
147
148                        let old_score = unsafe { scores.add(u.index()).read() };
149                        let new_score = base_score + damping_factor * incoming_total;
150
151                        unsafe { scores.add(u.index()).write(new_score) };
152                        let diff = (new_score - old_score) as f64;
153                        error += f64::abs(diff);
154
155                        unsafe {
156                            out_scores
157                                .add(u.index())
158                                .write(new_score / graph.out_degree(u).index() as f32)
159                        }
160                    }
161                }
162                total_error.fetch_add(error, Ordering::SeqCst);
163            });
164        }
165    });
166
167    total_error.load(Ordering::SeqCst)
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use crate::prelude::{CsrLayout, GraphBuilder};
174
175    #[test]
176    fn test_pr_two_components() {
177        let gdl = "(a)-->()-->()<--(a),(b)-->()-->()<--(b)";
178
179        let graph: DirectedCsrGraph<usize> = GraphBuilder::new()
180            .csr_layout(CsrLayout::Sorted)
181            .gdl_str::<usize, _>(gdl)
182            .build()
183            .unwrap();
184
185        let (scores, _, _) = page_rank(&graph, PageRankConfig::default());
186
187        let expected: Vec<f32> = vec![
188            0.024999997,
189            0.035624996,
190            0.06590624,
191            0.024999997,
192            0.035624996,
193            0.06590624,
194        ];
195
196        assert_eq!(scores, expected);
197    }
198}