Skip to main content

cugraph/structure/
core.rs

1//! Core-number algorithms.
2
3use std::{marker::PhantomData, num::NonZeroUsize, ptr};
4
5use cugraph_sys::{
6    cugraph_core_number, cugraph_core_result_get_core_numbers, cugraph_core_result_get_vertices, cugraph_core_result_t,
7    cugraph_error_t, cugraph_k_core, cugraph_k_core_degree_type_t, cugraph_k_core_degree_type_t_K_CORE_DEGREE_TYPE_IN,
8    cugraph_k_core_degree_type_t_K_CORE_DEGREE_TYPE_INOUT, cugraph_k_core_degree_type_t_K_CORE_DEGREE_TYPE_OUT,
9    cugraph_k_core_result_get_dst_vertices, cugraph_k_core_result_get_src_vertices, cugraph_k_core_result_get_weights,
10    cugraph_k_core_result_t, cugraph_type_erased_device_array_view_t,
11};
12
13use crate::{
14    array::RawDeviceArrayView,
15    cuda::{DeviceResourceKind, drop_device_resource},
16    cudf_interop::{core_number_result_to_dataframe, k_core_result_to_dataframe},
17    error::{Error, Result, check_error, require_non_null},
18    graph::Graph,
19    handle::{EnteredContext, ResourceHandleLease},
20    structure::typed_results::{IntegerValues, ScoreValues, VertexIds},
21    types::to_bool_t,
22};
23
24/// Degree direction used when computing core numbers.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
26pub enum KCoreDegreeType {
27    /// Compute core numbers from incoming edges.
28    In,
29    /// Compute core numbers from outgoing edges.
30    Out,
31    /// Compute core numbers from both incoming and outgoing edges.
32    #[default]
33    InOut,
34}
35
36impl From<KCoreDegreeType> for cugraph_k_core_degree_type_t {
37    fn from(value: KCoreDegreeType) -> Self {
38        match value {
39            KCoreDegreeType::In => cugraph_k_core_degree_type_t_K_CORE_DEGREE_TYPE_IN,
40            KCoreDegreeType::Out => cugraph_k_core_degree_type_t_K_CORE_DEGREE_TYPE_OUT,
41            KCoreDegreeType::InOut => cugraph_k_core_degree_type_t_K_CORE_DEGREE_TYPE_INOUT,
42        }
43    }
44}
45
46/// Parameters for [`Graph::core_number`](crate::graph::Graph::core_number).
47#[derive(Debug, Clone, Copy, Default)]
48#[must_use]
49pub struct CoreNumberParams {
50    /// Which edge direction to use when computing core numbers.
51    pub degree_type: KCoreDegreeType,
52    /// Whether to run expensive input validation in the C library.
53    pub do_expensive_check: bool,
54}
55
56/// Parameters for [`Graph::k_core`](crate::graph::Graph::k_core).
57#[derive(Debug, Clone, Copy)]
58#[must_use]
59pub struct KCoreParams {
60    /// K value for the extracted core. Defaults to 2.
61    pub k: NonZeroUsize,
62    /// Degree direction to use if no precomputed core-number result is supplied.
63    pub degree_type: KCoreDegreeType,
64    /// Whether to run expensive input validation in the C library.
65    pub do_expensive_check: bool,
66}
67
68impl Default for KCoreParams {
69    fn default() -> Self {
70        Self {
71            // SAFETY: the literal `2` is non-zero.
72            k: unsafe { NonZeroUsize::new_unchecked(2) },
73            degree_type: KCoreDegreeType::default(),
74            do_expensive_check: false,
75        }
76    }
77}
78
79/// Compute core numbers for all vertices in a graph.
80///
81/// Prefer using [`Graph::core_number`](crate::graph::Graph::core_number),
82/// which calls this function.
83pub(crate) fn core_number<T>(
84    handle: &EnteredContext<'_>,
85    graph: &mut Graph<T>,
86    params: &CoreNumberParams,
87) -> Result<CoreNumberResult> {
88    let mut result: *mut cugraph_core_result_t = ptr::null_mut();
89    let mut error: *mut cugraph_error_t = ptr::null_mut();
90    let graph_ptr = graph.as_ptr(handle, "core_number")?;
91
92    // SAFETY: `graph` and `handle` are live for the duration of the call, and
93    // `result`/`error` are valid out-pointers.
94    unsafe {
95        let code = cugraph_core_number(
96            handle.handle_ptr(),
97            graph_ptr,
98            params.degree_type.into(),
99            to_bool_t(params.do_expensive_check),
100            &mut result,
101            &mut error,
102        );
103        check_error(code, error, handle.device(), "core_number")?;
104    }
105
106    let result = require_non_null(result, "core_number", "core_result")?;
107
108    // SAFETY: on success, cuGraph transfers ownership of the result pointer.
109    unsafe { CoreNumberResult::from_raw(result, handle.device(), handle.runtime_bound_lease()) }
110}
111
112/// Extract a k-core subgraph.
113pub(crate) fn k_core<T>(
114    handle: &EnteredContext<'_>,
115    graph: &mut Graph<T>,
116    params: &KCoreParams,
117    core_numbers: Option<&CoreNumberResult>,
118) -> Result<KCoreResult> {
119    let mut result: *mut cugraph_k_core_result_t = ptr::null_mut();
120    let mut error: *mut cugraph_error_t = ptr::null_mut();
121    let core_result_ptr = core_numbers.map(CoreNumberResult::as_ptr).unwrap_or(ptr::null());
122    let graph_ptr = graph.as_ptr(handle, "k_core")?;
123
124    // SAFETY: `graph` and optional `core_result_ptr` are live for the call,
125    // and `result`/`error` are valid out-pointers.
126    unsafe {
127        let code = cugraph_k_core(
128            handle.handle_ptr(),
129            graph_ptr,
130            params.k.get(),
131            params.degree_type.into(),
132            core_result_ptr,
133            to_bool_t(params.do_expensive_check),
134            &mut result,
135            &mut error,
136        );
137        check_error(code, error, handle.device(), "k_core")?;
138    }
139
140    let result = require_non_null(result, "k_core", "k_core_result")?;
141
142    // SAFETY: on success, cuGraph transfers ownership of the result pointer.
143    unsafe { KCoreResult::from_raw(result, handle.device(), handle.runtime_bound_lease()) }
144}
145
146/// Result of a core-number computation.
147///
148/// Provides vertex ids and integer core numbers. Freed on drop via
149/// `cugraph_core_result_free`.
150pub struct CoreNumberResult {
151    raw: *mut cugraph_core_result_t,
152    device: i32,
153    // Keeps a runtime-bound cuGraph handle alive until after Drop frees `raw`.
154    _resource_handle_lease: Option<ResourceHandleLease>,
155    _nosend_nosync: PhantomData<*mut ()>,
156}
157
158impl std::fmt::Debug for CoreNumberResult {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        f.debug_struct("CoreNumberResult").finish_non_exhaustive()
161    }
162}
163
164impl CoreNumberResult {
165    /// Wrap a raw core-number result pointer.
166    ///
167    /// # Safety
168    ///
169    /// `raw` must be a valid, non-null pointer returned by
170    /// `cugraph_core_number`. Ownership is transferred to this struct.
171    pub(crate) unsafe fn from_raw(
172        raw: *mut cugraph_core_result_t,
173        device: i32,
174        resource_handle_lease: Option<ResourceHandleLease>,
175    ) -> Result<Self> {
176        let raw = require_non_null(raw, "CoreNumberResult::from_raw", "core_result")?;
177        Ok(Self { raw, device, _resource_handle_lease: resource_handle_lease, _nosend_nosync: PhantomData })
178    }
179
180    pub(crate) fn as_ptr(&self) -> *const cugraph_core_result_t {
181        self.raw.cast_const()
182    }
183
184    fn raw_view(
185        &self,
186        getter: unsafe extern "C" fn(
187            *mut cugraph_core_result_t,
188        ) -> *mut cugraph_sys::cugraph_type_erased_device_array_view_t,
189        operation: &'static str,
190        pointer_name: &'static str,
191    ) -> Result<RawDeviceArrayView<'_>> {
192        let view_ptr = require_non_null(
193            // SAFETY: `self.raw` is a live core result owned by this wrapper.
194            unsafe { getter(self.raw) },
195            operation,
196            pointer_name,
197        )?;
198        // SAFETY: cuGraph result getters allocate view metadata for the
199        // caller. The backing device allocation remains owned by `self.raw`.
200        unsafe { RawDeviceArrayView::from_owned_view(view_ptr, Some(self.device), operation, pointer_name) }
201    }
202
203    /// Get the vertex ids as a runtime-typed device array view.
204    pub fn vertex_ids(&self) -> Result<VertexIds<'_>> {
205        VertexIds::from_raw(self.raw_view(
206            cugraph_core_result_get_vertices,
207            "CoreNumberResult::vertex_ids",
208            "vertices",
209        )?)
210    }
211
212    /// Get the core numbers as a runtime-typed integer device array view.
213    pub fn core_numbers(&self) -> Result<IntegerValues<'_>> {
214        IntegerValues::from_raw(self.raw_view(
215            cugraph_core_result_get_core_numbers,
216            "CoreNumberResult::core_numbers",
217            "core_numbers",
218        )?)
219    }
220
221    /// Convert this core-number result to a cudf DataFrame.
222    ///
223    /// Returns columns `"vertex"` and `"core_number"`.
224    pub fn to_dataframe(&self, runtime: &cudf::Runtime) -> Result<cudf::DataFrame> {
225        core_number_result_to_dataframe(self, runtime)
226            .map_err(|e: Error| e.with_operation("CoreNumberResult::to_dataframe"))
227    }
228}
229
230impl Drop for CoreNumberResult {
231    fn drop(&mut self) {
232        drop_device_resource(&mut self.raw, self.device, "CoreNumberResult::drop", DeviceResourceKind::CoreResult);
233    }
234}
235
236/// Result of a k-core extraction.
237///
238/// Provides source and destination vertex ids plus optional edge weights.
239/// Freed on drop via `cugraph_k_core_result_free`.
240pub struct KCoreResult {
241    raw: *mut cugraph_k_core_result_t,
242    device: i32,
243    // Keeps a runtime-bound cuGraph handle alive until after Drop frees `raw`.
244    _resource_handle_lease: Option<ResourceHandleLease>,
245    _nosend_nosync: PhantomData<*mut ()>,
246}
247
248impl std::fmt::Debug for KCoreResult {
249    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250        f.debug_struct("KCoreResult").finish_non_exhaustive()
251    }
252}
253
254impl KCoreResult {
255    /// Wrap a raw k-core result pointer.
256    ///
257    /// # Safety
258    ///
259    /// `raw` must be a valid, non-null pointer returned by `cugraph_k_core`.
260    /// Ownership is transferred to this struct.
261    pub(crate) unsafe fn from_raw(
262        raw: *mut cugraph_k_core_result_t,
263        device: i32,
264        resource_handle_lease: Option<ResourceHandleLease>,
265    ) -> Result<Self> {
266        let raw = require_non_null(raw, "KCoreResult::from_raw", "k_core_result")?;
267        Ok(Self { raw, device, _resource_handle_lease: resource_handle_lease, _nosend_nosync: PhantomData })
268    }
269
270    fn raw_view(
271        &self,
272        getter: unsafe extern "C" fn(*mut cugraph_k_core_result_t) -> *mut cugraph_type_erased_device_array_view_t,
273        operation: &'static str,
274        pointer_name: &'static str,
275    ) -> Result<RawDeviceArrayView<'_>> {
276        let view_ptr = require_non_null(
277            // SAFETY: `self.raw` is a live k-core result owned by this wrapper.
278            unsafe { getter(self.raw) },
279            operation,
280            pointer_name,
281        )?;
282        // SAFETY: cuGraph allocates this view metadata; this wrapper frees it, and `self.raw` retains the backing arrays.
283        unsafe { RawDeviceArrayView::from_owned_view(view_ptr, Some(self.device), operation, pointer_name) }
284    }
285
286    fn optional_raw_view(
287        &self,
288        getter: unsafe extern "C" fn(*mut cugraph_k_core_result_t) -> *mut cugraph_type_erased_device_array_view_t,
289        operation: &'static str,
290    ) -> Result<Option<RawDeviceArrayView<'_>>> {
291        // SAFETY: `self.raw` is a live k-core result owned by this wrapper.
292        let view_ptr = unsafe { getter(self.raw) };
293        if view_ptr.is_null() {
294            return Ok(None);
295        }
296
297        // SAFETY: cuGraph result getters allocate view metadata for the
298        // caller. The backing device allocation remains owned by `self.raw`.
299        unsafe { RawDeviceArrayView::from_owned_view(view_ptr, Some(self.device), operation, "optional_view") }
300            .map(Some)
301    }
302
303    /// Get source vertex ids.
304    pub fn sources(&self) -> Result<VertexIds<'_>> {
305        VertexIds::from_raw(self.raw_view(cugraph_k_core_result_get_src_vertices, "KCoreResult::sources", "sources")?)
306    }
307
308    /// Get destination vertex ids.
309    pub fn destinations(&self) -> Result<VertexIds<'_>> {
310        VertexIds::from_raw(self.raw_view(
311            cugraph_k_core_result_get_dst_vertices,
312            "KCoreResult::destinations",
313            "destinations",
314        )?)
315    }
316
317    /// Get optional edge weights.
318    pub fn weights(&self) -> Result<Option<ScoreValues<'_>>> {
319        self.optional_raw_view(cugraph_k_core_result_get_weights, "KCoreResult::weights")?
320            .map(ScoreValues::from_raw)
321            .transpose()
322    }
323
324    /// Convert this result to a cudf DataFrame.
325    ///
326    /// Returns `"source"` and `"destination"` columns, plus `"weight"` when
327    /// the native result contains an edge-weight column.
328    pub fn to_dataframe(&self, runtime: &cudf::Runtime) -> Result<cudf::DataFrame> {
329        k_core_result_to_dataframe(self, runtime).map_err(|e: Error| e.with_operation("KCoreResult::to_dataframe"))
330    }
331}
332
333impl Drop for KCoreResult {
334    fn drop(&mut self) {
335        drop_device_resource(&mut self.raw, self.device, "KCoreResult::drop", DeviceResourceKind::KCoreResult);
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use std::ptr;
342
343    use super::*;
344    use crate::error::ErrorKind;
345
346    #[test]
347    fn core_number_result_from_raw_rejects_null() {
348        // SAFETY: this exercises the constructor's null guard only.
349        let err = match unsafe { CoreNumberResult::from_raw(ptr::null_mut(), 0, None) } {
350            Err(err) => err,
351            Ok(_) => panic!("null must be rejected"),
352        };
353        assert_eq!(err.kind(), ErrorKind::Unexpected);
354        assert_eq!(err.operation(), "CoreNumberResult::from_raw");
355    }
356
357    #[test]
358    fn k_core_result_from_raw_rejects_null() {
359        // SAFETY: this exercises the constructor's null guard only.
360        let err = match unsafe { KCoreResult::from_raw(ptr::null_mut(), 0, None) } {
361            Err(err) => err,
362            Ok(_) => panic!("null must be rejected"),
363        };
364        assert_eq!(err.kind(), ErrorKind::Unexpected);
365        assert_eq!(err.operation(), "KCoreResult::from_raw");
366    }
367}