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(°rees, 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}