Skip to main content

cugraph/
params.rs

1//! Strongly-typed parameter wrappers for cuGraph algorithm configuration.
2//!
3//! Enforces range, non-zero, and finiteness invariants at construction time.
4//!
5//! # Overview
6//!
7//! | Type | Constraint | Use Cases |
8//! |------|-----------|-----------|
9//! | [`IterationCount`] | `> 0` (u32) | `max_iter`, `max_iterations` |
10//! | [`BfsDepthLimit`] | `>= 0` (usize) | BFS `depth_limit` |
11//! | [`PositiveF64`] | `> 0.0`, finite | `scaling_ratio` |
12//! | [`UnitF64`] | `∈ (0.0, 1.0)`, finite | *(currently unused: kept for future algorithms)* |
13//! | [`NonNegativeF64`] | `>= 0.0`, finite | `epsilon`, `edge_weight_influence`, `gravity`, `jitter_tolerance` |
14//! | [`ClosedUnitF64`] | `∈ [0.0, 1.0]`, finite | `alpha` (damping factor), `barnes_hut_theta` |
15//! | [`FractionF64`] | `∈ (0.0, 1.0]`, finite | ECG `threshold` and `resolution` |
16//! | [`HalfOpenUnitF64`] | `∈ [0.0, 1.0)`, finite | spectral `evs_tolerance` |
17//! | [`ClusterCount`] | `>= 2` (usize) | spectral `n_clusters` |
18//!
19//! # Examples
20//!
21//! ```
22//! use cugraph::params::{IterationCount, PositiveF64, UnitF64};
23//!
24//! let iters = IterationCount::new(100).unwrap();
25//! assert_eq!(iters.get(), 100);
26//!
27//! let eps = PositiveF64::new(1e-5).unwrap();
28//! assert_eq!(eps.get(), 1e-5);
29//!
30//! let alpha = UnitF64::new(0.85).unwrap();
31//! assert_eq!(alpha.get(), 0.85);
32//! ```
33
34use std::{fmt, num::NonZeroU32};
35
36use crate::error::{Error, ErrorKind, Result};
37
38pub(crate) mod defaults {
39    use super::{ClosedUnitF64, FractionF64, IterationCount, NonNegativeF64, NonZeroU32, PositiveF64};
40
41    pub(crate) const PAGERANK_ALPHA: ClosedUnitF64 = ClosedUnitF64(0.85);
42    pub(crate) const PAGERANK_EPSILON: NonNegativeF64 = NonNegativeF64(1e-5);
43    pub(crate) const PAGERANK_MAX_ITERATIONS: IterationCount = IterationCount(NonZeroU32::new(100).unwrap());
44
45    pub(crate) const FORCE_ATLAS2_MAX_ITER: IterationCount = IterationCount(NonZeroU32::new(500).unwrap());
46    pub(crate) const FORCE_ATLAS2_OVERLAP_SCALING_RATIO: PositiveF64 = PositiveF64(2.0);
47    pub(crate) const FORCE_ATLAS2_EDGE_WEIGHT_INFLUENCE: NonNegativeF64 = NonNegativeF64(1.0);
48    pub(crate) const FORCE_ATLAS2_JITTER_TOLERANCE: NonNegativeF64 = NonNegativeF64(1.0);
49    pub(crate) const FORCE_ATLAS2_BARNES_HUT_THETA: ClosedUnitF64 = ClosedUnitF64(0.5);
50    pub(crate) const FORCE_ATLAS2_SCALING_RATIO: PositiveF64 = PositiveF64(2.0);
51    pub(crate) const FORCE_ATLAS2_GRAVITY: NonNegativeF64 = NonNegativeF64(1.0);
52
53    pub(crate) const LOUVAIN_MAX_LEVEL: IterationCount = IterationCount(NonZeroU32::new(100).unwrap());
54    pub(crate) const LOUVAIN_THRESHOLD: NonNegativeF64 = NonNegativeF64(1e-7);
55    pub(crate) const LOUVAIN_RESOLUTION: PositiveF64 = PositiveF64(1.0);
56
57    pub(crate) const ECG_THRESHOLD: FractionF64 = FractionF64(1e-7);
58    pub(crate) const ECG_RESOLUTION: FractionF64 = FractionF64(1.0);
59}
60
61/// A strictly positive iteration count.
62///
63/// Wraps a [`NonZeroU32`] to guarantee that iteration-count parameters (like
64/// `max_iter`) are always at least 1. This prevents no-ops that finish without an
65/// error from passing zero iterations to algorithm functions.
66///
67/// # Construction
68///
69/// ```
70/// use cugraph::params::IterationCount;
71///
72/// // From a runtime value (fallible):
73/// let count = IterationCount::new(500).unwrap();
74/// assert_eq!(count.get(), 500);
75///
76/// // TryFrom conversions:
77/// let count: IterationCount = 100u32.try_into().unwrap();
78/// let count: IterationCount = 100i32.try_into().unwrap();
79/// let count: IterationCount = 100usize.try_into().unwrap();
80/// ```
81#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
82pub struct IterationCount(NonZeroU32);
83
84impl IterationCount {
85    /// Create a new `IterationCount` from a `u32`.
86    ///
87    /// # Errors
88    ///
89    /// Returns [`ErrorKind::InvalidInput`] if `value` is zero.
90    pub fn new(value: u32) -> Result<Self> {
91        NonZeroU32::new(value).map(IterationCount).ok_or_else(|| {
92            Error::new(ErrorKind::InvalidInput, "IterationCount::new")
93                .with_message("iteration count must be greater than zero")
94        })
95    }
96
97    /// Create a new `IterationCount` from a [`NonZeroU32`] (infallible).
98    pub const fn from_non_zero(value: NonZeroU32) -> Self {
99        IterationCount(value)
100    }
101
102    /// Get the underlying value as a `u32`.
103    pub const fn get(self) -> u32 {
104        self.0.get()
105    }
106
107    /// Get the underlying [`NonZeroU32`].
108    pub const fn into_inner(self) -> NonZeroU32 {
109        self.0
110    }
111}
112
113impl fmt::Display for IterationCount {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        write!(f, "{}", self.0)
116    }
117}
118
119impl From<IterationCount> for u32 {
120    fn from(count: IterationCount) -> u32 {
121        count.get()
122    }
123}
124
125impl From<IterationCount> for usize {
126    fn from(count: IterationCount) -> usize {
127        count.get() as usize
128    }
129}
130
131impl TryFrom<IterationCount> for i32 {
132    type Error = Error;
133
134    fn try_from(count: IterationCount) -> Result<i32> {
135        i32::try_from(count.get()).map_err(|_| {
136            Error::new(ErrorKind::InvalidInput, "IterationCount::try_into<i32>")
137                .with_message("iteration count exceeds i32::MAX")
138                .with_context("value", count.get().to_string())
139        })
140    }
141}
142
143impl TryFrom<u32> for IterationCount {
144    type Error = Error;
145
146    fn try_from(value: u32) -> Result<Self> {
147        Self::new(value)
148    }
149}
150
151impl TryFrom<i32> for IterationCount {
152    type Error = Error;
153
154    fn try_from(value: i32) -> Result<Self> {
155        if value <= 0 {
156            return Err(Error::new(ErrorKind::InvalidInput, "IterationCount::try_from<i32>")
157                .with_message("iteration count must be greater than zero")
158                .with_context("value", value.to_string()));
159        }
160        Self::new(value as u32)
161    }
162}
163
164impl TryFrom<usize> for IterationCount {
165    type Error = Error;
166
167    fn try_from(value: usize) -> Result<Self> {
168        let val_u32 = u32::try_from(value).map_err(|_| {
169            Error::new(ErrorKind::InvalidInput, "IterationCount::try_from<usize>")
170                .with_message("iteration count exceeds u32::MAX")
171                .with_context("value", value.to_string())
172        })?;
173        Self::new(val_u32)
174    }
175}
176
177/// A BFS depth limit: the number of hops to expand from the sources.
178///
179/// `0` expands no hops and reaches only the sources. Use `None` at the API
180/// boundary to request unlimited traversal. A limit larger than the graph's
181/// vertex type can index behaves as unlimited.
182///
183/// # Construction
184///
185/// ```
186/// use cugraph::params::BfsDepthLimit;
187///
188/// let limit = BfsDepthLimit::new(3);
189/// assert_eq!(limit.get(), 3);
190///
191/// assert_eq!(BfsDepthLimit::new(0).get(), 0);
192/// assert!(BfsDepthLimit::try_from(-1i32).is_err());
193/// ```
194#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
195pub struct BfsDepthLimit(usize);
196
197impl BfsDepthLimit {
198    /// Create a new `BfsDepthLimit` from a `usize`.
199    pub const fn new(value: usize) -> Self {
200        BfsDepthLimit(value)
201    }
202
203    /// Get the underlying value as `usize`.
204    pub const fn get(self) -> usize {
205        self.0
206    }
207}
208
209impl fmt::Display for BfsDepthLimit {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        write!(f, "{}", self.0)
212    }
213}
214
215impl From<BfsDepthLimit> for usize {
216    fn from(limit: BfsDepthLimit) -> usize {
217        limit.get()
218    }
219}
220
221impl From<usize> for BfsDepthLimit {
222    fn from(value: usize) -> Self {
223        Self::new(value)
224    }
225}
226
227impl From<u32> for BfsDepthLimit {
228    fn from(value: u32) -> Self {
229        Self::new(value as usize)
230    }
231}
232
233impl TryFrom<i32> for BfsDepthLimit {
234    type Error = Error;
235
236    fn try_from(value: i32) -> Result<Self> {
237        usize::try_from(value).map(Self::new).map_err(|_| {
238            Error::new(ErrorKind::InvalidInput, "BfsDepthLimit::try_from<i32>")
239                .with_message("BFS depth limit must not be negative")
240                .with_context("value", value.to_string())
241        })
242    }
243}
244
245/// A strictly positive, finite `f64` value.
246///
247/// Used for parameters that must be greater than zero, such as scaling ratios.
248///
249/// # Construction
250///
251/// ```
252/// use cugraph::params::PositiveF64;
253///
254/// let eps = PositiveF64::new(1e-5).unwrap();
255/// assert_eq!(eps.get(), 1e-5);
256///
257/// // Zero and negative values are rejected:
258/// assert!(PositiveF64::new(0.0).is_err());
259/// assert!(PositiveF64::new(-1.0).is_err());
260///
261/// // Infinity and NaN are rejected:
262/// assert!(PositiveF64::new(f64::INFINITY).is_err());
263/// assert!(PositiveF64::new(f64::NAN).is_err());
264/// ```
265#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
266pub struct PositiveF64(f64);
267
268impl PositiveF64 {
269    /// Create a new `PositiveF64` from an `f64`.
270    ///
271    /// # Errors
272    ///
273    /// Returns [`ErrorKind::InvalidInput`] if `value` is not finite and
274    /// positive (i.e., `<= 0.0`, `NaN`, or infinity).
275    pub fn new(value: f64) -> Result<Self> {
276        if !value.is_finite() || value <= 0.0 {
277            return Err(Error::new(ErrorKind::InvalidInput, "PositiveF64::new")
278                .with_message("value must be a finite number greater than zero")
279                .with_context("value", format!("{value}")));
280        }
281        Ok(PositiveF64(value))
282    }
283
284    /// Get the underlying `f64` value.
285    pub const fn get(self) -> f64 {
286        self.0
287    }
288}
289
290impl fmt::Display for PositiveF64 {
291    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292        write!(f, "{}", self.0)
293    }
294}
295
296impl From<PositiveF64> for f64 {
297    fn from(val: PositiveF64) -> f64 {
298        val.get()
299    }
300}
301
302impl TryFrom<f64> for PositiveF64 {
303    type Error = Error;
304
305    fn try_from(value: f64) -> Result<Self> {
306        Self::new(value)
307    }
308}
309
310/// A finite `f64` value in the open interval `(0.0, 1.0)`.
311///
312/// Used for parameters that represent fractions or probabilities, such as the
313/// PageRank damping factor (`alpha`) and Barnes-Hut trade-off (`theta`).
314///
315/// # Construction
316///
317/// ```
318/// use cugraph::params::UnitF64;
319///
320/// let alpha = UnitF64::new(0.85).unwrap();
321/// assert_eq!(alpha.get(), 0.85);
322///
323/// // Boundary values are rejected (open interval):
324/// assert!(UnitF64::new(0.0).is_err());
325/// assert!(UnitF64::new(1.0).is_err());
326/// ```
327#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
328pub struct UnitF64(f64);
329
330impl UnitF64 {
331    /// Create a new `UnitF64` from an `f64`.
332    ///
333    /// # Errors
334    ///
335    /// Returns [`ErrorKind::InvalidInput`] if `value` is not in the open
336    /// interval `(0.0, 1.0)` or is not finite.
337    pub fn new(value: f64) -> Result<Self> {
338        if !value.is_finite() || value <= 0.0 || value >= 1.0 {
339            return Err(Error::new(ErrorKind::InvalidInput, "UnitF64::new")
340                .with_message("value must be in the open interval (0.0, 1.0)")
341                .with_context("value", format!("{value}")));
342        }
343        Ok(UnitF64(value))
344    }
345
346    /// Get the underlying `f64` value.
347    pub const fn get(self) -> f64 {
348        self.0
349    }
350}
351
352impl fmt::Display for UnitF64 {
353    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
354        write!(f, "{}", self.0)
355    }
356}
357
358impl From<UnitF64> for f64 {
359    fn from(val: UnitF64) -> f64 {
360        val.get()
361    }
362}
363
364impl TryFrom<f64> for UnitF64 {
365    type Error = Error;
366
367    fn try_from(value: f64) -> Result<Self> {
368        Self::new(value)
369    }
370}
371
372/// A non-negative, finite `f64` value (i.e., `>= 0.0`).
373///
374/// Used for parameters that may be zero but not negative, such as
375/// `edge_weight_influence` (where 0.0 means "no influence").
376///
377/// # Construction
378///
379/// ```
380/// use cugraph::params::NonNegativeF64;
381///
382/// let influence = NonNegativeF64::new(0.0).unwrap();
383/// assert_eq!(influence.get(), 0.0);
384///
385/// let influence = NonNegativeF64::new(1.5).unwrap();
386/// assert_eq!(influence.get(), 1.5);
387///
388/// // Negative values are rejected:
389/// assert!(NonNegativeF64::new(-0.1).is_err());
390/// ```
391#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
392pub struct NonNegativeF64(f64);
393
394impl NonNegativeF64 {
395    /// Create a new `NonNegativeF64` from an `f64`.
396    ///
397    /// # Errors
398    ///
399    /// Returns [`ErrorKind::InvalidInput`] if `value` is negative, `NaN`, or
400    /// infinite.
401    pub fn new(value: f64) -> Result<Self> {
402        if !value.is_finite() || value < 0.0 {
403            return Err(Error::new(ErrorKind::InvalidInput, "NonNegativeF64::new")
404                .with_message("value must be a finite non-negative number")
405                .with_context("value", format!("{value}")));
406        }
407        Ok(NonNegativeF64(value))
408    }
409
410    /// Get the underlying `f64` value.
411    pub const fn get(self) -> f64 {
412        self.0
413    }
414}
415
416impl fmt::Display for NonNegativeF64 {
417    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418        write!(f, "{}", self.0)
419    }
420}
421
422impl From<NonNegativeF64> for f64 {
423    fn from(val: NonNegativeF64) -> f64 {
424        val.get()
425    }
426}
427
428impl TryFrom<f64> for NonNegativeF64 {
429    type Error = Error;
430
431    fn try_from(value: f64) -> Result<Self> {
432        Self::new(value)
433    }
434}
435
436/// A finite `f64` value in the closed interval `[0.0, 1.0]`.
437///
438/// Used for parameters that represent interpolation weights or trade-offs
439/// where the boundary values are valid (e.g., Barnes-Hut theta where 0.0 means
440/// "maximum accuracy" and 1.0 means "maximum speed").
441///
442/// # Construction
443///
444/// ```
445/// use cugraph::params::ClosedUnitF64;
446///
447/// let theta = ClosedUnitF64::new(0.5).unwrap();
448/// assert_eq!(theta.get(), 0.5);
449///
450/// // Boundary values are accepted (closed interval):
451/// assert!(ClosedUnitF64::new(0.0).is_ok());
452/// assert!(ClosedUnitF64::new(1.0).is_ok());
453/// ```
454#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
455pub struct ClosedUnitF64(f64);
456
457impl ClosedUnitF64 {
458    /// Create a new `ClosedUnitF64` from an `f64`.
459    ///
460    /// # Errors
461    ///
462    /// Returns [`ErrorKind::InvalidInput`] if `value` is not in the closed
463    /// interval `[0.0, 1.0]` or is not finite.
464    pub fn new(value: f64) -> Result<Self> {
465        if !(0.0..=1.0).contains(&value) {
466            return Err(Error::new(ErrorKind::InvalidInput, "ClosedUnitF64::new")
467                .with_message("value must be in the closed interval [0.0, 1.0]")
468                .with_context("value", format!("{value}")));
469        }
470        Ok(ClosedUnitF64(value))
471    }
472
473    /// Get the underlying `f64` value.
474    pub const fn get(self) -> f64 {
475        self.0
476    }
477}
478
479impl fmt::Display for ClosedUnitF64 {
480    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
481        write!(f, "{}", self.0)
482    }
483}
484
485impl From<ClosedUnitF64> for f64 {
486    fn from(val: ClosedUnitF64) -> f64 {
487        val.get()
488    }
489}
490
491impl TryFrom<f64> for ClosedUnitF64 {
492    type Error = Error;
493
494    fn try_from(value: f64) -> Result<Self> {
495        Self::new(value)
496    }
497}
498
499/// A finite `f64` value in the half-open interval `(0.0, 1.0]`.
500///
501/// Used for parameters libcugraph requires to be positive and at most one,
502/// such as the ECG convergence `threshold` and `resolution`.
503///
504/// # Construction
505///
506/// ```
507/// use cugraph::params::FractionF64;
508///
509/// assert_eq!(FractionF64::new(1.0).unwrap().get(), 1.0);
510///
511/// // Zero and values above one are rejected:
512/// assert!(FractionF64::new(0.0).is_err());
513/// assert!(FractionF64::new(1.5).is_err());
514/// ```
515#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
516pub struct FractionF64(f64);
517
518impl FractionF64 {
519    /// Create a new `FractionF64` from an `f64`.
520    ///
521    /// # Errors
522    ///
523    /// Returns [`ErrorKind::InvalidInput`] if `value` is not in the interval
524    /// `(0.0, 1.0]` or is not finite.
525    pub fn new(value: f64) -> Result<Self> {
526        if !(value > 0.0 && value <= 1.0) {
527            return Err(Error::new(ErrorKind::InvalidInput, "FractionF64::new")
528                .with_message("value must be in the interval (0.0, 1.0]")
529                .with_context("value", format!("{value}")));
530        }
531        Ok(FractionF64(value))
532    }
533
534    /// Get the underlying `f64` value.
535    pub const fn get(self) -> f64 {
536        self.0
537    }
538}
539
540impl fmt::Display for FractionF64 {
541    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
542        write!(f, "{}", self.0)
543    }
544}
545
546impl From<FractionF64> for f64 {
547    fn from(val: FractionF64) -> f64 {
548        val.get()
549    }
550}
551
552impl TryFrom<f64> for FractionF64 {
553    type Error = Error;
554
555    fn try_from(value: f64) -> Result<Self> {
556        Self::new(value)
557    }
558}
559
560/// A finite `f64` value in the half-open interval `[0.0, 1.0)`.
561///
562/// Used for solver tolerances libcugraph requires to be non-negative and
563/// below one, such as the spectral clustering eigensolver and k-means
564/// tolerances.
565///
566/// # Construction
567///
568/// ```
569/// use cugraph::params::HalfOpenUnitF64;
570///
571/// assert_eq!(HalfOpenUnitF64::new(0.0).unwrap().get(), 0.0);
572///
573/// // One and values above it are rejected:
574/// assert!(HalfOpenUnitF64::new(1.0).is_err());
575/// assert!(HalfOpenUnitF64::new(-0.1).is_err());
576/// ```
577#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
578pub struct HalfOpenUnitF64(f64);
579
580impl HalfOpenUnitF64 {
581    /// Create a new `HalfOpenUnitF64` from an `f64`.
582    ///
583    /// # Errors
584    ///
585    /// Returns [`ErrorKind::InvalidInput`] if `value` is not in the interval
586    /// `[0.0, 1.0)` or is not finite.
587    pub fn new(value: f64) -> Result<Self> {
588        if !(0.0..1.0).contains(&value) {
589            return Err(Error::new(ErrorKind::InvalidInput, "HalfOpenUnitF64::new")
590                .with_message("value must be in the interval [0.0, 1.0)")
591                .with_context("value", format!("{value}")));
592        }
593        Ok(HalfOpenUnitF64(value))
594    }
595
596    /// Get the underlying `f64` value.
597    pub const fn get(self) -> f64 {
598        self.0
599    }
600}
601
602impl fmt::Display for HalfOpenUnitF64 {
603    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
604        write!(f, "{}", self.0)
605    }
606}
607
608impl From<HalfOpenUnitF64> for f64 {
609    fn from(val: HalfOpenUnitF64) -> f64 {
610        val.get()
611    }
612}
613
614impl TryFrom<f64> for HalfOpenUnitF64 {
615    type Error = Error;
616
617    fn try_from(value: f64) -> Result<Self> {
618        Self::new(value)
619    }
620}
621
622/// A cluster count of at least two.
623///
624/// Upstream libcugraph spectral clustering requires at least two clusters.
625///
626/// # Construction
627///
628/// ```
629/// use cugraph::params::ClusterCount;
630///
631/// assert_eq!(ClusterCount::new(2).unwrap().get(), 2);
632/// assert!(ClusterCount::new(1).is_err());
633/// ```
634#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
635pub struct ClusterCount(usize);
636
637impl ClusterCount {
638    /// Create a new `ClusterCount` from a `usize`.
639    ///
640    /// # Errors
641    ///
642    /// Returns [`ErrorKind::InvalidInput`] if `value` is below two.
643    pub fn new(value: usize) -> Result<Self> {
644        if value < 2 {
645            return Err(Error::new(ErrorKind::InvalidInput, "ClusterCount::new")
646                .with_message("cluster count must be at least 2")
647                .with_context("value", value.to_string()));
648        }
649        Ok(ClusterCount(value))
650    }
651
652    /// Get the underlying value as `usize`.
653    pub const fn get(self) -> usize {
654        self.0
655    }
656}
657
658impl fmt::Display for ClusterCount {
659    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
660        write!(f, "{}", self.0)
661    }
662}
663
664impl From<ClusterCount> for usize {
665    fn from(count: ClusterCount) -> usize {
666        count.get()
667    }
668}
669
670impl TryFrom<usize> for ClusterCount {
671    type Error = Error;
672
673    fn try_from(value: usize) -> Result<Self> {
674        Self::new(value)
675    }
676}