use std::{marker::PhantomData, ptr};
use cugraph_sys::{
cugraph_error_t, cugraph_force_atlas2, cugraph_layout_result_get_vertices, cugraph_layout_result_get_x_axis,
cugraph_layout_result_get_y_axis, cugraph_layout_result_t,
};
use crate::{
array::{DeviceArrayView, RawDeviceArrayView},
cuda::{DeviceResourceKind, drop_device_resource},
cudf_interop::layout_result_to_dataframe,
error::{Error, ErrorKind, Result, check_error, require_non_null},
graph::{Graph, Transposition},
handle::{EnteredContext, ResourceHandleLease},
params::{ClosedUnitF64, IterationCount, NonNegativeF64, PositiveF64, defaults},
random::RngState,
structure::{
VertexSelection,
typed_results::{ScoreValues, VertexIds},
},
types::{CugraphElement, DataType, to_bool_t},
};
#[derive(Debug, Clone, Copy)]
#[must_use]
pub struct ForceAtlas2Params {
pub max_iter: IterationCount,
pub outbound_attraction_distribution: bool,
pub lin_log_mode: bool,
pub prevent_overlapping: bool,
pub overlap_scaling_ratio: PositiveF64,
pub edge_weight_influence: NonNegativeF64,
pub jitter_tolerance: NonNegativeF64,
pub barnes_hut_optimize: bool,
pub barnes_hut_theta: ClosedUnitF64,
pub scaling_ratio: PositiveF64,
pub strong_gravity_mode: bool,
pub gravity: NonNegativeF64,
pub verbose: bool,
pub do_expensive_check: bool,
}
impl Default for ForceAtlas2Params {
fn default() -> Self {
Self {
max_iter: defaults::FORCE_ATLAS2_MAX_ITER,
outbound_attraction_distribution: false,
lin_log_mode: false,
prevent_overlapping: false,
overlap_scaling_ratio: defaults::FORCE_ATLAS2_OVERLAP_SCALING_RATIO,
edge_weight_influence: defaults::FORCE_ATLAS2_EDGE_WEIGHT_INFLUENCE,
jitter_tolerance: defaults::FORCE_ATLAS2_JITTER_TOLERANCE,
barnes_hut_optimize: false,
barnes_hut_theta: defaults::FORCE_ATLAS2_BARNES_HUT_THETA,
scaling_ratio: defaults::FORCE_ATLAS2_SCALING_RATIO,
strong_gravity_mode: false,
gravity: defaults::FORCE_ATLAS2_GRAVITY,
verbose: false,
do_expensive_check: false,
}
}
}
impl<V: CugraphElement, W: CugraphElement> std::fmt::Debug for ForceAtlas2VertexAttrs<'_, V, W> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ForceAtlas2VertexAttrs")
.field("radius", &self.radius.is_some())
.field("mobility", &self.mobility.is_some())
.field("mass", &self.mass.is_some())
.finish()
}
}
pub struct ForceAtlas2VertexAttrs<'a, V: CugraphElement = i32, W: CugraphElement = f32> {
pub radius: Option<(&'a DeviceArrayView<'a, V>, &'a DeviceArrayView<'a, W>)>,
pub mobility: Option<(&'a DeviceArrayView<'a, V>, &'a DeviceArrayView<'a, W>)>,
pub mass: Option<(&'a DeviceArrayView<'a, V>, &'a DeviceArrayView<'a, W>)>,
}
impl<V: CugraphElement, W: CugraphElement> Default for ForceAtlas2VertexAttrs<'_, V, W> {
fn default() -> Self {
Self { radius: None, mobility: None, mass: None }
}
}
impl<V: CugraphElement, W: CugraphElement> std::fmt::Debug for InitialPositions<'_, V, W> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InitialPositions").finish_non_exhaustive()
}
}
pub struct InitialPositions<'a, V: CugraphElement = i32, W: CugraphElement = f32> {
pub vertices: &'a DeviceArrayView<'a, V>,
pub x: &'a DeviceArrayView<'a, W>,
pub y: &'a DeviceArrayView<'a, W>,
}
pub struct LayoutResult {
raw: *mut cugraph_layout_result_t,
device: i32,
_resource_handle_lease: Option<ResourceHandleLease>,
_nosend_nosync: PhantomData<*mut ()>,
}
impl std::fmt::Debug for LayoutResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LayoutResult").finish_non_exhaustive()
}
}
impl LayoutResult {
unsafe fn from_raw(
raw: *mut cugraph_layout_result_t,
device: i32,
resource_handle_lease: Option<ResourceHandleLease>,
) -> Result<Self> {
let raw = require_non_null(raw, "LayoutResult::from_raw", "layout_result")?;
Ok(Self { raw, device, _resource_handle_lease: resource_handle_lease, _nosend_nosync: PhantomData })
}
pub fn vertex_ids(&self) -> Result<VertexIds<'_>> {
let view_ptr = require_non_null(
unsafe { cugraph_layout_result_get_vertices(self.raw) },
"LayoutResult::vertex_ids",
"vertices",
)?;
let raw = unsafe {
RawDeviceArrayView::from_owned_view(view_ptr, Some(self.device), "LayoutResult::vertex_ids", "vertices")
}?;
VertexIds::from_raw(raw)
}
pub fn x(&self) -> Result<ScoreValues<'_>> {
let view_ptr =
require_non_null(unsafe { cugraph_layout_result_get_x_axis(self.raw) }, "LayoutResult::x", "x_axis")?;
let raw =
unsafe { RawDeviceArrayView::from_owned_view(view_ptr, Some(self.device), "LayoutResult::x", "x_axis") }?;
ScoreValues::from_raw(raw)
}
pub fn y(&self) -> Result<ScoreValues<'_>> {
let view_ptr =
require_non_null(unsafe { cugraph_layout_result_get_y_axis(self.raw) }, "LayoutResult::y", "y_axis")?;
let raw =
unsafe { RawDeviceArrayView::from_owned_view(view_ptr, Some(self.device), "LayoutResult::y", "y_axis") }?;
ScoreValues::from_raw(raw)
}
pub fn to_dataframe(&self, runtime: &cudf::Runtime) -> Result<cudf::DataFrame> {
layout_result_to_dataframe(self, runtime).map_err(|e: Error| e.with_operation("LayoutResult::to_dataframe"))
}
}
impl Drop for LayoutResult {
fn drop(&mut self) {
drop_device_resource(&mut self.raw, self.device, "LayoutResult::drop", DeviceResourceKind::LayoutResult);
}
}
fn validate_force_atlas2_vertex_helper_dtype<T, V: CugraphElement>(
graph: &Graph<T>,
input_name: &'static str,
) -> Result<()> {
let dtype = V::data_type();
if !dtype.is_supported_index_type() {
return Err(Error::new(ErrorKind::UnsupportedType, "force_atlas2")
.with_message(format!("{input_name} require Int32 or Int64 vertex ids, got {dtype}")));
}
if dtype != graph.vertex_dtype() {
return Err(Error::new(ErrorKind::UnsupportedType, "force_atlas2").with_message(format!(
"{input_name} vertex dtype {dtype} does not match graph vertex dtype {}",
graph.vertex_dtype()
)));
}
Ok(())
}
fn validate_force_atlas2_value_helper_dtype<T, W: CugraphElement>(
_graph: &Graph<T>,
input_name: &'static str,
) -> Result<()> {
let dtype = W::data_type();
if dtype != DataType::Float32 {
return Err(Error::new(ErrorKind::UnsupportedType, "force_atlas2")
.with_message(format!("{input_name} require Float32 values, got {dtype}")));
}
Ok(())
}
fn graph_vertex_count<T: Transposition>(handle: &EnteredContext<'_>, graph: &mut Graph<T>) -> Result<usize> {
let degrees = graph.out_degrees(handle, VertexSelection::<i32>::All)?;
let vertex_ids = degrees.vertex_ids()?;
Ok(vertex_ids.len())
}
pub(crate) fn force_atlas2<T: Transposition, V: CugraphElement, W: CugraphElement>(
handle: &EnteredContext<'_>,
rng_state: &mut RngState,
graph: &mut Graph<T>,
params: &ForceAtlas2Params,
vertex_attrs: Option<&ForceAtlas2VertexAttrs<'_, V, W>>,
initial_pos: Option<&InitialPositions<'_, V, W>>,
) -> Result<LayoutResult> {
let graph_vertex_count =
if initial_pos.is_some() || vertex_attrs.is_some() { Some(graph_vertex_count(handle, graph)?) } else { None };
if let Some(pos) = initial_pos {
let graph_vertex_count = graph_vertex_count.expect("graph vertex count should exist for initial positions");
let n = pos.vertices.len();
if pos.x.len() != n {
return Err(Error::new(ErrorKind::InvalidInput, "force_atlas2")
.with_message("initial positions: x length must match vertices length")
.with_context("vertices_len", n.to_string())
.with_context("x_len", pos.x.len().to_string()));
}
if pos.y.len() != n {
return Err(Error::new(ErrorKind::InvalidInput, "force_atlas2")
.with_message("initial positions: y length must match vertices length")
.with_context("vertices_len", n.to_string())
.with_context("y_len", pos.y.len().to_string()));
}
validate_force_atlas2_vertex_helper_dtype::<_, V>(graph, "initial positions")?;
validate_force_atlas2_value_helper_dtype::<_, W>(graph, "initial positions")?;
if n != graph_vertex_count {
return Err(Error::new(ErrorKind::InvalidInput, "force_atlas2")
.with_message("initial positions must provide one coordinate pair for each graph vertex")
.with_context("graph_vertex_count", graph_vertex_count.to_string())
.with_context("vertices_len", n.to_string()));
}
}
if let Some(attrs) = vertex_attrs {
let graph_vertex_count = graph_vertex_count.expect("graph vertex count should exist for vertex attrs");
let has_any_attrs = attrs.radius.is_some() || attrs.mobility.is_some() || attrs.mass.is_some();
if has_any_attrs {
validate_force_atlas2_vertex_helper_dtype::<_, V>(graph, "vertex attributes")?;
validate_force_atlas2_value_helper_dtype::<_, W>(graph, "vertex attributes")?;
}
if let Some((verts, vals)) = attrs.radius {
if verts.len() != vals.len() {
return Err(Error::new(ErrorKind::InvalidInput, "force_atlas2")
.with_message("vertex radius: vertices and values must have the same length")
.with_context("vertices_len", verts.len().to_string())
.with_context("values_len", vals.len().to_string()));
}
if vals.len() != graph_vertex_count {
return Err(Error::new(ErrorKind::InvalidInput, "force_atlas2")
.with_message("vertex radius must provide one value for each graph vertex")
.with_context("graph_vertex_count", graph_vertex_count.to_string())
.with_context("values_len", vals.len().to_string()));
}
}
if let Some((verts, vals)) = attrs.mobility {
if verts.len() != vals.len() {
return Err(Error::new(ErrorKind::InvalidInput, "force_atlas2")
.with_message("vertex mobility: vertices and values must have the same length")
.with_context("vertices_len", verts.len().to_string())
.with_context("values_len", vals.len().to_string()));
}
if vals.len() != graph_vertex_count {
return Err(Error::new(ErrorKind::InvalidInput, "force_atlas2")
.with_message("vertex mobility must provide one value for each graph vertex")
.with_context("graph_vertex_count", graph_vertex_count.to_string())
.with_context("values_len", vals.len().to_string()));
}
}
if let Some((verts, vals)) = attrs.mass {
if verts.len() != vals.len() {
return Err(Error::new(ErrorKind::InvalidInput, "force_atlas2")
.with_message("vertex mass: vertices and values must have the same length")
.with_context("vertices_len", verts.len().to_string())
.with_context("values_len", vals.len().to_string()));
}
if vals.len() != graph_vertex_count {
return Err(Error::new(ErrorKind::InvalidInput, "force_atlas2")
.with_message("vertex mass must provide one value for each graph vertex")
.with_context("graph_vertex_count", graph_vertex_count.to_string())
.with_context("values_len", vals.len().to_string()));
}
}
}
let (start_verts_ptr, x_start_ptr, y_start_ptr) = match initial_pos {
Some(pos) => (pos.vertices.as_ptr(), pos.x.as_ptr(), pos.y.as_ptr()),
None => (ptr::null_mut(), ptr::null_mut(), ptr::null_mut()),
};
let empty = ForceAtlas2VertexAttrs::<V, W>::default();
let attrs = vertex_attrs.unwrap_or(&empty);
let (radius_verts_ptr, radius_vals_ptr) = match attrs.radius {
Some((v, val)) => (v.as_ptr(), val.as_ptr()),
None => (ptr::null_mut(), ptr::null_mut()),
};
let (mobility_verts_ptr, mobility_vals_ptr) = match attrs.mobility {
Some((v, val)) => (v.as_ptr(), val.as_ptr()),
None => (ptr::null_mut(), ptr::null_mut()),
};
let (mass_verts_ptr, mass_vals_ptr) = match attrs.mass {
Some((v, val)) => (v.as_ptr(), val.as_ptr()),
None => (ptr::null_mut(), ptr::null_mut()),
};
let mut result: *mut cugraph_layout_result_t = ptr::null_mut();
let mut error: *mut cugraph_error_t = ptr::null_mut();
let graph_ptr = graph.as_ptr(handle, "force_atlas2")?;
unsafe {
let code = cugraph_force_atlas2(
handle.handle_ptr(),
rng_state.as_ptr(),
graph_ptr,
i32::try_from(params.max_iter)?,
start_verts_ptr,
x_start_ptr,
y_start_ptr,
to_bool_t(params.outbound_attraction_distribution),
to_bool_t(params.lin_log_mode),
to_bool_t(params.prevent_overlapping),
radius_verts_ptr,
radius_vals_ptr,
params.overlap_scaling_ratio.get(),
params.edge_weight_influence.get(),
params.jitter_tolerance.get(),
to_bool_t(params.barnes_hut_optimize),
params.barnes_hut_theta.get(),
params.scaling_ratio.get(),
to_bool_t(params.strong_gravity_mode),
params.gravity.get(),
mobility_verts_ptr,
mobility_vals_ptr,
mass_verts_ptr,
mass_vals_ptr,
to_bool_t(params.verbose),
to_bool_t(params.do_expensive_check),
&mut result,
&mut error,
);
check_error(code, error, handle.device(), "force_atlas2")?;
}
let result = require_non_null(result, "force_atlas2", "layout_result")?;
unsafe { LayoutResult::from_raw(result, handle.device(), handle.runtime_bound_lease()) }
}
#[cfg(test)]
mod layout_result_constructor_tests {
use std::ptr;
use super::*;
#[test]
fn layout_result_from_raw_rejects_null() {
let err = unsafe { LayoutResult::from_raw(ptr::null_mut(), 0, None) }.expect_err("null must be rejected");
assert_eq!(err.kind(), ErrorKind::Unexpected);
assert_eq!(err.operation(), "LayoutResult::from_raw");
}
}