use std::ptr;
use cugraph_sys::{
cugraph_centrality_result_t, cugraph_error_t, cugraph_pagerank_allow_nonconvergence,
cugraph_personalized_pagerank_allow_nonconvergence,
};
use super::CentralityResult;
use crate::{
array::DeviceArrayView,
cuda::{DeviceResourceKind, drop_device_resource},
error::{Error, ErrorKind, Result, check_error, require_non_null},
graph::{Graph, Transposed},
handle::EnteredContext,
params::{ClosedUnitF64, IterationCount, NonNegativeF64, defaults},
types::{CugraphElement, to_bool_t},
};
#[derive(Debug, Clone, Copy)]
#[must_use]
pub struct PageRankParams {
pub alpha: ClosedUnitF64,
pub epsilon: NonNegativeF64,
pub max_iterations: IterationCount,
pub do_expensive_check: bool,
}
impl Default for PageRankParams {
fn default() -> Self {
Self {
alpha: defaults::PAGERANK_ALPHA,
epsilon: defaults::PAGERANK_EPSILON,
max_iterations: defaults::PAGERANK_MAX_ITERATIONS,
do_expensive_check: false,
}
}
}
#[derive(Debug, Clone, Copy)]
#[must_use]
pub struct PersonalizedPageRankParams {
pub alpha: ClosedUnitF64,
pub epsilon: NonNegativeF64,
pub max_iterations: IterationCount,
pub do_expensive_check: bool,
}
impl Default for PersonalizedPageRankParams {
fn default() -> Self {
Self {
alpha: defaults::PAGERANK_ALPHA,
epsilon: defaults::PAGERANK_EPSILON,
max_iterations: defaults::PAGERANK_MAX_ITERATIONS,
do_expensive_check: false,
}
}
}
#[derive(Debug)]
pub struct PrecomputedWeights<'a, V: CugraphElement = i32, W: CugraphElement = f32> {
pub vertices: &'a DeviceArrayView<'a, V>,
pub sums: &'a DeviceArrayView<'a, W>,
}
#[derive(Debug)]
pub struct InitialGuess<'a, V: CugraphElement = i32, W: CugraphElement = f32> {
pub vertices: &'a DeviceArrayView<'a, V>,
pub values: &'a DeviceArrayView<'a, W>,
}
#[derive(Debug)]
pub struct PersonalizationVector<'a, V: CugraphElement = i32, W: CugraphElement = f32> {
pub vertices: &'a DeviceArrayView<'a, V>,
pub values: &'a DeviceArrayView<'a, W>,
}
#[derive(Debug, Clone, Copy)]
enum PageRankCall {
Standard,
Personalized,
}
pub(crate) fn pagerank<V: CugraphElement, W: CugraphElement>(
handle: &EnteredContext<'_>,
graph: &mut Graph<Transposed>,
params: &PageRankParams,
precomputed_weights: Option<&PrecomputedWeights<'_, V, W>>,
initial_guess: Option<&InitialGuess<'_, V, W>>,
) -> Result<CentralityResult> {
pagerank_impl(
handle,
graph,
params.alpha,
params.epsilon,
params.max_iterations,
params.do_expensive_check,
precomputed_weights,
initial_guess,
None,
PageRankCall::Standard,
"pagerank",
)
}
pub(crate) fn personalized_pagerank<V: CugraphElement, W: CugraphElement>(
handle: &EnteredContext<'_>,
graph: &mut Graph<Transposed>,
params: &PersonalizedPageRankParams,
personalization: &PersonalizationVector<'_, V, W>,
precomputed_weights: Option<&PrecomputedWeights<'_, V, W>>,
initial_guess: Option<&InitialGuess<'_, V, W>>,
) -> Result<CentralityResult> {
pagerank_impl(
handle,
graph,
params.alpha,
params.epsilon,
params.max_iterations,
params.do_expensive_check,
precomputed_weights,
initial_guess,
Some(personalization),
PageRankCall::Personalized,
"personalized_pagerank",
)
}
#[expect(clippy::too_many_arguments, reason = "all parameters required to invoke PageRank FFI")]
fn pagerank_impl<V: CugraphElement, W: CugraphElement>(
handle: &EnteredContext<'_>,
graph: &mut Graph<Transposed>,
alpha: ClosedUnitF64,
epsilon: NonNegativeF64,
max_iterations: IterationCount,
do_expensive_check: bool,
precomputed_weights: Option<&PrecomputedWeights<'_, V, W>>,
initial_guess: Option<&InitialGuess<'_, V, W>>,
personalization: Option<&PersonalizationVector<'_, V, W>>,
call: PageRankCall,
operation: &'static str,
) -> Result<CentralityResult> {
if let Some(pw) = precomputed_weights {
if pw.vertices.len() != pw.sums.len() {
return Err(Error::new(ErrorKind::InvalidInput, operation)
.with_message("precomputed weights: vertices and sums must have the same length")
.with_context("vertices_len", pw.vertices.len().to_string())
.with_context("sums_len", pw.sums.len().to_string()));
}
validate_vertex_dtype::<V>(graph, operation, "precomputed weights vertices")?;
validate_weight_dtype::<W>(operation, "precomputed weights sums")?;
}
if let Some(ig) = initial_guess {
if ig.vertices.len() != ig.values.len() {
return Err(Error::new(ErrorKind::InvalidInput, operation)
.with_message("initial guess: vertices and values must have the same length")
.with_context("vertices_len", ig.vertices.len().to_string())
.with_context("values_len", ig.values.len().to_string()));
}
validate_vertex_dtype::<V>(graph, operation, "initial guess vertices")?;
validate_weight_dtype::<W>(operation, "initial guess values")?;
}
if let Some(personalization) = personalization {
if personalization.vertices.len() != personalization.values.len() {
return Err(Error::new(ErrorKind::InvalidInput, operation)
.with_message("personalization: vertices and values must have the same length")
.with_context("vertices_len", personalization.vertices.len().to_string())
.with_context("values_len", personalization.values.len().to_string()));
}
validate_vertex_dtype::<V>(graph, operation, "personalization vertices")?;
validate_weight_dtype::<W>(operation, "personalization values")?;
}
let (pw_verts_ptr, pw_sums_ptr) = match precomputed_weights {
Some(pw) => (pw.vertices.as_ptr() as *const _, pw.sums.as_ptr() as *const _),
None => (ptr::null(), ptr::null()),
};
let (ig_verts_ptr, ig_vals_ptr) = match initial_guess {
Some(ig) => (ig.vertices.as_ptr() as *const _, ig.values.as_ptr() as *const _),
None => (ptr::null(), ptr::null()),
};
let (personalization_verts_ptr, personalization_vals_ptr) = match personalization {
Some(p) => (p.vertices.as_ptr() as *const _, p.values.as_ptr() as *const _),
None => (ptr::null(), ptr::null()),
};
let do_expensive_check =
do_expensive_check || precomputed_weights.is_some() || initial_guess.is_some() || personalization.is_some();
let mut result: *mut cugraph_centrality_result_t = ptr::null_mut();
let mut error: *mut cugraph_error_t = ptr::null_mut();
let graph_ptr = graph.as_ptr(handle, operation)?;
unsafe {
let code = match call {
PageRankCall::Standard => cugraph_pagerank_allow_nonconvergence(
handle.handle_ptr(),
graph_ptr,
pw_verts_ptr,
pw_sums_ptr,
ig_verts_ptr,
ig_vals_ptr,
alpha.get(),
epsilon.get(),
usize::from(max_iterations),
to_bool_t(do_expensive_check),
&mut result,
&mut error,
),
PageRankCall::Personalized => cugraph_personalized_pagerank_allow_nonconvergence(
handle.handle_ptr(),
graph_ptr,
pw_verts_ptr,
pw_sums_ptr,
ig_verts_ptr,
ig_vals_ptr,
personalization_verts_ptr,
personalization_vals_ptr,
alpha.get(),
epsilon.get(),
usize::from(max_iterations),
to_bool_t(do_expensive_check),
&mut result,
&mut error,
),
};
if let Err(err) = check_error(code, error, handle.device(), operation) {
if !result.is_null() {
drop_device_resource(&mut result, handle.device(), operation, DeviceResourceKind::CentralityResult);
}
return Err(err);
}
}
let result = require_non_null(result, operation, "centrality_result")?;
unsafe { CentralityResult::from_raw(result, handle.device(), handle.runtime_bound_lease()) }
}
fn validate_vertex_dtype<V: CugraphElement>(
graph: &Graph<Transposed>,
operation: &'static str,
input_name: &str,
) -> Result<()> {
let dtype = V::data_type();
if !dtype.is_supported_index_type() {
return Err(Error::new(ErrorKind::UnsupportedType, operation)
.with_message(format!("{input_name} require Int32 or Int64 vertex ids, got {dtype}")));
}
if dtype != graph.vertex_dtype() {
return Err(Error::new(ErrorKind::UnsupportedType, operation).with_message(format!(
"{input_name} vertex dtype {dtype} does not match graph vertex dtype {}",
graph.vertex_dtype()
)));
}
Ok(())
}
fn validate_weight_dtype<W: CugraphElement>(operation: &'static str, input_name: &str) -> Result<()> {
let dtype = W::data_type();
if !dtype.is_float() {
return Err(Error::new(ErrorKind::UnsupportedType, operation)
.with_message(format!("{input_name} require Float32 or Float64 values, got {dtype}")));
}
Ok(())
}