Skip to main content

arpack_rssolve

Function arpack_rssolve 

Source
pub fn arpack_rssolve<F: FnMut(&[f64], &mut [f64])>(
    n: usize,
    matvec: F,
    options: &ArpackOptions,
    storage: Option<&mut ArpackStorage>,
) -> Result<ArpackSymmetricResult>
Expand description

Eigenvalues and eigenvectors of a symmetric linear operator given as a Rust closure, with ARPACK’s implicitly restarted Lanczos method (igraph_arpack_rssolve).

matvec(x, y) must store the product A x into y (which is zeroed before each call, so accumulating into it is fine); both slices have length n. The matrix is never formed: this is the method of choice for large sparse or structured matrices. In ArpackMode::ShiftInvert the closure must compute (A - sigma I)^-1 x instead. A panic in the closure stops ARPACK and is resumed by this function; a product containing NaN or infinite values stops it with an ErrorKind::InvalidValue error.

Time complexity: depends on the matrix-vector product; usually a few iterations suffice, so with an O(n) product the eigenvalues are found in about O(n) time.

In regular mode, 1 × 1 and 2 × 2 problems are solved in closed form by igraph without running ARPACK. For 2 × 2 problems igraph 1.0.0 and 1.0.1 confuse the magnitude-based choices with the algebraic ones and return unnormalized eigenvectors; this wrapper computes both eigenpairs and selects and normalizes the requested ones itself.

§Singular operators: the start vector is multiplied first

ARPACK does not use the start vector v0 itself as the first Krylov vector: it first applies the operator and starts from A v0 (to force the start vector into the range of the operator, see dgetv0). For a singular symmetric operator the range is orthogonal to the null space, so the component of v0 along the null vectors is wiped out: when A v0 has no null component exactly (e.g. integer matrices and start vectors, as for graph Laplacians), the eigenvalue 0 can be invisible to the iteration, and ARPACK then silently returns other eigenvalues instead. A start vector lying in the null space fails outright with ArpackError::ZeroStart. The random default start vector usually recovers the null space through rounding errors, but there is no guarantee.

The robust fix is a shift: solve for A + s I with some s > 0 that makes the operator non-singular (for a positive semidefinite A such as a Laplacian any s > 0 works, and “smallest magnitude” becomes “smallest algebraic”), then subtract s from the eigenvalues; the eigenvectors are unchanged. See the second example below.

§Nesting

igraph’s ARPACK keeps the state of the running iteration in thread-local statics, so it is not re-entrant: matvec (and any interruption or progress handler, which igraph calls from inside the ARPACK loop) must not start another ARPACK computation on the same thread. The ARPACK-based functions of this module (this function, arpack_rnsolve, the SparseMat ARPACK solvers, the ARPACK paths of eigen_matrix_symmetric and eigen_symmetric_fn, Graph::eigen_adjacency and the spectral embeddings) check this: started while another one of them runs on the thread, they fail with an ErrorKind::Failure error instead of corrupting the running solver. So do the ARPACK-based functions of other modules: Graph::eigenvector_centrality, Graph::hub_and_authority_scores, Graph::centralization_eigenvector_centrality, PageRank with PageRankAlgo::Arpack and Graph::community_leading_eigenvector. Calling other igraph functions from matvec is fine, even failing ones: the closure runs in its own level of igraph’s cleanup stack.

Binds igraph_arpack_rssolve.

See also SparseMat::arpack_rssolve for a stored sparse matrix, eigen_symmetric_fn for a front-end that can also use LAPACK, and the graph-level spectral routines built on ARPACK, e.g. Graph::eigenvector_centrality and Graph::eigen_adjacency.

§Errors

Invalid options (see ArpackOptions) give ErrorKind::InvalidValue; ARPACK failures give ErrorKind::Arpack, see ArpackError::from_error for the ARPACK error condition.

§Examples

The cycle graph C_n has adjacency eigenvalues 2 cos(2 pi k / n): the largest is 2, with the constant eigenvector.

use igraph::linalg::{arpack_rssolve, ArpackOptions, ArpackWhich};
let n = 12;
let cycle = |x: &[f64], y: &mut [f64]| {
    for i in 0..n {
        y[i] = x[(i + 1) % n] + x[(i + n - 1) % n];
    }
};
let opts = ArpackOptions::default().with_nev(1).with_which(ArpackWhich::LargestAlgebraic);
let res = arpack_rssolve(n, cycle, &opts, None).unwrap();
assert!((res.values[0] - 2.0).abs() < 1e-10);
let v = res.vectors.column(0);
assert!(v.iter().all(|x| (x.abs() - 1.0 / (n as f64).sqrt()).abs() < 1e-8));

The Laplacian of the path on 10 vertices is singular (eigenvalue 0, with the constant eigenvector). The ramp v0 = (0, 1, ..., 9) has a constant component, but ARPACK iterates from L v0 = e_9 - e_0, which has exactly none (and, by the mirror symmetry of the path, neither have the following Krylov vectors): the eigenvalue 0 is missed altogether. Shifting to L + I keeps the constant component of v0 and finds it:

use igraph::linalg::{arpack_rssolve, ArpackOptions, ArpackWhich};
use std::f64::consts::PI;
let n = 10;
// y = (L + shift I) x for the path Laplacian L.
let laplacian = move |shift: f64| {
    move |x: &[f64], y: &mut [f64]| {
        for i in 0..n {
            let degree = if i == 0 || i == n - 1 { 1.0 } else { 2.0 };
            y[i] = (degree + shift) * x[i];
            if i > 0 {
                y[i] -= x[i - 1];
            }
            if i + 1 < n {
                y[i] -= x[i + 1];
            }
        }
    }
};
let ramp: Vec<f64> = (0..n).map(|i| i as f64).collect();
let lambda1 = 2.0 - 2.0 * (PI / n as f64).cos(); // the smallest non-zero eigenvalue

let opts = ArpackOptions::default()
    .with_nev(2)
    .with_which(ArpackWhich::SmallestMagnitude)
    .with_start(ramp);
let plain = arpack_rssolve(n, laplacian(0.0), &opts, None).unwrap();
assert!(plain.values.iter().all(|v| v.abs() > 0.09)); // 0 is missed
assert!(plain.values.iter().any(|v| (v - lambda1).abs() < 1e-8));

let shifted_opts = opts.with_which(ArpackWhich::SmallestAlgebraic);
let shifted = arpack_rssolve(n, laplacian(1.0), &shifted_opts, None).unwrap();
let values: Vec<f64> = shifted.values.iter().map(|v| v - 1.0).collect();
assert!(values[0].abs() < 1e-10);
assert!((values[1] - lambda1).abs() < 1e-8);