Skip to main content

igraph/linalg/
mod.rs

1//! Linear algebra: sparse matrices, eigensolvers (ARPACK, LAPACK), BLAS
2//! helpers and spectral embeddings of graphs.
3//!
4//! This module covers six igraph C headers:
5//!
6//! | C header              | Rust                                   | What it offers |
7//! |-----------------------|----------------------------------------|----------------|
8//! | `igraph_sparsemat.h`  | [`SparseMat`], [`SparseLu`], [`SparseQr`], [`SparseMatIter`] | Owned sparse matrices (CXSparse), triplet and column-compressed formats, arithmetic, triangular/LU/QR/Cholesky solvers, ARPACK on sparse matrices |
9//! | `igraph_arpack.h`     | [`arpack_rssolve`], [`arpack_rnsolve`], [`ArpackOptions`], [`ArpackStorage`] | Matrix-free eigensolvers driven by a Rust closure computing `y = A x` |
10//! | `igraph_eigen.h`      | [`eigen_matrix_symmetric`], [`eigen_matrix`], [`Graph::eigen_adjacency`](crate::Graph::eigen_adjacency) | A unified front-end choosing between LAPACK and ARPACK |
11//! | `igraph_lapack.h`     | [`lapack_dgesv`], [`lapack_dgetrf`], [`lapack_dgetrs`], [`lapack_dsyevr`], [`lapack_dgeev`], [`lapack_dgeevx`], [`lapack_dgehrd`] | Dense linear systems and eigenproblems |
12//! | `igraph_blas.h`       | [`blas_dgemv`], [`blas_dgemv_array`], [`blas_dgemm`], [`blas_ddot`], [`blas_dnrm2`] | Dense matrix products, dot products and norms |
13//! | `igraph_embedding.h`  | [`Graph::adjacency_spectral_embedding`](crate::Graph::adjacency_spectral_embedding), [`Graph::laplacian_spectral_embedding`](crate::Graph::laplacian_spectral_embedding), [`dim_select`] | Spectral embeddings of graphs and dimensionality selection |
14//!
15//! Dense matrices are the crate's [`Matrix`](crate::matrix::Matrix) (column-major, like
16//! igraph and LAPACK); vectors are plain Rust slices and [`Vec`]s. Complex numbers
17//! are [`Complex`] (igraph's `igraph_complex_t`, with [`re`](Complex::re) and
18//! [`im`](Complex::im) accessors).
19//!
20//! # Safety net around the C library
21//!
22//! The C routines wrapped here trust their callers a lot: a wrong vector length
23//! reads out of bounds, and an invalid argument handed to the bundled LAPACK
24//! or BLAS makes it *terminate the process*. The wrappers therefore validate
25//! every dimension, index and range on the Rust side and report problems as
26//! [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) errors; a few
27//! igraph quirks (present in both igraph 1.0.0 and 1.0.1: the linear algebra
28//! sources did not change in 1.0.1) are worked around and documented on the
29//! affected functions (e.g. [`SparseMat::transpose`] of non-square triplet matrices,
30//! [`SparseMat::max`], [`blas_dgemm`] with `beta != 0`, 2 × 2 problems in
31//! [`arpack_rssolve`]). One cannot be worked around: the last ARPACK error
32//! is a process-wide C variable, so [`arpack_last_error`] is `unsafe`; use
33//! the thread-safe [`ArpackError::from_error`] instead.
34//!
35//! igraph's ARPACK is not re-entrant (its iteration state lives in
36//! thread-local statics): every ARPACK-based function of the crate (those of
37//! this module, and eigenvector centrality, hub and authority scores,
38//! eigenvector centralization, PageRank with the ARPACK algorithm and
39//! leading eigenvector communities) refuses to run nested inside another
40//! ARPACK run on the same thread (from its matrix-vector closure, or from a
41//! progress or interruption handler called by it), with an
42//! [`ErrorKind::Failure`](crate::ErrorKind::Failure) error (see
43//! [`arpack_rssolve`]). Also note that ARPACK starts from `A v0`, not from
44//! the start vector `v0`: null vectors of singular operators can be missed,
45//! shift the operator instead (see [`arpack_rssolve`]).
46//!
47//! `igraph_arpack_options_get_default` is not wrapped: it hands out a mutable
48//! pointer to igraph's shared default options; use
49//! [`ArpackOptions::default()`](ArpackOptions) (`igraph_arpack_options_init`)
50//! instead.
51//!
52//! # Related functionality in other modules
53//!
54//! | Need | Where |
55//! |------|-------|
56//! | Adjacency matrix of a graph, dense or sparse | [`Graph::get_adjacency`](crate::Graph::get_adjacency), [`Graph::get_adjacency_sparse`](crate::Graph::get_adjacency_sparse) (conversion) |
57//! | Laplacian matrix of a graph, dense or as triplets | [`Graph::get_laplacian`](crate::Graph::get_laplacian), [`Graph::get_laplacian_sparse`](crate::Graph::get_laplacian_sparse) (structural) |
58//! | Stochastic (random-walk) matrix | [`Graph::get_stochastic`](crate::Graph::get_stochastic), [`Graph::get_stochastic_sparse`](crate::Graph::get_stochastic_sparse) (conversion) |
59//! | A graph from a (sparse) adjacency matrix | [`Graph::adjacency`](crate::Graph::adjacency), [`Graph::sparse_adjacency`](crate::Graph::sparse_adjacency), [`Graph::sparse_weighted_adjacency`](crate::Graph::sparse_weighted_adjacency) (constructors) |
60//! | Spectral centralities (ARPACK/PRPACK inside) | [`Graph::eigenvector_centrality`](crate::Graph::eigenvector_centrality), [`Graph::hub_and_authority_scores`](crate::Graph::hub_and_authority_scores), [`Graph::pagerank`](crate::Graph::pagerank) (centrality) |
61//! | Spectral community detection | [`Graph::community_leading_eigenvector`](crate::Graph::community_leading_eigenvector) (community) |
62//! | Spectral layouts | [`Graph::layout_mds`](crate::Graph::layout_mds) (layout) |
63//! | Seeding the random start vectors of ARPACK | [`rng::seed`](crate::rng::seed) (per thread) |
64//!
65//! # Example: the spectrum of a path graph
66//!
67//! The Laplacian of the path on `n` vertices has eigenvalues
68//! `2 - 2 cos(pi k / n)`, `k = 0, ..., n - 1`. Here we take it from the
69//! structural module as sparse triplets, load it into a [`SparseMat`],
70//! compute its two largest eigenvalues with ARPACK and check the smallest
71//! one with a dense LAPACK solver.
72//!
73//! ```
74//! use igraph::linalg::*;
75//! use igraph::prelude::*;
76//! use igraph::structural::LaplacianNormalization;
77//! use std::f64::consts::PI;
78//!
79//! let n = 10;
80//! let path = Graph::ring(n, false, false, false).unwrap();
81//! let triplets: Vec<(usize, usize, f64)> = path
82//!     .get_laplacian_sparse(NeighborMode::All, LaplacianNormalization::Unnormalized, None)
83//!     .unwrap()
84//!     .into_iter()
85//!     .map(|(i, j, x)| (i as usize, j as usize, x))
86//!     .collect();
87//! let lap = SparseMat::from_triplets(n, n, &triplets).unwrap().compress().unwrap();
88//! assert!(lap.is_symmetric().unwrap());
89//!
90//! // ARPACK, largest algebraic eigenvalues (the random start vector comes
91//! // from this thread's igraph RNG: seed it for reproducible iterations).
92//! rng::seed(42).unwrap();
93//! let opts = ArpackOptions::default().with_nev(2).with_which(ArpackWhich::LargestAlgebraic);
94//! let top = lap.arpack_rssolve(&opts, SparseSolveMethod::Lu).unwrap();
95//! for (k, value) in top.values.iter().enumerate() {
96//!     let expected = 2.0 - 2.0 * (PI * (n - 1 - k) as f64 / n as f64).cos();
97//!     assert!((value - expected).abs() < 1e-8);
98//! }
99//!
100//! // LAPACK on the dense copy: the smallest eigenvalue is 0.
101//! let dense = lap.to_dense().unwrap();
102//! let low = lapack_dsyevr(&dense, &SymmetricRange::Select(0..1), 1e-12).unwrap();
103//! assert!(low.values[0].abs() < 1e-10);
104//! ```
105
106mod arpack;
107mod blas;
108mod eigen;
109mod embedding;
110mod lapack;
111mod sparsemat;
112
113pub use arpack::*;
114pub use blas::*;
115pub use eigen::*;
116pub use embedding::*;
117pub use lapack::*;
118pub use sparsemat::*;
119
120use crate::{
121    error::{Error, Result},
122    ffi::*,
123};
124use std::ffi::c_int;
125
126/// A complex number (`igraph_complex_t`), with [`re`](Complex::re) and
127/// [`im`](Complex::im) accessors and [`Complex::new`].
128pub type Complex = igraph_complex_t;
129
130/// Converts a dimension to a C `int` (as used by BLAS, LAPACK and ARPACK),
131/// failing with an [`ErrorKind::Overflow`](crate::ErrorKind::Overflow)-like
132/// invalid value error when it does not fit.
133pub(crate) fn to_c_int(value: usize, what: &str) -> Result<c_int> {
134    c_int::try_from(value).map_err(|_| Error::invalid(format!("{what} ({value}) is too large")))
135}
136
137/// Fails unless every value is finite: NaN and infinite values make the
138/// bundled LAPACK (also used inside ARPACK) terminate the process.
139pub(crate) fn check_finite(values: &[f64], what: &str) -> Result<()> {
140    if values.iter().all(|x| x.is_finite()) {
141        Ok(())
142    } else {
143        Err(Error::invalid(format!(
144            "{what} contains NaN or infinite values"
145        )))
146    }
147}
148
149/// Unpacks LAPACK's compressed real representation of complex eigenvectors:
150/// a real eigenvalue owns one column, a complex conjugate pair `(j, j+1)`
151/// shares columns `j` (real part) and `j+1` (imaginary part).
152pub(crate) fn unpack_lapack_vectors(imag: &[f64], m: &crate::matrix::Matrix) -> Vec<Vec<Complex>> {
153    let n = m.nrow();
154    let mut out = Vec::with_capacity(imag.len());
155    let mut j = 0;
156    while j < imag.len() {
157        if imag[j] == 0.0 || j + 1 >= m.ncol() {
158            out.push(m.column(j).iter().map(|&x| Complex::new(x, 0.0)).collect());
159            j += 1;
160        } else {
161            let (re, im) = (m.column(j), m.column(j + 1));
162            out.push((0..n).map(|r| Complex::new(re[r], im[r])).collect());
163            out.push((0..n).map(|r| Complex::new(re[r], -im[r])).collect());
164            j += 2;
165        }
166    }
167    out
168}