Skip to main content

igraph/misc/
nongraph.rs

1//! Non-graph utilities (`igraph_nongraph.h`, `igraph_version.h`).
2
3use super::lossy;
4use crate::{
5    error::{Error, ErrorKind, Result},
6    ffi::*,
7    igraph_call,
8    vector::{Vector, VectorInt},
9};
10use std::{cmp::Ordering, fmt};
11
12/// Upper bound (2^62) on the samples (and `xmin`) of a discrete model
13/// accepted by [`power_law_fit`] and [`PowerLawFit::p_value`].
14const DISCRETE_SAMPLE_LIMIT: f64 = 4_611_686_018_427_387_904.0;
15
16/// A power-law distribution fitted to a sample by [`power_law_fit`] (the
17/// Rust counterpart of `igraph_plfit_result_t`).
18///
19/// The fitted model is `P(X = x) ∝ x^(-alpha)` for `x >= xmin`. The struct
20/// borrows the fitted sample, which is needed to compute the
21/// [p-value](PowerLawFit::p_value) of the fit.
22#[derive(Debug, Clone, PartialEq)]
23pub struct PowerLawFit<'a> {
24    /// Whether a continuous (`true`) or a discrete (`false`) power law was fitted.
25    pub continuous: bool,
26    /// The fitted exponent `alpha` (larger than 1 for a normalizable law).
27    pub alpha: f64,
28    /// The threshold above which the power-law behavior holds (given, or
29    /// estimated by minimizing the Kolmogorov–Smirnov statistic).
30    pub xmin: f64,
31    /// The log-likelihood of the fitted parameters (`L` in igraph).
32    pub log_likelihood: f64,
33    /// The Kolmogorov–Smirnov test statistic between the fitted
34    /// distribution and the sample (`D` in igraph); the smaller, the better.
35    pub ks_statistic: f64,
36    /// The sample the model was fitted to.
37    pub data: &'a [f64],
38}
39
40impl PowerLawFit<'_> {
41    fn to_raw(&self, data: &Vector) -> igraph_plfit_result_t {
42        igraph_plfit_result_t {
43            continuous: self.continuous,
44            alpha: self.alpha,
45            xmin: self.xmin,
46            L: self.log_likelihood,
47            D: self.ks_statistic,
48            data,
49        }
50    }
51
52    /// Rejects models that igraph's resampling code cannot handle.
53    fn check_model(&self) -> Result<()> {
54        if self.data.is_empty() || self.data.iter().any(|x| !x.is_finite()) {
55            return Err(Error::invalid(
56                "the sample of the model must be non-empty and finite",
57            ));
58        }
59        // plfit's discrete sampler converts heavy-tailed draws to a C `long`
60        // (see `p_value`); huge samples make such overflowing draws likely.
61        if !self.continuous && self.data.iter().any(|&x| x >= DISCRETE_SAMPLE_LIMIT) {
62            return Err(Error::invalid(format!(
63                "the sample of a discrete model must be below 2^62 = {DISCRETE_SAMPLE_LIMIT}"
64            )));
65        }
66        if !(self.alpha > 1.0 && self.alpha.is_finite()) {
67            return Err(Error::invalid(format!(
68                "the exponent of the model must be finite and greater than 1, got {}",
69                self.alpha
70            )));
71        }
72        // plfit converts a discrete `xmin` to a C `long`.
73        let xmin_ok = if self.continuous {
74            self.xmin > 0.0 && self.xmin.is_finite()
75        } else {
76            (1.0..4.0e18).contains(&self.xmin)
77        };
78        if !xmin_ok {
79            return Err(Error::invalid(format!(
80                "invalid xmin for a {} power law: {}",
81                if self.continuous {
82                    "continuous"
83                } else {
84                    "discrete"
85                },
86                self.xmin
87            )));
88        }
89        Ok(())
90    }
91
92    /// Computes the p-value of the fit by a (slow) resampling procedure.
93    ///
94    /// Many synthetic datasets are drawn: the part of the sample below `xmin`
95    /// is resampled from the data itself, the part above `xmin` from the
96    /// fitted power law. A power law is fitted to each of them, and the
97    /// p-value is the fraction of synthetic datasets whose Kolmogorov–Smirnov
98    /// statistic is *larger* than the observed one. Small p-values (e.g.
99    /// below 0.1) mean that the power-law hypothesis can be rejected.
100    ///
101    /// The number of resampling rounds is `0.25 / precision²`: a precision of
102    /// `0.01` means 2500 rounds. Results depend on the thread's default
103    /// random number generator (seed it with [`rng::seed`](crate::rng::seed));
104    /// if igraph was built with OpenMP, the rounds run in parallel and the
105    /// results are not reproducible unless OpenMP is limited to one thread.
106    ///
107    /// Binds [`igraph_plfit_result_calculate_p_value`](https://igraph.org/c/html/latest/igraph-Nongraph.html#igraph_plfit_result_calculate_p_value).
108    ///
109    /// The fields of the struct are public, so the model is checked before
110    /// calling igraph (whose resampling code crashes or loops forever on
111    /// some invalid models): it must have a non-empty, finite sample, a
112    /// finite `alpha > 1` (degenerate fits with `alpha = inf` are rejected),
113    /// and a finite `xmin`, positive for a continuous law and in `[1, 4e18)`
114    /// for a discrete one. The samples of a discrete model must also be below
115    /// `2^62`.
116    ///
117    /// **Known upstream issue.** For discrete models, plfit draws from the
118    /// fitted law with `(long) floor(pow(1 - u, -1 / (alpha - 1)) * xmin)`,
119    /// relying on the conversion of too large values to a C `long` to "handle
120    /// overflow" (`vendor/plfit/sampling.c`). Such a conversion is undefined
121    /// behavior in C; on x86-64 it yields a negative number and the draw is
122    /// retried. This cannot be ruled out from Rust without constraining
123    /// `alpha`: it only happens for extremely heavy tails (`alpha` close to
124    /// 1), which huge samples produce most readily, hence the `2^62` bound
125    /// above.
126    ///
127    /// # Errors
128    /// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if
129    /// `precision` is not a positive number, is so small that the number of
130    /// rounds would not fit in a 64-bit integer (below about `2.5e-10`), or
131    /// so large that there would be no round at all (above `0.5`); if the
132    /// model is invalid (see above); and the errors of [`power_law_fit`].
133    pub fn p_value(&self, precision: f64) -> Result<f64> {
134        if precision.is_nan() || precision <= 0.0 {
135            return Err(Error::invalid(
136                "the precision of the p-value must be positive",
137            ));
138        }
139        // plfit converts the number of rounds to a C `long`: an out of range
140        // value would be undefined behavior.
141        if 0.25 / precision / precision >= 4.0e18 {
142            return Err(Error::invalid(format!(
143                "the precision of the p-value is too small, got {precision}"
144            )));
145        }
146        self.check_model()?;
147        let data = Vector::view(self.data);
148        let raw = self.to_raw(&data);
149        let mut p = 0.0;
150        igraph_call!(igraph_plfit_result_calculate_p_value(
151            &raw, &mut p, precision
152        ))?;
153        Ok(p)
154    }
155}
156
157/// Fits a power-law distribution to a sample, with the maximum likelihood
158/// method of Clauset, Shalizi and Newman.
159///
160/// `data` holds the *samples* (e.g. the degrees of the vertices of a graph),
161/// not a histogram or a distribution function. For a given `xmin`, the
162/// exponent `alpha` that maximizes the likelihood of the samples `>= xmin`
163/// is returned. The threshold is:
164///
165/// - `None`: estimated, choosing the `xmin` for which the
166///   Kolmogorov–Smirnov distance between the fitted law and the sample is
167///   the smallest (slower, as every distinct value is tried);
168/// - `Some(x)`: fixed to `x`; samples below `x` are ignored. `x` must be
169///   positive for a continuous law and at least 1 for a discrete one (so
170///   `Some(1.0)` uses all the positive integer samples); `Some(0.0)` is
171///   rejected by igraph.
172///
173/// A discrete power law is fitted if all samples are integers, unless
174/// `force_continuous` is `true`; a continuous one otherwise.
175///
176/// Degenerate samples give degenerate or failed fits. For a discrete law
177/// with no sample reaching the given `xmin`, igraph returns `alpha = inf`
178/// and a NaN log-likelihood (check `alpha.is_finite()` when `xmin` is
179/// chosen by hand); the continuous fit fails with
180/// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) instead, and
181/// a single sample makes both fail.
182///
183/// See also [`Graph::degree`](crate::Graph::degree) (the usual sample to
184/// fit), and the generators of scale-free graphs
185/// [`Graph::barabasi_game`](crate::Graph::barabasi_game) and
186/// [`Graph::static_power_law_game`](crate::Graph::static_power_law_game).
187///
188/// Reference: A. Clauset, C. R. Shalizi and M. E. J. Newman, *Power-law
189/// distributions in empirical data*, SIAM Review 51(4):661–703, 2009.
190///
191/// Binds [`igraph_power_law_fit`](https://igraph.org/c/html/latest/igraph-Nongraph.html#igraph_power_law_fit).
192/// Time complexity: `O(n log n)` in the continuous case with a fixed `xmin`;
193/// the discrete case is dominated by an L-BFGS optimization; estimating
194/// `xmin` multiplies the cost by the number of distinct samples.
195///
196/// # Errors
197/// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) for invalid
198/// data (e.g. no samples, NaN or infinite samples, an `xmin` below 1 for
199/// discrete or not positive for continuous samples, discrete samples or
200/// `xmin` of `2^62` or more), and other kinds for numerical failures of the
201/// fitting procedure.
202///
203/// Integer samples of `2^62` (about `4.6e18`) or more are only accepted with
204/// `force_continuous = true`: for huge discrete samples plfit's L-BFGS
205/// optimization fails, and igraph 1.0.1 then reads the error message from a
206/// stack buffer that no longer exists. The bound matches the one of
207/// [`PowerLawFit::p_value`], so every discrete fit can be tested.
208///
209/// # Examples
210///
211/// Continuous samples drawn by inverse transform sampling from a power law
212/// with `alpha = 2.5` and `xmin = 1`:
213///
214/// ```
215/// use igraph::{misc, prelude::*};
216///
217/// rng::seed(7)?;
218/// let sample: Vec<f64> =
219///     (0..5000).map(|_| (1.0 - rng::uniform01()).powf(-1.0 / 1.5)).collect();
220/// let fit = misc::power_law_fit(&sample, Some(1.0), false)?;
221/// assert!(fit.continuous);
222/// assert!((fit.alpha - 2.5).abs() < 0.1);
223/// assert_eq!(fit.xmin, 1.0);
224/// # Ok::<(), igraph::Error>(())
225/// ```
226///
227/// The degrees of a Barabási–Albert graph (igraph's
228/// `examples/simple/igraph_power_law_fit.c`, with the same seed and output):
229///
230/// ```
231/// use igraph::{games::BarabasiOptions, misc, prelude::*};
232///
233/// rng::seed(42)?;
234/// let options = BarabasiOptions {
235///     m: 2,
236///     algorithm: BarabasiAlgorithm::Bag,
237///     ..Default::default()
238/// };
239/// let g = Graph::barabasi_game(10_000, &options)?;
240/// let degrees: Vec<f64> = g
241///     .degree(.., NeighborMode::All, Loops::None)?
242///     .into_iter()
243///     .map(|d| d as f64)
244///     .collect();
245/// let fit = misc::power_law_fit(&degrees, None, false)?;
246/// assert!(!fit.continuous); // integer samples: a discrete law
247/// assert_eq!(fit.xmin, 7.0);
248/// assert!((fit.alpha - 3.04393).abs() < 1e-5);
249/// # Ok::<(), igraph::Error>(())
250/// ```
251pub fn power_law_fit(
252    data: &[f64],
253    xmin: Option<f64>,
254    force_continuous: bool,
255) -> Result<PowerLawFit<'_>> {
256    let xmin = match xmin {
257        None => -1.0,
258        Some(x) if x >= 0.0 => x,
259        Some(x) => {
260            return Err(Error::invalid(format!(
261                "xmin must be non-negative, got {x}"
262            )));
263        }
264    };
265    // When the L-BFGS optimization of the discrete fit fails, plfit formats
266    // its error message into a stack buffer that igraph reads after it is
267    // gone (a use-after-return in igraph 1.0.1). It fails for infinite
268    // samples, and for huge ones (seen from about 1e158 on): reject both,
269    // with a wide margin for the latter.
270    if data.iter().any(|x| !x.is_finite()) {
271        return Err(Error::invalid(
272            "the data must not contain NaN or infinite values",
273        ));
274    }
275    // igraph fits a discrete law iff all samples are integers.
276    let discrete = !force_continuous && data.iter().all(|x| x.trunc() == *x);
277    if discrete
278        && (xmin >= DISCRETE_SAMPLE_LIMIT || data.iter().any(|&x| x >= DISCRETE_SAMPLE_LIMIT))
279    {
280        return Err(Error::invalid(format!(
281            "discrete samples and xmin must be below 2^62 = {DISCRETE_SAMPLE_LIMIT} \
282             (use force_continuous for larger values)"
283        )));
284    }
285    let view = Vector::view(data);
286    let mut raw = igraph_plfit_result_t {
287        continuous: false,
288        alpha: 0.0,
289        xmin: 0.0,
290        L: 0.0,
291        D: 0.0,
292        data: std::ptr::null(),
293    };
294    igraph_call!(igraph_power_law_fit(
295        view.as_ptr(),
296        &mut raw,
297        xmin,
298        force_continuous
299    ))?;
300    Ok(PowerLawFit {
301        continuous: raw.continuous,
302        alpha: raw.alpha,
303        xmin: raw.xmin,
304        log_likelihood: raw.L,
305        ks_statistic: raw.D,
306        data,
307    })
308}
309
310/// Running (moving) mean of `data` over windows of `binwidth` consecutive
311/// values.
312///
313/// The result has `data.len() - binwidth + 1` elements: element `i` is the
314/// mean of `data[i..i + binwidth]`.
315///
316/// Binds [`igraph_running_mean`](https://igraph.org/c/html/latest/igraph-Nongraph.html#igraph_running_mean).
317/// Time complexity: `O(n)`.
318///
319/// # Errors
320/// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if
321/// `binwidth` is zero or larger than `data.len()`.
322///
323/// # Examples
324///
325/// ```
326/// use igraph::misc;
327/// assert_eq!(misc::running_mean(&[1.0, 2.0, 3.0, 4.0, 5.0], 2)?, vec![1.5, 2.5, 3.5, 4.5]);
328/// # Ok::<(), igraph::Error>(())
329/// ```
330pub fn running_mean(data: &[f64], binwidth: usize) -> Result<Vec<f64>> {
331    let view = Vector::view(data);
332    let mut res = Vector::new();
333    igraph_call!(igraph_running_mean(
334        view.as_ptr(),
335        &mut res,
336        binwidth as igraph_int_t
337    ))?;
338    Ok(res.into())
339}
340
341/// Draws `length` distinct integers uniformly at random from the closed
342/// interval `[low, high]`, returned in increasing order.
343///
344/// It uses Vitter's sequential sampling algorithm ("Method D"), which runs in
345/// expected `O(length)` time and memory, regardless of the size of the
346/// interval: ideal to select a few edges out of a huge set of candidates.
347/// It draws from the thread's default random number generator. An empty
348/// sample (`length == 0`) is always empty, also when `low == high` (igraph
349/// 1.0.0 and 1.0.1 return `[low]` there; the wrapper corrects this).
350///
351/// See also [`rng::shuffle`](crate::rng::shuffle) for random permutations,
352/// and [`Graph::erdos_renyi_game_gnm`](crate::Graph::erdos_renyi_game_gnm),
353/// which picks its edges among all vertex pairs with the same method.
354///
355/// Reference: J. S. Vitter, *An efficient algorithm for sequential random
356/// sampling*, ACM Transactions on Mathematical Software 13(1):58–67, 1987.
357///
358/// Binds [`igraph_random_sample`](https://igraph.org/c/html/latest/igraph-Nongraph.html#igraph_random_sample).
359///
360/// # Errors
361/// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) if
362/// `low > high` or `length` exceeds the size of the interval;
363/// [`ErrorKind::Overflow`](crate::ErrorKind::Overflow) for a non-empty
364/// sample from an interval with more than `i64::MAX` elements.
365///
366/// # Examples
367///
368/// ```
369/// use igraph::{misc, prelude::*};
370/// rng::seed(1)?;
371/// let s = misc::random_sample(0, 1_000_000_000_000, 5)?;
372/// assert_eq!(s.len(), 5);
373/// assert!(s.windows(2).all(|w| w[0] < w[1]));
374/// # Ok::<(), igraph::Error>(())
375/// ```
376pub fn random_sample(low: i64, high: i64, length: usize) -> Result<Vec<i64>> {
377    if low > high {
378        return Err(Error::invalid(format!(
379            "the lower limit {low} is greater than the upper limit {high}"
380        )));
381    }
382    let Ok(length) = igraph_int_t::try_from(length) else {
383        return Err(Error::invalid("sample size exceeds size of candidate pool"));
384    };
385    // igraph 1.0.0 and 1.0.1 return `[low]` when `low == high`, even for
386    // `length == 0`.
387    if length == 0 {
388        return Ok(Vec::new());
389    }
390    // igraph computes `high + (-low)`, which overflows (undefined behavior
391    // in C) for `low == i64::MIN`: sample from `[0, high - low]` and shift.
392    // The algorithm only depends on the size of the interval, so the result
393    // (and the use of the random number generator) is the same.
394    let Some(span) = high.checked_sub(low) else {
395        return Err(Error::new(
396            ErrorKind::Overflow,
397            "the interval has more than i64::MAX elements",
398        ));
399    };
400    let mut res = VectorInt::new();
401    igraph_call!(igraph_random_sample(&mut res, 0, span, length))?;
402    Ok(res.iter().map(|&x| x + low).collect())
403}
404
405/// Whether `a` and `b` are equal up to the relative tolerance `eps`, i.e.
406/// whether `|a - b| / (|a| + |b|) < eps` (with sensible handling of zeros,
407/// infinities and NaNs, see [`cmp_epsilon`]).
408///
409/// Binds [`igraph_almost_equals`](https://igraph.org/c/html/latest/igraph-Nongraph.html#igraph_almost_equals).
410///
411/// # Examples
412///
413/// ```
414/// use igraph::misc::almost_equals;
415/// assert!(almost_equals(0.1 + 0.2, 0.3, 1e-12));
416/// assert!(!almost_equals(1.0, 1.1, 1e-3));
417/// ```
418pub fn almost_equals(a: f64, b: f64, eps: f64) -> bool {
419    unsafe { igraph_almost_equals(a, b, eps) }
420}
421
422/// Three-way comparison of `a` and `b` with the relative tolerance `eps`:
423/// [`Ordering::Equal`] when `|a - b| / (|a| + |b|) < eps`, otherwise the
424/// natural order of the two numbers.
425///
426/// Infinities are equal to infinities of the same sign and are larger (or
427/// smaller) than every finite number, whatever the tolerance. NaN is never
428/// equal to anything (not even NaN), but the direction of the resulting
429/// ordering is unspecified. An `eps` of zero means exact comparison;
430/// negative values of `eps` give unspecified results.
431///
432/// Binds [`igraph_cmp_epsilon`](https://igraph.org/c/html/latest/igraph-Nongraph.html#igraph_cmp_epsilon).
433///
434/// # Examples
435///
436/// ```
437/// use igraph::misc::cmp_epsilon;
438/// use std::cmp::Ordering;
439///
440/// assert_eq!(cmp_epsilon(1.0, 1.0 + 1e-12, 1e-9), Ordering::Equal);
441/// assert_eq!(cmp_epsilon(1.0, 2.0, 1e-9), Ordering::Less);
442/// assert_eq!(cmp_epsilon(f64::INFINITY, 1e300, 0.5), Ordering::Greater);
443/// ```
444pub fn cmp_epsilon(a: f64, b: f64, eps: f64) -> Ordering {
445    unsafe { igraph_cmp_epsilon(a, b, eps) }.cmp(&0)
446}
447
448/// The version of the igraph C library this crate is linked against, as
449/// returned by [`version`].
450#[derive(Debug, Clone, PartialEq, Eq, Hash)]
451pub struct Version {
452    /// The major version, e.g. `1` for `"1.0.1"`.
453    pub major: i32,
454    /// The minor version, e.g. `0` for `"1.0.1"`.
455    pub minor: i32,
456    /// The patch (subminor) version, e.g. `1` for `"1.0.1"`.
457    pub patch: i32,
458    /// The full version string: three dot-separated numbers, possibly
459    /// followed by a dash-separated pre-release suffix
460    /// (e.g. `"0.10.13-14-g997f59ad7"`).
461    pub string: String,
462}
463
464impl Version {
465    /// The `(major, minor, patch)` triple, handy for comparisons such as
466    /// `version().triple() >= (1, 0, 0)`.
467    pub fn triple(&self) -> (i32, i32, i32) {
468        (self.major, self.minor, self.patch)
469    }
470}
471
472impl fmt::Display for Version {
473    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
474        f.write_str(&self.string)
475    }
476}
477
478/// The version of the igraph C library in use (at run time).
479///
480/// This is the library actually loaded by the dynamic linker, which may
481/// differ from the one whose headers the crate was compiled against (e.g.
482/// when `LD_LIBRARY_PATH` points to another build): this crate targets
483/// igraph 1.0.1.
484///
485/// Binds [`igraph_version`](https://igraph.org/c/html/latest/igraph-Nongraph.html#igraph_version).
486///
487/// # Examples
488///
489/// ```
490/// let v = igraph::misc::version();
491/// assert!(v.triple() >= (1, 0, 0));
492/// assert!(v.to_string().starts_with(&format!("{}.{}.{}", v.major, v.minor, v.patch)));
493/// ```
494pub fn version() -> Version {
495    let mut string: *const std::ffi::c_char = std::ptr::null();
496    let (mut major, mut minor, mut patch) = (0, 0, 0);
497    unsafe { igraph_version(&mut string, &mut major, &mut minor, &mut patch) };
498    Version {
499        major,
500        minor,
501        patch,
502        string: lossy(string),
503    }
504}