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}