Skip to main content

cugraph/centrality/
pagerank.rs

1//! PageRank centrality algorithm.
2
3use std::ptr;
4
5use cugraph_sys::{
6    cugraph_centrality_result_t, cugraph_error_t, cugraph_pagerank_allow_nonconvergence,
7    cugraph_personalized_pagerank_allow_nonconvergence,
8};
9
10use super::CentralityResult;
11use crate::{
12    array::DeviceArrayView,
13    cuda::{DeviceResourceKind, drop_device_resource},
14    error::{Error, ErrorKind, Result, check_error, require_non_null},
15    graph::{Graph, Transposed},
16    handle::EnteredContext,
17    params::{ClosedUnitF64, IterationCount, NonNegativeF64, defaults},
18    types::{CugraphElement, to_bool_t},
19};
20
21/// Parameters for the PageRank algorithm.
22///
23/// Baseline configuration is provided by [`Default`].
24///
25/// # Example
26///
27/// ```
28/// use cugraph::centrality::PageRankParams;
29///
30/// // Default parameters
31/// let params = PageRankParams::default();
32/// assert_eq!(params.alpha.get(), 0.85);
33///
34/// // Custom parameters
35/// use cugraph::params::{ClosedUnitF64, IterationCount, NonNegativeF64};
36/// let params = PageRankParams {
37///     alpha: ClosedUnitF64::new(0.9).unwrap(),
38///     epsilon: NonNegativeF64::new(1e-6).unwrap(),
39///     max_iterations: IterationCount::new(200).unwrap(),
40///     do_expensive_check: false,
41/// };
42/// ```
43#[derive(Debug, Clone, Copy)]
44#[must_use]
45pub struct PageRankParams {
46    /// Damping factor (default 0.85). Must be in the closed interval `[0.0, 1.0]`.
47    pub alpha: ClosedUnitF64,
48    /// Convergence tolerance (default 1e-5). Must be non-negative. Zero
49    /// disables early convergence (always runs for `max_iterations`).
50    pub epsilon: NonNegativeF64,
51    /// Maximum number of iterations (default 100). Must be at least 1.
52    pub max_iterations: IterationCount,
53    /// Whether to run expensive input validation checks in the C library
54    /// (default: `false`).
55    pub do_expensive_check: bool,
56}
57
58impl Default for PageRankParams {
59    fn default() -> Self {
60        Self {
61            alpha: defaults::PAGERANK_ALPHA,
62            epsilon: defaults::PAGERANK_EPSILON,
63            max_iterations: defaults::PAGERANK_MAX_ITERATIONS,
64            do_expensive_check: false,
65        }
66    }
67}
68
69/// Parameters for personalized PageRank.
70///
71/// These have the same invariants and defaults as [`PageRankParams`], but are
72/// kept as a distinct public type because personalized PageRank requires an
73/// explicit personalization vector.
74#[derive(Debug, Clone, Copy)]
75#[must_use]
76pub struct PersonalizedPageRankParams {
77    /// Damping factor (default 0.85). Must be in the closed interval `[0.0, 1.0]`.
78    pub alpha: ClosedUnitF64,
79    /// Convergence tolerance (default 1e-5). Must be non-negative.
80    pub epsilon: NonNegativeF64,
81    /// Maximum number of iterations (default 100). Must be at least 1.
82    pub max_iterations: IterationCount,
83    /// Whether to run expensive input validation checks in the C library
84    /// (default: `false`).
85    pub do_expensive_check: bool,
86}
87
88impl Default for PersonalizedPageRankParams {
89    fn default() -> Self {
90        Self {
91            alpha: defaults::PAGERANK_ALPHA,
92            epsilon: defaults::PAGERANK_EPSILON,
93            max_iterations: defaults::PAGERANK_MAX_ITERATIONS,
94            do_expensive_check: false,
95        }
96    }
97}
98
99/// Precomputed vertex out-weight sums for PageRank.
100///
101/// Providing these avoids recomputation inside the C library and can
102/// improve performance on repeated runs with the same graph.
103#[derive(Debug)]
104pub struct PrecomputedWeights<'a, V: CugraphElement = i32, W: CugraphElement = f32> {
105    /// Vertex ids for which out-weight sums are provided.
106    pub vertices: &'a DeviceArrayView<'a, V>,
107    /// Precomputed sum of out-edge weights for each vertex.
108    pub sums: &'a DeviceArrayView<'a, W>,
109}
110
111/// Initial guess of PageRank values.
112///
113/// When provided, the solver starts from these values. Without an initial
114/// guess, the solver starts from the uniform `1/N` default. Useful for
115/// iterative refinement.
116#[derive(Debug)]
117pub struct InitialGuess<'a, V: CugraphElement = i32, W: CugraphElement = f32> {
118    /// Vertex ids for the initial guess.
119    pub vertices: &'a DeviceArrayView<'a, V>,
120    /// Initial PageRank values for each vertex.
121    pub values: &'a DeviceArrayView<'a, W>,
122}
123
124/// Personalization vector for personalized PageRank.
125#[derive(Debug)]
126pub struct PersonalizationVector<'a, V: CugraphElement = i32, W: CugraphElement = f32> {
127    /// Vertex ids in the personalization set.
128    pub vertices: &'a DeviceArrayView<'a, V>,
129    /// Personalization values for each vertex.
130    pub values: &'a DeviceArrayView<'a, W>,
131}
132
133#[derive(Debug, Clone, Copy)]
134enum PageRankCall {
135    Standard,
136    Personalized,
137}
138
139/// Compute PageRank on a graph.
140///
141/// Callers use [`Graph::pagerank`](crate::graph::Graph::pagerank) or
142/// [`Graph::pagerank_with`](crate::graph::Graph::pagerank_with), which call
143/// this function.
144pub(crate) fn pagerank<V: CugraphElement, W: CugraphElement>(
145    handle: &EnteredContext<'_>,
146    graph: &mut Graph<Transposed>,
147    params: &PageRankParams,
148    precomputed_weights: Option<&PrecomputedWeights<'_, V, W>>,
149    initial_guess: Option<&InitialGuess<'_, V, W>>,
150) -> Result<CentralityResult> {
151    pagerank_impl(
152        handle,
153        graph,
154        params.alpha,
155        params.epsilon,
156        params.max_iterations,
157        params.do_expensive_check,
158        precomputed_weights,
159        initial_guess,
160        None,
161        PageRankCall::Standard,
162        "pagerank",
163    )
164}
165
166pub(crate) fn personalized_pagerank<V: CugraphElement, W: CugraphElement>(
167    handle: &EnteredContext<'_>,
168    graph: &mut Graph<Transposed>,
169    params: &PersonalizedPageRankParams,
170    personalization: &PersonalizationVector<'_, V, W>,
171    precomputed_weights: Option<&PrecomputedWeights<'_, V, W>>,
172    initial_guess: Option<&InitialGuess<'_, V, W>>,
173) -> Result<CentralityResult> {
174    pagerank_impl(
175        handle,
176        graph,
177        params.alpha,
178        params.epsilon,
179        params.max_iterations,
180        params.do_expensive_check,
181        precomputed_weights,
182        initial_guess,
183        Some(personalization),
184        PageRankCall::Personalized,
185        "personalized_pagerank",
186    )
187}
188
189#[expect(clippy::too_many_arguments, reason = "all parameters required to invoke PageRank FFI")]
190fn pagerank_impl<V: CugraphElement, W: CugraphElement>(
191    handle: &EnteredContext<'_>,
192    graph: &mut Graph<Transposed>,
193    alpha: ClosedUnitF64,
194    epsilon: NonNegativeF64,
195    max_iterations: IterationCount,
196    do_expensive_check: bool,
197    precomputed_weights: Option<&PrecomputedWeights<'_, V, W>>,
198    initial_guess: Option<&InitialGuess<'_, V, W>>,
199    personalization: Option<&PersonalizationVector<'_, V, W>>,
200    call: PageRankCall,
201    operation: &'static str,
202) -> Result<CentralityResult> {
203    if let Some(pw) = precomputed_weights {
204        if pw.vertices.len() != pw.sums.len() {
205            return Err(Error::new(ErrorKind::InvalidInput, operation)
206                .with_message("precomputed weights: vertices and sums must have the same length")
207                .with_context("vertices_len", pw.vertices.len().to_string())
208                .with_context("sums_len", pw.sums.len().to_string()));
209        }
210        validate_vertex_dtype::<V>(graph, operation, "precomputed weights vertices")?;
211        validate_weight_dtype::<W>(operation, "precomputed weights sums")?;
212    }
213
214    if let Some(ig) = initial_guess {
215        if ig.vertices.len() != ig.values.len() {
216            return Err(Error::new(ErrorKind::InvalidInput, operation)
217                .with_message("initial guess: vertices and values must have the same length")
218                .with_context("vertices_len", ig.vertices.len().to_string())
219                .with_context("values_len", ig.values.len().to_string()));
220        }
221        validate_vertex_dtype::<V>(graph, operation, "initial guess vertices")?;
222        validate_weight_dtype::<W>(operation, "initial guess values")?;
223    }
224
225    if let Some(personalization) = personalization {
226        if personalization.vertices.len() != personalization.values.len() {
227            return Err(Error::new(ErrorKind::InvalidInput, operation)
228                .with_message("personalization: vertices and values must have the same length")
229                .with_context("vertices_len", personalization.vertices.len().to_string())
230                .with_context("values_len", personalization.values.len().to_string()));
231        }
232        validate_vertex_dtype::<V>(graph, operation, "personalization vertices")?;
233        validate_weight_dtype::<W>(operation, "personalization values")?;
234    }
235
236    let (pw_verts_ptr, pw_sums_ptr) = match precomputed_weights {
237        Some(pw) => (pw.vertices.as_ptr() as *const _, pw.sums.as_ptr() as *const _),
238        None => (ptr::null(), ptr::null()),
239    };
240
241    let (ig_verts_ptr, ig_vals_ptr) = match initial_guess {
242        Some(ig) => (ig.vertices.as_ptr() as *const _, ig.values.as_ptr() as *const _),
243        None => (ptr::null(), ptr::null()),
244    };
245
246    let (personalization_verts_ptr, personalization_vals_ptr) = match personalization {
247        Some(p) => (p.vertices.as_ptr() as *const _, p.values.as_ptr() as *const _),
248        None => (ptr::null(), ptr::null()),
249    };
250    let do_expensive_check =
251        do_expensive_check || precomputed_weights.is_some() || initial_guess.is_some() || personalization.is_some();
252
253    let mut result: *mut cugraph_centrality_result_t = ptr::null_mut();
254    let mut error: *mut cugraph_error_t = ptr::null_mut();
255    let graph_ptr = graph.as_ptr(handle, operation)?;
256
257    // SAFETY: all optional input pointers are either null or borrowed from
258    // validated device views that outlive the call. Non-null user vertex
259    // arrays force libcugraph validation so out-of-range vertices cannot drive
260    // unchecked native indexing. `result`/`error` are valid out-pointers.
261    unsafe {
262        let code = match call {
263            PageRankCall::Standard => cugraph_pagerank_allow_nonconvergence(
264                handle.handle_ptr(),
265                graph_ptr,
266                pw_verts_ptr,
267                pw_sums_ptr,
268                ig_verts_ptr,
269                ig_vals_ptr,
270                alpha.get(),
271                epsilon.get(),
272                usize::from(max_iterations),
273                to_bool_t(do_expensive_check),
274                &mut result,
275                &mut error,
276            ),
277            PageRankCall::Personalized => cugraph_personalized_pagerank_allow_nonconvergence(
278                handle.handle_ptr(),
279                graph_ptr,
280                pw_verts_ptr,
281                pw_sums_ptr,
282                ig_verts_ptr,
283                ig_vals_ptr,
284                personalization_verts_ptr,
285                personalization_vals_ptr,
286                alpha.get(),
287                epsilon.get(),
288                usize::from(max_iterations),
289                to_bool_t(do_expensive_check),
290                &mut result,
291                &mut error,
292            ),
293        };
294        if let Err(err) = check_error(code, error, handle.device(), operation) {
295            if !result.is_null() {
296                drop_device_resource(&mut result, handle.device(), operation, DeviceResourceKind::CentralityResult);
297            }
298            return Err(err);
299        }
300    }
301
302    let result = require_non_null(result, operation, "centrality_result")?;
303
304    // SAFETY: on success, cuGraph returns ownership of `result` to the caller.
305    unsafe { CentralityResult::from_raw(result, handle.device(), handle.runtime_bound_lease()) }
306}
307
308fn validate_vertex_dtype<V: CugraphElement>(
309    graph: &Graph<Transposed>,
310    operation: &'static str,
311    input_name: &str,
312) -> Result<()> {
313    let dtype = V::data_type();
314    if !dtype.is_supported_index_type() {
315        return Err(Error::new(ErrorKind::UnsupportedType, operation)
316            .with_message(format!("{input_name} require Int32 or Int64 vertex ids, got {dtype}")));
317    }
318    if dtype != graph.vertex_dtype() {
319        return Err(Error::new(ErrorKind::UnsupportedType, operation).with_message(format!(
320            "{input_name} vertex dtype {dtype} does not match graph vertex dtype {}",
321            graph.vertex_dtype()
322        )));
323    }
324    Ok(())
325}
326
327fn validate_weight_dtype<W: CugraphElement>(operation: &'static str, input_name: &str) -> Result<()> {
328    let dtype = W::data_type();
329    if !dtype.is_float() {
330        return Err(Error::new(ErrorKind::UnsupportedType, operation)
331            .with_message(format!("{input_name} require Float32 or Float64 values, got {dtype}")));
332    }
333    Ok(())
334}