Skip to main content

igraph/linalg/
arpack.rs

1//! The ARPACK interface (`igraph_arpack.h`).
2//!
3//! `igraph_arpack_options_get_default` is not wrapped: it hands out a mutable
4//! pointer to igraph's shared default options; use
5//! [`ArpackOptions::default()`](ArpackOptions) (`igraph_arpack_options_init`)
6//! instead.
7
8use super::{Complex, to_c_int};
9use crate::{
10    error::{Error, ErrorKind, Result, catch_panic, check, ensure_init},
11    ffi::*,
12    igraph_call,
13    matrix::Matrix,
14    vector::Vector,
15};
16use std::{
17    cell::Cell,
18    ffi::{CStr, c_char, c_int, c_void},
19    fmt,
20    mem::MaybeUninit,
21    ptr,
22};
23
24/// Which eigenvalues ARPACK should compute (the `which` field of
25/// `igraph_arpack_options_t`).
26///
27/// The first five are valid for symmetric problems ([`arpack_rssolve`]),
28/// the magnitude-based and the real/imaginary-part-based ones for
29/// non-symmetric problems ([`arpack_rnsolve`]).
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
31pub enum ArpackWhich {
32    /// Largest magnitude (`"LM"`), the default; symmetric and non-symmetric.
33    #[default]
34    LargestMagnitude,
35    /// Smallest magnitude (`"SM"`); symmetric and non-symmetric.
36    SmallestMagnitude,
37    /// Largest algebraic value (`"LA"`); symmetric only.
38    LargestAlgebraic,
39    /// Smallest algebraic value (`"SA"`); symmetric only.
40    SmallestAlgebraic,
41    /// Half from each end of the spectrum, one more from the high end when
42    /// `nev` is odd (`"BE"`); symmetric only.
43    BothEnds,
44    /// Largest real part (`"LR"`); non-symmetric only.
45    LargestReal,
46    /// Smallest real part (`"SR"`); non-symmetric only.
47    SmallestReal,
48    /// Largest imaginary part (`"LI"`); non-symmetric only.
49    LargestImaginary,
50    /// Smallest imaginary part (`"SI"`); non-symmetric only.
51    SmallestImaginary,
52}
53
54impl ArpackWhich {
55    /// The two-letter ARPACK code, e.g. `"LM"`.
56    pub fn code(self) -> &'static str {
57        match self {
58            Self::LargestMagnitude => "LM",
59            Self::SmallestMagnitude => "SM",
60            Self::LargestAlgebraic => "LA",
61            Self::SmallestAlgebraic => "SA",
62            Self::BothEnds => "BE",
63            Self::LargestReal => "LR",
64            Self::SmallestReal => "SR",
65            Self::LargestImaginary => "LI",
66            Self::SmallestImaginary => "SI",
67        }
68    }
69
70    /// Whether this choice is valid for symmetric problems.
71    pub fn is_symmetric(self) -> bool {
72        matches!(
73            self,
74            Self::LargestMagnitude
75                | Self::SmallestMagnitude
76                | Self::LargestAlgebraic
77                | Self::SmallestAlgebraic
78                | Self::BothEnds
79        )
80    }
81
82    /// Whether this choice is valid for non-symmetric problems.
83    pub fn is_nonsymmetric(self) -> bool {
84        !matches!(
85            self,
86            Self::LargestAlgebraic | Self::SmallestAlgebraic | Self::BothEnds
87        )
88    }
89}
90
91/// The kind of eigenproblem ARPACK solves (the `mode` field of
92/// `igraph_arpack_options_t`).
93#[derive(Debug, Clone, Copy, PartialEq, Default)]
94pub enum ArpackMode {
95    /// Standard problem `A x = lambda x`, driven by products `y = A x`
96    /// (`mode = 1`). The default.
97    #[default]
98    Regular,
99    /// Shift-and-invert mode (`mode = 3`): the operator is
100    /// `(A - sigma I)^-1`, so that the eigenvalues of `A` closest to `sigma`
101    /// become the largest in magnitude. With the closure-based solvers the
102    /// closure must compute `y = (A - sigma I)^-1 x`; the returned
103    /// eigenvalues are those of `A`. [`SparseMat::arpack_rssolve`](super::SparseMat::arpack_rssolve)
104    /// factorizes `A - sigma I` itself.
105    ShiftInvert {
106        /// The shift.
107        sigma: f64,
108    },
109}
110
111/// Options of the ARPACK eigensolvers (a safe subset of
112/// `igraph_arpack_options_t`).
113///
114/// [`Default`] gives igraph's defaults (`igraph_arpack_options_init`): one
115/// eigenvalue of largest magnitude, machine precision tolerance, automatic
116/// number of Lanczos/Arnoldi vectors, at most 3000 iterations, regular mode
117/// and a random start vector (drawn from igraph's RNG, so results are
118/// reproducible after [`rng::seed`](crate::rng::seed)).
119///
120/// The functions that pick `which` and `nev` themselves (e.g.
121/// [`eigen_matrix_symmetric`](super::eigen_matrix_symmetric),
122/// [`Graph::eigen_adjacency`](crate::Graph::eigen_adjacency) or the spectral
123/// embeddings) ignore those fields as well as `ncv`, `mode` and the start
124/// vector: they only use `tol` and `mxiter`.
125///
126/// ```
127/// use igraph::linalg::{ArpackOptions, ArpackWhich};
128/// let opts = ArpackOptions::default().with_nev(3).with_which(ArpackWhich::SmallestAlgebraic).with_tol(1e-10);
129/// assert_eq!(opts.nev, 3);
130/// assert_eq!(opts.mxiter, 3000);
131/// ```
132#[derive(Debug, Clone, PartialEq)]
133pub struct ArpackOptions {
134    /// Number of eigenvalues to compute (`nev`), at least 1 and smaller than
135    /// the order of the matrix (at most `n - 2` for non-symmetric problems).
136    pub nev: usize,
137    /// Which eigenvalues to compute.
138    pub which: ArpackWhich,
139    /// Relative accuracy of the Ritz values; `0` means machine precision.
140    pub tol: f64,
141    /// Number of Lanczos (Arnoldi) vectors; `0` lets igraph choose. Otherwise
142    /// it must satisfy `nev < ncv <= n`.
143    pub ncv: usize,
144    /// Maximum number of Arnoldi update iterations.
145    pub mxiter: usize,
146    /// Regular or shift-and-invert mode.
147    pub mode: ArpackMode,
148    /// Starting vector (length `n`); `None` draws a random one.
149    pub start: Option<Vec<f64>>,
150}
151
152impl Default for ArpackOptions {
153    fn default() -> Self {
154        Self {
155            nev: 1,
156            which: ArpackWhich::LargestMagnitude,
157            tol: 0.0,
158            ncv: 0,
159            mxiter: 3000,
160            mode: ArpackMode::Regular,
161            start: None,
162        }
163    }
164}
165
166impl ArpackOptions {
167    /// Sets the number of eigenvalues to compute.
168    pub fn with_nev(mut self, nev: usize) -> Self {
169        self.nev = nev;
170        self
171    }
172
173    /// Sets which eigenvalues to compute.
174    pub fn with_which(mut self, which: ArpackWhich) -> Self {
175        self.which = which;
176        self
177    }
178
179    /// Sets the tolerance.
180    pub fn with_tol(mut self, tol: f64) -> Self {
181        self.tol = tol;
182        self
183    }
184
185    /// Sets the number of Lanczos/Arnoldi vectors (`0` = automatic).
186    pub fn with_ncv(mut self, ncv: usize) -> Self {
187        self.ncv = ncv;
188        self
189    }
190
191    /// Sets the maximum number of iterations.
192    pub fn with_mxiter(mut self, mxiter: usize) -> Self {
193        self.mxiter = mxiter;
194        self
195    }
196
197    /// Selects the shift-and-invert mode with the given shift.
198    pub fn with_shift_invert(mut self, sigma: f64) -> Self {
199        self.mode = ArpackMode::ShiftInvert { sigma };
200        self
201    }
202
203    /// Sets the starting vector.
204    pub fn with_start(mut self, start: Vec<f64>) -> Self {
205        self.start = Some(start);
206        self
207    }
208
209    /// Validates the options for a problem of order `n` and converts them to
210    /// the raw C struct.
211    pub(crate) fn to_raw(&self, n: usize, symmetric: bool) -> Result<igraph_arpack_options_t> {
212        let cn = to_c_int(n, "matrix order")?;
213        if n == 0 {
214            return Err(Error::invalid("ARPACK needs a matrix of order at least 1"));
215        }
216        if self.nev == 0 {
217            return Err(Error::invalid("ARPACK: nev must be at least 1"));
218        }
219        if self.ncv != 0 && (self.ncv <= self.nev || self.ncv > n) {
220            return Err(Error::invalid(format!(
221                "ARPACK: ncv ({}) must satisfy nev < ncv <= n",
222                self.ncv
223            )));
224        }
225        if self.mxiter == 0 {
226            return Err(Error::invalid("ARPACK: mxiter must be positive"));
227        }
228        if symmetric && !self.which.is_symmetric() {
229            return Err(Error::invalid(format!(
230                "ARPACK: '{}' is not valid for symmetric problems",
231                self.which.code()
232            )));
233        }
234        if !symmetric && !self.which.is_nonsymmetric() {
235            return Err(Error::invalid(format!(
236                "ARPACK: '{}' is not valid for non-symmetric problems",
237                self.which.code()
238            )));
239        }
240        if !(self.tol.is_finite() && self.tol >= 0.0) {
241            return Err(Error::invalid(
242                "ARPACK: tol must be finite and non-negative",
243            ));
244        }
245        if let ArpackMode::ShiftInvert { sigma } = self.mode
246            && !sigma.is_finite()
247        {
248            return Err(Error::invalid("ARPACK: the shift must be finite"));
249        }
250        if let Some(start) = &self.start
251            && start.len() != n
252        {
253            return Err(Error::invalid(format!(
254                "ARPACK: start vector has length {}, expected {n}",
255                start.len()
256            )));
257        }
258        let mut raw = default_raw_options();
259        raw.n = cn;
260        raw.nev = to_c_int(self.nev, "nev")?;
261        let code = self.which.code().as_bytes();
262        raw.which = [code[0] as c_char, code[1] as c_char];
263        raw.tol = self.tol;
264        raw.ncv = to_c_int(self.ncv, "ncv")?;
265        raw.mxiter = to_c_int(self.mxiter, "mxiter")?;
266        match self.mode {
267            ArpackMode::Regular => raw.mode = 1,
268            ArpackMode::ShiftInvert { sigma } => {
269                raw.mode = 3;
270                raw.sigma = sigma;
271            }
272        }
273        raw.start = self.start.is_some() as c_int;
274        Ok(raw)
275    }
276
277    /// Raw options for the functions that set `which`/`nev` themselves: the
278    /// start vector is dropped.
279    pub(crate) fn to_raw_unchecked_which(&self, n: usize) -> Result<igraph_arpack_options_t> {
280        let mut o = self.clone();
281        o.start = None;
282        o.which = ArpackWhich::LargestMagnitude;
283        o.nev = 1;
284        o.ncv = 0;
285        o.mode = ArpackMode::Regular;
286        o.to_raw(n.max(1), true)
287    }
288
289    /// The `vectors` matrix to hand to the solver: `n` × 1 holding the start
290    /// vector when there is one (igraph reads it from the first column).
291    pub(crate) fn start_matrix(&self, n: usize) -> Result<Matrix> {
292        match &self.start {
293            Some(s) => Matrix::from_column_major(n, 1, s),
294            None => Ok(Matrix::new()),
295        }
296    }
297}
298
299fn default_raw_options() -> igraph_arpack_options_t {
300    let mut raw = MaybeUninit::<igraph_arpack_options_t>::uninit();
301    unsafe {
302        igraph_arpack_options_init(raw.as_mut_ptr());
303        raw.assume_init()
304    }
305}
306
307/// Statistics reported by ARPACK after a successful run (the output fields
308/// of `igraph_arpack_options_t`).
309#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
310pub struct ArpackInfo {
311    /// Number of Arnoldi iterations taken (`noiter`).
312    pub iterations: usize,
313    /// Number of converged Ritz values (`nconv`).
314    pub nconv: usize,
315    /// Total number of operator applications, i.e. calls of the matrix-vector
316    /// product (`numop`), as reported by ARPACK (the ARPACK bundled with
317    /// igraph 1.0.0 and 1.0.1 leave it at 0).
318    pub numop: usize,
319    /// Total number of re-orthogonalization steps (`numreo`).
320    pub numreo: usize,
321}
322
323impl ArpackInfo {
324    pub(crate) fn from_raw(raw: &igraph_arpack_options_t) -> Self {
325        let u = |x: c_int| x.max(0) as usize;
326        Self {
327            iterations: u(raw.noiter),
328            nconv: u(raw.nconv),
329            numop: u(raw.numop),
330            numreo: u(raw.numreo),
331        }
332    }
333}
334
335/// Result of the symmetric ARPACK solvers.
336#[derive(Debug, Clone, PartialEq)]
337pub struct ArpackSymmetricResult {
338    /// The eigenvalues, ordered according to [`ArpackOptions::which`]:
339    /// decreasing for `LargestAlgebraic`, increasing for `SmallestAlgebraic`,
340    /// by decreasing (increasing) magnitude for `LargestMagnitude`
341    /// (`SmallestMagnitude`), and alternating smallest / largest (starting
342    /// from the smallest) for `BothEnds`.
343    pub values: Vec<f64>,
344    /// The corresponding unit eigenvectors, in the columns (`n` × `nev`).
345    pub vectors: Matrix,
346    /// Convergence statistics.
347    pub info: ArpackInfo,
348}
349
350impl ArpackSymmetricResult {
351    pub(crate) fn from_raw(values: Vector, vectors: Matrix, raw: &igraph_arpack_options_t) -> Self {
352        Self {
353            values: values.into(),
354            vectors,
355            info: ArpackInfo::from_raw(raw),
356        }
357    }
358
359    /// Picks the requested eigenpairs out of both eigenpairs of a 2 × 2
360    /// problem solved by igraph's closed-form shortcut (see
361    /// [`uses_2x2_shortcut`]), normalizing the eigenvectors and ordering them
362    /// as ARPACK would.
363    pub(crate) fn select_2x2(self, which: ArpackWhich, nev: usize) -> Result<Self> {
364        let n = self.vectors.nrow();
365        let mut pairs: Vec<(f64, Vec<f64>)> = self
366            .values
367            .iter()
368            .enumerate()
369            .map(|(j, &value)| {
370                let v = self.vectors.column(j);
371                let norm = v.iter().map(|x| x * x).sum::<f64>().sqrt();
372                let v = if norm > 0.0 {
373                    v.iter().map(|x| x / norm).collect()
374                } else {
375                    v.to_vec()
376                };
377                (value, v)
378            })
379            .collect();
380        let key: fn(&f64, &f64) -> std::cmp::Ordering = match which {
381            ArpackWhich::LargestAlgebraic => |a, b| b.total_cmp(a),
382            ArpackWhich::SmallestAlgebraic | ArpackWhich::BothEnds => |a, b| a.total_cmp(b),
383            ArpackWhich::SmallestMagnitude => |a, b| a.abs().total_cmp(&b.abs()),
384            _ => |a, b| b.abs().total_cmp(&a.abs()),
385        };
386        pairs.sort_by(|a, b| key(&a.0, &b.0));
387        let keep = nev.min(pairs.len());
388        if which == ArpackWhich::BothEnds {
389            // Increasing order; with a single eigenvalue it is the largest.
390            pairs.drain(..pairs.len() - keep);
391        } else {
392            pairs.truncate(keep);
393        }
394        let data: Vec<f64> = pairs.iter().flat_map(|(_, v)| v.iter().copied()).collect();
395        let mut info = self.info;
396        info.nconv = info.nconv.min(keep);
397        Ok(Self {
398            values: pairs.iter().map(|&(x, _)| x).collect(),
399            vectors: Matrix::from_column_major(n, keep, &data)?,
400            info,
401        })
402    }
403}
404
405/// Whether igraph solves this symmetric problem with its closed-form 2 × 2
406/// shortcut (`igraph_i_arpack_rssolve_2x2`, regular mode only) instead of
407/// ARPACK. In igraph 1.0.0 and 1.0.1 that shortcut treats "largest
408/// (smallest) magnitude" as "largest (smallest) algebraic" and does not
409/// normalize the eigenvectors: the callers ask it for both eigenpairs and
410/// finish with [`ArpackSymmetricResult::select_2x2`].
411pub(crate) fn uses_2x2_shortcut(n: usize, options: &ArpackOptions) -> bool {
412    n == 2 && options.mode == ArpackMode::Regular
413}
414
415/// Result of the non-symmetric ARPACK solvers.
416#[derive(Debug, Clone, PartialEq)]
417pub struct ArpackNonSymmetricResult {
418    /// The (possibly complex) eigenvalues; complex ones come in conjugate pairs.
419    pub values: Vec<Complex>,
420    /// The corresponding eigenvectors, `vectors[k]` belonging to `values[k]`,
421    /// with unit Euclidean norm.
422    pub vectors: Vec<Vec<Complex>>,
423    /// Convergence statistics.
424    pub info: ArpackInfo,
425}
426
427impl ArpackNonSymmetricResult {
428    /// Decodes the packed output of `igraph_arpack_rnsolve` (values: `k` × 2
429    /// matrix of real/imaginary parts; vectors: one column per real
430    /// eigenvalue, two columns per complex conjugate pair).
431    pub(crate) fn from_raw(
432        values: Matrix,
433        vectors: Matrix,
434        raw: &igraph_arpack_options_t,
435        nev: usize,
436    ) -> Result<Self> {
437        let k = nev.min(values.nrow());
438        let (re, im): (Vec<f64>, Vec<f64>) =
439            (0..k).map(|i| (values[(i, 0)], values[(i, 1)])).unzip();
440        let n = vectors.nrow();
441        let mut out_values = Vec::with_capacity(k);
442        let mut out_vectors = Vec::with_capacity(k);
443        let (mut i, mut col) = (0, 0);
444        while i < k && col < vectors.ncol() {
445            if im[i] == 0.0 {
446                out_values.push(Complex::new(re[i], 0.0));
447                out_vectors.push(
448                    vectors
449                        .column(col)
450                        .iter()
451                        .map(|&x| Complex::new(x, 0.0))
452                        .collect(),
453                );
454                col += 1;
455                i += 1;
456                continue;
457            }
458            if col + 1 >= vectors.ncol() {
459                break;
460            }
461            // The stored vector belongs to the eigenvalue with positive imaginary part.
462            let (vr, vi) = (vectors.column(col), vectors.column(col + 1));
463            let pos: Vec<Complex> = (0..n).map(|r| Complex::new(vr[r], vi[r])).collect();
464            let conj: Vec<Complex> = pos.iter().map(|c| Complex::new(c.re(), -c.im())).collect();
465            let (first, second) = if im[i] > 0.0 {
466                (pos, conj)
467            } else {
468                (conj, pos)
469            };
470            out_values.push(Complex::new(re[i], im[i]));
471            out_vectors.push(first);
472            if i + 1 < k && im[i + 1] == -im[i] && re[i + 1] == re[i] {
473                out_values.push(Complex::new(re[i + 1], im[i + 1]));
474                out_vectors.push(second);
475                i += 2;
476            } else {
477                i += 1;
478            }
479            col += 2;
480        }
481        if n <= 2 {
482            // igraph's closed-form 1 × 1 and 2 × 2 shortcuts do not normalize
483            // the eigenvectors, unlike ARPACK.
484            for v in &mut out_vectors {
485                let norm = v
486                    .iter()
487                    .map(|c| c.re() * c.re() + c.im() * c.im())
488                    .sum::<f64>()
489                    .sqrt();
490                if norm > 0.0 {
491                    for c in v.iter_mut() {
492                        *c = Complex::new(c.re() / norm, c.im() / norm);
493                    }
494                }
495            }
496        }
497        Ok(Self {
498            values: out_values,
499            vectors: out_vectors,
500            info: ArpackInfo::from_raw(raw),
501        })
502    }
503}
504
505/// Preallocated working memory for repeated ARPACK runs
506/// (`igraph_arpack_storage_t`), freed on drop (`igraph_arpack_storage_destroy`).
507///
508/// Pass it to [`arpack_rssolve`]/[`arpack_rnsolve`] to avoid reallocating
509/// the workspace for every eigenproblem of order at most `maxn` using at most
510/// `maxncv` Lanczos/Arnoldi vectors (remember that `ncv = 0` lets igraph pick
511/// a value up to `n`).
512///
513/// Binds [`igraph_arpack_storage_init`](https://igraph.org/c/html/latest/igraph-Linalg.html#igraph_arpack_storage_init).
514pub struct ArpackStorage {
515    raw: igraph_arpack_storage_t,
516    symmetric: bool,
517}
518
519// The storage is plain heap memory owned exclusively by this value.
520unsafe impl Send for ArpackStorage {}
521
522impl ArpackStorage {
523    /// Allocates storage for problems of order at most `maxn`, with at most
524    /// `maxncv` Lanczos/Arnoldi vectors; `symmetric` selects the layout for
525    /// [`arpack_rssolve`] (`true`) or [`arpack_rnsolve`] (`false`; such a
526    /// storage also works for symmetric problems).
527    pub fn new(maxn: usize, maxncv: usize, symmetric: bool) -> Result<Self> {
528        if maxn == 0 || maxncv == 0 {
529            return Err(Error::invalid("ARPACK storage dimensions must be positive"));
530        }
531        to_c_int(maxn, "maxn")?;
532        to_c_int(maxncv, "maxncv")?;
533        ensure_init();
534        let mut raw = MaybeUninit::<igraph_arpack_storage_t>::zeroed();
535        let (n, ncv) = (maxn as igraph_int_t, maxncv as igraph_int_t);
536        check(unsafe { igraph_arpack_storage_init(raw.as_mut_ptr(), n, ncv, n, symmetric) })?;
537        Ok(Self {
538            raw: unsafe { raw.assume_init() },
539            symmetric,
540        })
541    }
542
543    /// Maximum order of the problems.
544    pub fn maxn(&self) -> usize {
545        self.raw.maxn as usize
546    }
547
548    /// Maximum number of Lanczos/Arnoldi vectors.
549    pub fn maxncv(&self) -> usize {
550        self.raw.maxncv as usize
551    }
552
553    /// Whether the storage was allocated for symmetric problems only.
554    pub fn is_symmetric(&self) -> bool {
555        self.symmetric
556    }
557}
558
559impl Drop for ArpackStorage {
560    fn drop(&mut self) {
561        unsafe { igraph_arpack_storage_destroy(&mut self.raw) };
562    }
563}
564
565impl fmt::Debug for ArpackStorage {
566    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
567        f.debug_struct("ArpackStorage")
568            .field("maxn", &self.maxn())
569            .field("maxncv", &self.maxncv())
570            .field("symmetric", &self.symmetric)
571            .finish()
572    }
573}
574
575thread_local! {
576    static ARPACK_ACTIVE: Cell<bool> = const { Cell::new(false) };
577}
578
579/// Marks an ARPACK run on the calling thread, to refuse nested runs.
580///
581/// igraph's vendored ARPACK is not re-entrant: its Fortran-translated
582/// routines (`dsaupd`, `dsaup2`, `dnaupd`, `dnaup2`, `dgetv0`, ...) keep the
583/// state of the running iteration in `IGRAPH_F77_SAVE` variables, i.e.
584/// thread-local statics shared by every ARPACK run on the thread. A second run
585/// started from inside a running one (from the matrix-vector closure of
586/// [`arpack_rssolve`], or from an interruption or progress handler, which
587/// igraph calls from inside the ARPACK loop) overwrites that state, and the
588/// outer run then returns wrong results, fails or hangs; as the state includes
589/// indices into the work arrays, it could even access memory out of bounds.
590///
591/// A wrapper whose C function may run ARPACK holds a guard for the whole C
592/// call (`let _arpack = crate::linalg::ArpackGuard::enter()?;`); a nested
593/// entry returns an [`ErrorKind::Failure`] error instead. Every wrapper whose
594/// C function may run ARPACK takes it: those of `crate::linalg`, the spectral
595/// centralities and ARPACK PageRank of `centrality.rs`, and the leading
596/// eigenvector community detection of `community.rs`.
597pub(crate) struct ArpackGuard {
598    _not_send: std::marker::PhantomData<*const ()>,
599}
600
601impl ArpackGuard {
602    /// Enters an ARPACK run, or fails if one is already running on this thread.
603    pub(crate) fn enter() -> Result<Self> {
604        if ARPACK_ACTIVE.with(|a| a.replace(true)) {
605            return Err(Error::new(
606                ErrorKind::Failure,
607                "ARPACK-based functions cannot be nested inside ARPACK callbacks or handlers",
608            ));
609        }
610        Ok(Self {
611            _not_send: std::marker::PhantomData,
612        })
613    }
614}
615
616impl Drop for ArpackGuard {
617    fn drop(&mut self) {
618        ARPACK_ACTIVE.with(|a| a.set(false));
619    }
620}
621
622/// The C callback handed to ARPACK: calls the Rust closure `F` with
623/// `x = from` and `y = to` (zeroed first), catching panics.
624///
625/// The closure runs in a fresh level of igraph's "finally" stack
626/// (`IGRAPH_FINALLY_ENTER` / `IGRAPH_FINALLY_EXIT`): the running solver keeps
627/// its work vectors on that stack, and igraph's error handler frees the
628/// current level when a call fails. Without the new level, a failing igraph
629/// call made by the closure (whose `Err` the closure may well ignore) would
630/// free the workspace ARPACK is still writing into.
631pub(crate) unsafe extern "C" fn matvec_trampoline<F: FnMut(&[f64], &mut [f64])>(
632    to: *mut igraph_real_t,
633    from: *const igraph_real_t,
634    n: c_int,
635    extra: *mut c_void,
636) -> igraph_error_t {
637    // SAFETY: plain bookkeeping on igraph's thread-local finally stack; the
638    // matching EXIT runs below, as `catch_panic` never unwinds.
639    unsafe { IGRAPH_FINALLY_ENTER() };
640    let code = catch_panic(|| {
641        let f = unsafe { &mut *(extra as *mut F) };
642        let n = n.max(0) as usize;
643        let (x, y) = unsafe {
644            (
645                std::slice::from_raw_parts(from, n),
646                std::slice::from_raw_parts_mut(to, n),
647            )
648        };
649        y.fill(0.0);
650        f(x, y);
651        // Non-finite values would make ARPACK's LAPACK routines abort the
652        // process: stop with an error instead.
653        if y.iter().all(|v| v.is_finite()) {
654            igraph_error_type_t_IGRAPH_SUCCESS
655        } else {
656            igraph_error_type_t_IGRAPH_EINVAL
657        }
658    });
659    unsafe { IGRAPH_FINALLY_EXIT() };
660    code
661}
662
663/// Runs `call` with a wrapped closure that remembers whether `matvec`
664/// produced non-finite values, to report that as a proper error.
665pub(crate) fn with_finite_guard<F: FnMut(&[f64], &mut [f64]), R>(
666    mut matvec: F,
667    call: impl FnOnce(&mut dyn FnMut(&[f64], &mut [f64])) -> Result<R>,
668) -> Result<R> {
669    let mut nonfinite = false;
670    let res = call(&mut |x: &[f64], y: &mut [f64]| {
671        matvec(x, y);
672        if !y.iter().all(|v| v.is_finite()) {
673            nonfinite = true;
674        }
675    });
676    if nonfinite {
677        return Err(Error::invalid(
678            "the matrix-vector product returned NaN or infinite values",
679        ));
680    }
681    res
682}
683
684/// Eigenvalues and eigenvectors of a **symmetric** linear operator given as
685/// a Rust closure, with ARPACK's implicitly restarted Lanczos method
686/// (`igraph_arpack_rssolve`).
687///
688/// `matvec(x, y)` must store the product `A x` into `y` (which is zeroed
689/// before each call, so accumulating into it is fine); both slices have
690/// length `n`. The matrix is never formed: this is the method of choice for
691/// large sparse or structured matrices. In
692/// [`ArpackMode::ShiftInvert`] the closure must compute `(A - sigma I)^-1 x`
693/// instead. A panic in the closure stops ARPACK and is resumed by this
694/// function; a product containing NaN or infinite values stops it with an
695/// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue) error.
696///
697/// Time complexity: depends on the matrix-vector product; usually a few
698/// iterations suffice, so with an O(n) product the eigenvalues are found in
699/// about O(n) time.
700///
701/// In regular mode, 1 × 1 and 2 × 2 problems are solved in closed form by
702/// igraph without running ARPACK. For 2 × 2 problems igraph 1.0.0 and 1.0.1
703/// confuse the magnitude-based choices with the algebraic ones and return
704/// unnormalized eigenvectors; this wrapper computes both eigenpairs and
705/// selects and normalizes the requested ones itself.
706///
707/// # Singular operators: the start vector is multiplied first
708///
709/// ARPACK does not use the start vector `v0` itself as the first Krylov
710/// vector: it first applies the operator and starts from `A v0` (to force the
711/// start vector into the range of the operator, see `dgetv0`). For a
712/// **singular** symmetric operator the range is orthogonal to the null space,
713/// so the component of `v0` along the null vectors is wiped out: when `A v0`
714/// has no null component *exactly* (e.g. integer matrices and start vectors,
715/// as for graph Laplacians), the eigenvalue `0` can be invisible to the
716/// iteration, and ARPACK then silently returns other eigenvalues instead. A start vector
717/// lying in the null space fails outright with
718/// [`ArpackError::ZeroStart`]. The random default start vector usually
719/// recovers the null space through rounding errors, but there is no
720/// guarantee.
721///
722/// The robust fix is a shift: solve for `A + s I` with some `s > 0` that makes
723/// the operator non-singular (for a positive semidefinite `A` such as a
724/// Laplacian any `s > 0` works, and "smallest magnitude" becomes "smallest
725/// algebraic"), then subtract `s` from the eigenvalues; the eigenvectors are
726/// unchanged. See the second example below.
727///
728/// # Nesting
729///
730/// igraph's ARPACK keeps the state of the running iteration in thread-local
731/// statics, so it is not re-entrant: `matvec` (and any interruption or
732/// progress handler, which igraph calls from inside the ARPACK loop) must not
733/// start another ARPACK computation on the same thread. The ARPACK-based
734/// functions of this module (this function, [`arpack_rnsolve`], the
735/// [`SparseMat`](super::SparseMat) ARPACK solvers, the ARPACK paths of
736/// [`eigen_matrix_symmetric`](super::eigen_matrix_symmetric) and
737/// [`eigen_symmetric_fn`](super::eigen_symmetric_fn),
738/// [`Graph::eigen_adjacency`](crate::Graph::eigen_adjacency) and the spectral
739/// embeddings) check this: started while another one of them runs on the
740/// thread, they fail with an
741/// [`ErrorKind::Failure`](crate::ErrorKind::Failure) error instead of
742/// corrupting the running solver. So do the ARPACK-based functions of other
743/// modules: [`Graph::eigenvector_centrality`](crate::Graph::eigenvector_centrality),
744/// [`Graph::hub_and_authority_scores`](crate::Graph::hub_and_authority_scores),
745/// [`Graph::centralization_eigenvector_centrality`](crate::Graph::centralization_eigenvector_centrality),
746/// PageRank with [`PageRankAlgo::Arpack`](crate::centrality::PageRankAlgo::Arpack)
747/// and [`Graph::community_leading_eigenvector`](crate::Graph::community_leading_eigenvector).
748/// Calling other igraph functions from `matvec` is fine, even
749/// failing ones: the closure runs in its own level of igraph's cleanup stack.
750///
751/// Binds [`igraph_arpack_rssolve`](https://igraph.org/c/html/latest/igraph-Linalg.html#igraph_arpack_rssolve).
752///
753/// See also [`SparseMat::arpack_rssolve`](super::SparseMat::arpack_rssolve)
754/// for a stored sparse matrix, [`eigen_symmetric_fn`](super::eigen_symmetric_fn)
755/// for a front-end that can also use LAPACK, and the graph-level spectral
756/// routines built on ARPACK, e.g.
757/// [`Graph::eigenvector_centrality`](crate::Graph::eigenvector_centrality)
758/// and [`Graph::eigen_adjacency`](crate::Graph::eigen_adjacency).
759///
760/// # Errors
761/// Invalid options (see [`ArpackOptions`]) give
762/// [`ErrorKind::InvalidValue`](crate::ErrorKind::InvalidValue); ARPACK
763/// failures give [`ErrorKind::Arpack`](crate::ErrorKind::Arpack), see
764/// [`ArpackError::from_error`] for the ARPACK error condition.
765///
766/// # Examples
767///
768/// The cycle graph `C_n` has adjacency eigenvalues `2 cos(2 pi k / n)`: the
769/// largest is 2, with the constant eigenvector.
770///
771/// ```
772/// use igraph::linalg::{arpack_rssolve, ArpackOptions, ArpackWhich};
773/// let n = 12;
774/// let cycle = |x: &[f64], y: &mut [f64]| {
775///     for i in 0..n {
776///         y[i] = x[(i + 1) % n] + x[(i + n - 1) % n];
777///     }
778/// };
779/// let opts = ArpackOptions::default().with_nev(1).with_which(ArpackWhich::LargestAlgebraic);
780/// let res = arpack_rssolve(n, cycle, &opts, None).unwrap();
781/// assert!((res.values[0] - 2.0).abs() < 1e-10);
782/// let v = res.vectors.column(0);
783/// assert!(v.iter().all(|x| (x.abs() - 1.0 / (n as f64).sqrt()).abs() < 1e-8));
784/// ```
785///
786/// The Laplacian of the path on 10 vertices is singular (eigenvalue 0, with
787/// the constant eigenvector). The ramp `v0 = (0, 1, ..., 9)` has a constant
788/// component, but ARPACK iterates from `L v0 = e_9 - e_0`, which has exactly
789/// none (and, by the mirror symmetry of the path, neither have the following
790/// Krylov vectors): the eigenvalue 0 is missed altogether. Shifting to
791/// `L + I` keeps the constant component of `v0` and finds it:
792///
793/// ```
794/// use igraph::linalg::{arpack_rssolve, ArpackOptions, ArpackWhich};
795/// use std::f64::consts::PI;
796/// let n = 10;
797/// // y = (L + shift I) x for the path Laplacian L.
798/// let laplacian = move |shift: f64| {
799///     move |x: &[f64], y: &mut [f64]| {
800///         for i in 0..n {
801///             let degree = if i == 0 || i == n - 1 { 1.0 } else { 2.0 };
802///             y[i] = (degree + shift) * x[i];
803///             if i > 0 {
804///                 y[i] -= x[i - 1];
805///             }
806///             if i + 1 < n {
807///                 y[i] -= x[i + 1];
808///             }
809///         }
810///     }
811/// };
812/// let ramp: Vec<f64> = (0..n).map(|i| i as f64).collect();
813/// let lambda1 = 2.0 - 2.0 * (PI / n as f64).cos(); // the smallest non-zero eigenvalue
814///
815/// let opts = ArpackOptions::default()
816///     .with_nev(2)
817///     .with_which(ArpackWhich::SmallestMagnitude)
818///     .with_start(ramp);
819/// let plain = arpack_rssolve(n, laplacian(0.0), &opts, None).unwrap();
820/// assert!(plain.values.iter().all(|v| v.abs() > 0.09)); // 0 is missed
821/// assert!(plain.values.iter().any(|v| (v - lambda1).abs() < 1e-8));
822///
823/// let shifted_opts = opts.with_which(ArpackWhich::SmallestAlgebraic);
824/// let shifted = arpack_rssolve(n, laplacian(1.0), &shifted_opts, None).unwrap();
825/// let values: Vec<f64> = shifted.values.iter().map(|v| v - 1.0).collect();
826/// assert!(values[0].abs() < 1e-10);
827/// assert!((values[1] - lambda1).abs() < 1e-8);
828/// ```
829pub fn arpack_rssolve<F: FnMut(&[f64], &mut [f64])>(
830    n: usize,
831    matvec: F,
832    options: &ArpackOptions,
833    storage: Option<&mut ArpackStorage>,
834) -> Result<ArpackSymmetricResult> {
835    let mut raw = options.to_raw(n, true)?;
836    let shortcut = uses_2x2_shortcut(n, options);
837    if shortcut {
838        raw.nev = 2;
839    }
840    let storage_ptr = storage.map_or(ptr::null_mut(), |s| {
841        &mut s.raw as *mut igraph_arpack_storage_t
842    });
843    let mut values = Vector::new();
844    let mut vectors = options.start_matrix(n)?;
845    let _arpack = ArpackGuard::enter()?;
846    with_finite_guard(matvec, |mut f| {
847        igraph_call!(igraph_arpack_rssolve(
848            Some(matvec_trampoline::<&mut dyn FnMut(&[f64], &mut [f64])>),
849            &mut f as *mut &mut dyn FnMut(&[f64], &mut [f64]) as *mut c_void,
850            &mut raw,
851            storage_ptr,
852            &mut values,
853            &mut vectors
854        ))
855    })?;
856    let res = ArpackSymmetricResult::from_raw(values, vectors, &raw);
857    if shortcut {
858        res.select_2x2(options.which, options.nev)
859    } else {
860        Ok(res)
861    }
862}
863
864/// Eigenvalues and eigenvectors of a general (**non-symmetric**) linear
865/// operator given as a Rust closure, with ARPACK's implicitly restarted
866/// Arnoldi method (`igraph_arpack_rnsolve`).
867///
868/// `matvec(x, y)` stores `A x` into `y`, as in [`arpack_rssolve`]. The
869/// eigenvalues may be complex: they are returned as [`Complex`] numbers,
870/// with complex conjugate pairs next to each other, and the eigenvectors are
871/// unpacked into complex vectors. Note that ARPACK requires `nev <= n - 2`
872/// here; in regular mode 1 × 1 and 2 × 2 problems are solved exactly by
873/// igraph (in closed form, without ARPACK).
874///
875/// As explained for [`arpack_rssolve`], ARPACK starts from `A v0` rather
876/// than from the start vector `v0`, so eigenvectors of a singular operator
877/// with eigenvalue 0 may be missed (shift the operator to `A + s I` and
878/// subtract `s` afterwards), and ARPACK computations cannot be nested inside
879/// `matvec` or inside handlers (a nested call fails with
880/// [`ErrorKind::Failure`](crate::ErrorKind::Failure)).
881///
882/// Binds [`igraph_arpack_rnsolve`](https://igraph.org/c/html/latest/igraph-Linalg.html#igraph_arpack_rnsolve).
883///
884/// See also [`SparseMat::arpack_rnsolve`](super::SparseMat::arpack_rnsolve),
885/// [`eigen_matrix`](super::eigen_matrix) for dense matrices, and
886/// [`Graph::pagerank`](crate::Graph::pagerank) /
887/// [`Graph::hub_and_authority_scores`](crate::Graph::hub_and_authority_scores),
888/// the classic non-symmetric eigenproblems on graphs.
889///
890/// # Errors
891/// As [`arpack_rssolve`]: invalid options (see [`ArpackOptions`]; ARPACK
892/// rejects `nev > n - 2` for `n > 2`), a non-finite product, or an ARPACK
893/// failure ([`ErrorKind::Arpack`](crate::ErrorKind::Arpack), see
894/// [`ArpackError::from_error`]).
895///
896/// # Examples
897///
898/// A directed cycle is a permutation matrix: its eigenvalues are the `n`-th
899/// roots of unity, all of magnitude 1.
900///
901/// ```
902/// use igraph::linalg::{arpack_rnsolve, ArpackOptions, ArpackWhich};
903/// let n = 8;
904/// let shift = |x: &[f64], y: &mut [f64]| {
905///     for i in 0..n {
906///         y[i] = x[(i + 1) % n];
907///     }
908/// };
909/// let opts = ArpackOptions::default().with_nev(3).with_which(ArpackWhich::LargestReal);
910/// let res = arpack_rnsolve(n, shift, &opts, None).unwrap();
911/// assert!((res.values[0].re() - 1.0).abs() < 1e-8 && res.values[0].im().abs() < 1e-8);
912/// for v in &res.values {
913///     assert!(((v.re() * v.re() + v.im() * v.im()).sqrt() - 1.0).abs() < 1e-8);
914/// }
915/// ```
916pub fn arpack_rnsolve<F: FnMut(&[f64], &mut [f64])>(
917    n: usize,
918    matvec: F,
919    options: &ArpackOptions,
920    storage: Option<&mut ArpackStorage>,
921) -> Result<ArpackNonSymmetricResult> {
922    let mut raw = options.to_raw(n, false)?;
923    if let Some(s) = &storage
924        && s.symmetric
925    {
926        return Err(Error::invalid(
927            "the non-symmetric ARPACK solver needs a non-symmetric ArpackStorage",
928        ));
929    }
930    let storage_ptr = storage.map_or(ptr::null_mut(), |s| {
931        &mut s.raw as *mut igraph_arpack_storage_t
932    });
933    let mut values = Matrix::new();
934    let mut vectors = options.start_matrix(n)?;
935    let _arpack = ArpackGuard::enter()?;
936    with_finite_guard(matvec, |mut f| {
937        igraph_call!(igraph_arpack_rnsolve(
938            Some(matvec_trampoline::<&mut dyn FnMut(&[f64], &mut [f64])>),
939            &mut f as *mut &mut dyn FnMut(&[f64], &mut [f64]) as *mut c_void,
940            &mut raw,
941            storage_ptr,
942            &mut values,
943            &mut vectors
944        ))
945    })?;
946    ArpackNonSymmetricResult::from_raw(values, vectors, &raw, options.nev)
947}
948
949/// Rewrites the packed output of the non-symmetric ARPACK solver into a
950/// regular form (`igraph_arpack_unpack_complex`).
951///
952/// `values` is a `k` × 2 matrix of real and imaginary parts, `vectors` the
953/// packed eigenvector matrix (one column per real eigenvalue, two columns —
954/// real and imaginary part — per complex conjugate pair). The result keeps
955/// the first `nev` eigenvalues as an `nev` × 2 matrix, and returns an
956/// `n` × `2 nev` eigenvector matrix where eigenvector `k` occupies columns
957/// `2k` (real part) and `2k + 1` (imaginary part).
958///
959/// [`arpack_rnsolve`] already returns unpacked [`Complex`] vectors; this is
960/// useful to post-process raw data.
961///
962/// Binds [`igraph_arpack_unpack_complex`](https://igraph.org/c/html/latest/igraph-Linalg.html#igraph_arpack_unpack_complex).
963///
964/// # Errors
965/// If `values` does not have two columns, `nev` exceeds its rows, the packed
966/// vectors are too few, or a complex eigenvalue is not followed by its
967/// conjugate.
968pub fn arpack_unpack_complex(
969    vectors: &Matrix,
970    values: &Matrix,
971    nev: usize,
972) -> Result<(Matrix, Matrix)> {
973    if values.ncol() != 2 {
974        return Err(Error::invalid(
975            "the eigenvalue matrix must have two columns",
976        ));
977    }
978    if nev > values.nrow() {
979        return Err(Error::invalid(
980            "nev is larger than the number of eigenvalues",
981        ));
982    }
983    // Mirror igraph's walk to make sure it stays within the vector matrix.
984    let (mut i, mut col) = (0, 0);
985    while i < nev && col < vectors.ncol() {
986        if values[(i, 1)] == 0.0 {
987            col += 1;
988        } else {
989            if col + 1 >= vectors.ncol() {
990                return Err(Error::invalid(
991                    "too few columns in the packed eigenvector matrix",
992                ));
993            }
994            i += 1;
995            col += 2;
996        }
997        i += 1;
998    }
999    let mut vec_out = vectors.clone();
1000    let mut val_out = values.clone();
1001    igraph_call!(igraph_arpack_unpack_complex(
1002        &mut vec_out,
1003        &mut val_out,
1004        nev as igraph_int_t
1005    ))?;
1006    Ok((vec_out, val_out))
1007}
1008
1009crate::ffi_enum! {
1010    /// Error conditions reported by ARPACK (`igraph_arpack_error_t`), see
1011    /// [`ArpackError::from_error`] and [`arpack_error_to_string`].
1012    pub enum ArpackError: igraph_arpack_error_t {
1013        /// No error.
1014        NoError = igraph_arpack_error_t_IGRAPH_ARPACK_NO_ERROR,
1015        /// Matrix-vector product failed (not used any more).
1016        Prod = igraph_arpack_error_t_IGRAPH_ARPACK_PROD,
1017        /// N must be positive.
1018        NPos = igraph_arpack_error_t_IGRAPH_ARPACK_NPOS,
1019        /// NEV must be positive.
1020        NevNPos = igraph_arpack_error_t_IGRAPH_ARPACK_NEVNPOS,
1021        /// NCV must be bigger.
1022        NcvSmall = igraph_arpack_error_t_IGRAPH_ARPACK_NCVSMALL,
1023        /// Maximum number of iterations should be positive.
1024        NonPosI = igraph_arpack_error_t_IGRAPH_ARPACK_NONPOSI,
1025        /// Invalid WHICH parameter.
1026        WhichInv = igraph_arpack_error_t_IGRAPH_ARPACK_WHICHINV,
1027        /// Invalid BMAT parameter.
1028        BmatInv = igraph_arpack_error_t_IGRAPH_ARPACK_BMATINV,
1029        /// WORKL is too small.
1030        WorklSmall = igraph_arpack_error_t_IGRAPH_ARPACK_WORKLSMALL,
1031        /// LAPACK error in tridiagonal eigenvalue calculation.
1032        TriDErr = igraph_arpack_error_t_IGRAPH_ARPACK_TRIDERR,
1033        /// Starting vector is zero.
1034        ZeroStart = igraph_arpack_error_t_IGRAPH_ARPACK_ZEROSTART,
1035        /// MODE is invalid.
1036        ModeInv = igraph_arpack_error_t_IGRAPH_ARPACK_MODEINV,
1037        /// MODE and BMAT are not compatible.
1038        ModeBmat = igraph_arpack_error_t_IGRAPH_ARPACK_MODEBMAT,
1039        /// ISHIFT must be 0 or 1.
1040        IShift = igraph_arpack_error_t_IGRAPH_ARPACK_ISHIFT,
1041        /// NEV and WHICH='BE' are incompatible.
1042        NevBe = igraph_arpack_error_t_IGRAPH_ARPACK_NEVBE,
1043        /// Could not build an Arnoldi factorization.
1044        NoFact = igraph_arpack_error_t_IGRAPH_ARPACK_NOFACT,
1045        /// No eigenvalues to sufficient accuracy.
1046        Failed = igraph_arpack_error_t_IGRAPH_ARPACK_FAILED,
1047        /// HOWMNY is invalid.
1048        Howmny = igraph_arpack_error_t_IGRAPH_ARPACK_HOWMNY,
1049        /// HOWMNY='S' is not implemented.
1050        HowmnyS = igraph_arpack_error_t_IGRAPH_ARPACK_HOWMNYS,
1051        /// Different number of converged Ritz values.
1052        EvDiff = igraph_arpack_error_t_IGRAPH_ARPACK_EVDIFF,
1053        /// Error from calculation of a real Schur form.
1054        Shur = igraph_arpack_error_t_IGRAPH_ARPACK_SHUR,
1055        /// LAPACK (dtrevc) error for calculating eigenvectors.
1056        Lapack = igraph_arpack_error_t_IGRAPH_ARPACK_LAPACK,
1057        /// Unknown ARPACK error.
1058        Unknown = igraph_arpack_error_t_IGRAPH_ARPACK_UNKNOWN,
1059        /// Maximum number of iterations reached.
1060        MaxIt = igraph_arpack_error_t_IGRAPH_ARPACK_MAXIT,
1061        /// No shifts could be applied during a cycle of the implicitly
1062        /// restarted Arnoldi iteration.
1063        NoShift = igraph_arpack_error_t_IGRAPH_ARPACK_NOSHIFT,
1064        /// The Schur form could not be reordered.
1065        Reorder = igraph_arpack_error_t_IGRAPH_ARPACK_REORDER,
1066    }
1067}
1068
1069impl ArpackError {
1070    const ALL: [Self; 26] = [
1071        Self::NoError,
1072        Self::Prod,
1073        Self::NPos,
1074        Self::NevNPos,
1075        Self::NcvSmall,
1076        Self::NonPosI,
1077        Self::WhichInv,
1078        Self::BmatInv,
1079        Self::WorklSmall,
1080        Self::TriDErr,
1081        Self::ZeroStart,
1082        Self::ModeInv,
1083        Self::ModeBmat,
1084        Self::IShift,
1085        Self::NevBe,
1086        Self::NoFact,
1087        Self::Failed,
1088        Self::Howmny,
1089        Self::HowmnyS,
1090        Self::EvDiff,
1091        Self::Shur,
1092        Self::Lapack,
1093        Self::Unknown,
1094        Self::MaxIt,
1095        Self::NoShift,
1096        Self::Reorder,
1097    ];
1098
1099    /// The ARPACK error condition behind an
1100    /// [`ErrorKind::Arpack`](crate::ErrorKind::Arpack) error, recognized from
1101    /// its message (igraph reports ARPACK failures with the text of
1102    /// [`arpack_error_to_string`]); `None` for other errors.
1103    ///
1104    /// Unlike [`arpack_last_error`] this is thread-safe: it only looks at the
1105    /// error value returned by the failed call.
1106    ///
1107    /// ```
1108    /// use igraph::{linalg::*, ErrorKind};
1109    /// // The Laplacian of a path, started from its eigenvector (1, ..., 1) of
1110    /// // eigenvalue 0: ARPACK's first Krylov vector is zero.
1111    /// let n = 6;
1112    /// let path = move |x: &[f64], y: &mut [f64]| {
1113    ///     for i in 0..n {
1114    ///         let deg = if i == 0 || i == n - 1 { 1.0 } else { 2.0 };
1115    ///         y[i] = deg * x[i];
1116    ///         if i > 0 {
1117    ///             y[i] -= x[i - 1];
1118    ///         }
1119    ///         if i + 1 < n {
1120    ///             y[i] -= x[i + 1];
1121    ///         }
1122    ///     }
1123    /// };
1124    /// let opts = ArpackOptions::default()
1125    ///     .with_start(vec![1.0; n])
1126    ///     .with_which(ArpackWhich::SmallestMagnitude);
1127    /// let err = arpack_rssolve(n, path, &opts, None).unwrap_err();
1128    /// assert_eq!(err.kind(), ErrorKind::Arpack);
1129    /// assert_eq!(ArpackError::from_error(&err), Some(ArpackError::ZeroStart));
1130    /// ```
1131    pub fn from_error(error: &Error) -> Option<Self> {
1132        if error.kind() != crate::ErrorKind::Arpack {
1133            return None;
1134        }
1135        Self::ALL
1136            .into_iter()
1137            .find(|&e| arpack_error_to_string(e) == error.message())
1138    }
1139}
1140
1141impl fmt::Display for ArpackError {
1142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1143        f.write_str(&arpack_error_to_string(*self))
1144    }
1145}
1146
1147/// Human readable description of an ARPACK error code
1148/// (`igraph_arpack_error_to_string`).
1149///
1150/// Binds [`igraph_arpack_error_to_string`](https://igraph.org/c/html/latest/igraph-Linalg.html#igraph_arpack_error_to_string).
1151///
1152/// ```
1153/// use igraph::linalg::{arpack_error_to_string, ArpackError};
1154/// assert_eq!(arpack_error_to_string(ArpackError::NoError), "No error");
1155/// assert!(!arpack_error_to_string(ArpackError::NcvSmall).is_empty());
1156/// ```
1157pub fn arpack_error_to_string(error: ArpackError) -> String {
1158    let s = unsafe { igraph_arpack_error_to_string(error.into()) };
1159    if s.is_null() {
1160        String::new()
1161    } else {
1162        unsafe { CStr::from_ptr(s) }.to_string_lossy().into_owned()
1163    }
1164}
1165
1166/// The error code of the last ARPACK failure in the process
1167/// (`igraph_arpack_get_last_error`).
1168///
1169/// Prefer [`ArpackError::from_error`], which reads the same information
1170/// from the returned error in a thread-safe way.
1171///
1172/// Binds [`igraph_arpack_get_last_error`](https://igraph.org/c/html/latest/igraph-Linalg.html#igraph_arpack_get_last_error).
1173///
1174/// # Safety
1175///
1176/// In igraph 1.0.0 and 1.0.1 the last ARPACK error is kept in a plain,
1177/// process-wide C variable (not thread-local, unlike the rest of igraph's
1178/// state), written by every failing ARPACK run. The caller must make sure
1179/// that no other thread runs an ARPACK-based igraph computation
1180/// concurrently (this includes the eigensolvers and spectral embeddings of
1181/// this module and ARPACK-based functions of other modules, such as
1182/// eigenvector centrality); otherwise the read is a data race.
1183pub unsafe fn arpack_last_error() -> ArpackError {
1184    let raw = unsafe { igraph_arpack_get_last_error() };
1185    ArpackError::try_from(raw).unwrap_or(ArpackError::Unknown)
1186}