Spectra 1.2.0
Header-only C++ Library for Large Scale Eigenvalue Problems
GenEigsBase.h
1// Copyright (C) 2018-2025 Yixuan Qiu <yixuan.qiu@cos.name>
2//
3// This Source Code Form is subject to the terms of the Mozilla
4// Public License v. 2.0. If a copy of the MPL was not distributed
5// with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
6
7#ifndef SPECTRA_GEN_EIGS_BASE_H
8#define SPECTRA_GEN_EIGS_BASE_H
9
10#include <Eigen/Core>
11#include <vector> // std::vector
12#include <cmath> // std::abs, std::pow, std::sqrt
13#include <algorithm> // std::min, std::copy
14#include <complex> // std::complex, std::norm, std::abs
15#include <stdexcept> // std::invalid_argument
16
17#include "Util/Version.h"
18#include "Util/TypeTraits.h"
19#include "Util/SelectionRule.h"
20#include "Util/CompInfo.h"
21#include "Util/SimpleRandom.h"
22#include "MatOp/internal/ArnoldiOp.h"
23#include "LinAlg/UpperHessenbergQR.h"
24#include "LinAlg/DoubleShiftQR.h"
25#include "LinAlg/UpperHessenbergEigen.h"
26#include "LinAlg/Arnoldi.h"
27
28namespace Spectra {
29
30// Helper class to restart Arnoldi factorization
31//
32// Given the current upper Hessenberg matrix H and a number of
33// shifts mu[1], ..., mu[n]:
34//
35// 1. Compute QR decomposition H - mu[i] * I = Qi * Ri
36// 2. Update Q <- Q * Qi
37// 3. Update H <- Qi^H * H * Qi
38//
39// The updated H has the same eigenvalues as the original one,
40// but typically the new H is closer to a diagonal matrix
41//
42// Default implementation for real type
43template <typename Scalar, typename ArnoldiFac>
45{
46private:
47 using Index = Eigen::Index;
48 using Complex = std::complex<Scalar>;
49 using Matrix = Eigen::Matrix<Scalar, Eigen::Dynamic, Eigen::Dynamic>;
50 using ComplexVector = Eigen::Matrix<Complex, Eigen::Dynamic, 1>;
51
52 // Real Ritz values calculated from UpperHessenbergEigen have exact zero imaginary part
53 // Complex Ritz values have exact conjugate pairs
54 // So we use exact tests here
55 static bool is_complex(const Complex& v) { return v.imag() != Scalar(0); }
56 static bool is_conj(const Complex& v1, const Complex& v2) { return v1 == Eigen::numext::conj(v2); }
57
58public:
59 // Shifts are ritz_val[k], ritz_val[k+1], ...
60 static void run(const ComplexVector& ritz_val, Index k, ArnoldiFac& fac, Matrix& Q)
61 {
62 using std::norm;
63
64 const Index ncv = ritz_val.size();
65 DoubleShiftQR<Scalar> decomp_ds(ncv);
66 UpperHessenbergQR<Scalar> decomp_hb(ncv);
67
68 for (Index i = k; i < ncv; i++)
69 {
70 if (is_complex(ritz_val[i]) && is_conj(ritz_val[i], ritz_val[i + 1]))
71 {
72 // For real-valued H and two conjugate shifts mu, conj(mu),
73 // H - mu * I = Q * R may be a complex-valued QR decomposition,
74 // which is costly in computation
75 //
76 // Instead, we can apply two conjugate shifts simultaneously, i.e.,
77 // (H - mu * I) * (H - conj(mu) * I) = Q * R, and then update
78 // H <- Q'HQ
79 //
80 // This is called a double shift QR decomposition
81 // (H - mu * I) * (H - conj(mu) * I) = H^2 - 2 * Re(mu) * H + |mu|^2 * I
82 const Scalar s = Scalar(2) * ritz_val[i].real();
83 const Scalar t = norm(ritz_val[i]);
84 decomp_ds.compute(fac.matrix_H(), s, t);
85
86 // Q <- Q * Qi
87 decomp_ds.apply_YQ(Q);
88 // H <- Qi' * H * Qi
89 // Matrix Q = Matrix::Identity(ncv, ncv);
90 // decomp_ds.apply_YQ(Q);
91 // fac_H = Q.transpose() * fac_H * Q;
92 fac.compress_H(decomp_ds);
93
94 i++;
95 }
96 else
97 {
98 // QR decomposition of H - mu[i] * I, mu[i] is real
99 decomp_hb.compute(fac.matrix_H(), ritz_val[i].real());
100
101 // Q <- Q * Qi
102 decomp_hb.apply_YQ(Q);
103 // H <- Qi' * H * Qi = Ri * Qi + mu[i] * I
104 fac.compress_H(decomp_hb);
105 }
106 }
107 }
108};
109
110// Partial specialization for complex-valued matrices
111template <typename RealScalar, typename ArnoldiFac>
112class RestartArnoldi<std::complex<RealScalar>, ArnoldiFac>
113{
114private:
115 using Index = Eigen::Index;
116 using Scalar = std::complex<RealScalar>;
117 using Complex = Scalar;
118 using Matrix = Eigen::Matrix<Scalar, Eigen::Dynamic, Eigen::Dynamic>;
119 using ComplexVector = Eigen::Matrix<Complex, Eigen::Dynamic, 1>;
120
121public:
122 static void run(const ComplexVector& ritz_val, Index k, ArnoldiFac& fac, Matrix& Q)
123 {
124 const Index ncv = ritz_val.size();
125 // This is the complex-valued QR decomposition
126 UpperHessenbergQR<Scalar> decomp_hb(ncv);
127
128 // For complex-valued H, simply apply complex shifts mu[i] one by one
129 for (Index i = k; i < ncv; i++)
130 {
131 // QR decomposition of H - mu[i] * I
132 decomp_hb.compute(fac.matrix_H(), ritz_val[i]);
133
134 // Q <- Q * Qi
135 decomp_hb.apply_YQ(Q);
136 // H <- Qi^H * H * Qi = Ri * Qi + mu[i] * I
137 fac.compress_H(decomp_hb);
138 }
139 }
140};
141
149template <typename OpType, typename BOpType>
151{
152private:
153 // Scalar is the type of the matrix element
154 // Can be real or complex
155 using Scalar = typename OpType::Scalar;
156 // The real part type of the matrix element, e.g.,
157 // Scalar = double => RealScalar = double
158 // Scalar = std::complex<double> => RealScalar = double
159 using RealScalar = typename Eigen::NumTraits<Scalar>::Real;
160 // The eigenvalues are known to be complex numbers
161 using Complex = std::complex<RealScalar>;
162 using Index = Eigen::Index;
163 using Matrix = Eigen::Matrix<Scalar, Eigen::Dynamic, Eigen::Dynamic>;
164 using Vector = Eigen::Matrix<Scalar, Eigen::Dynamic, 1>;
165 using ComplexMatrix = Eigen::Matrix<Complex, Eigen::Dynamic, Eigen::Dynamic>;
166 using ComplexVector = Eigen::Matrix<Complex, Eigen::Dynamic, 1>;
167 using RealArray = Eigen::Array<RealScalar, Eigen::Dynamic, 1>;
168 using BoolArray = Eigen::Array<bool, Eigen::Dynamic, 1>;
169 using MapMat = Eigen::Map<Matrix>;
170 using MapVec = Eigen::Map<Vector>;
171 using MapConstVec = Eigen::Map<const Vector>;
172
173 using ArnoldiOpType = ArnoldiOp<OpType, BOpType>;
174 using ArnoldiFac = Arnoldi<ArnoldiOpType>;
175
176protected:
177 // clang-format off
178 OpType& m_op; // object to conduct matrix operation,
179 // e.g. matrix-vector product
180 const Index m_n; // dimension of matrix A
181 const Index m_nev; // number of eigenvalues requested
182 const Index m_ncv; // dimension of Krylov subspace in the Arnoldi method
183 Index m_nmatop; // number of matrix operations called
184 Index m_niter; // number of restarting iterations
185
186 ArnoldiFac m_fac; // Arnoldi factorization
187
188 ComplexVector m_ritz_val; // Ritz values
189 ComplexMatrix m_ritz_vec; // Ritz vectors
190 ComplexVector m_ritz_est; // last row of m_ritz_vec, also called the Ritz estimates
191
192private:
193 BoolArray m_ritz_conv; // indicator of the convergence of Ritz values
194 CompInfo m_info; // status of the computation
195 // clang-format on
196
197 // Real Ritz values calculated from UpperHessenbergEigen have exact zero imaginary part
198 // Complex Ritz values have exact conjugate pairs
199 // So we use exact tests here
200 static bool is_complex(const Complex& v) { return v.imag() != Scalar(0); }
201 static bool is_conj(const Complex& v1, const Complex& v2) { return v1 == Eigen::numext::conj(v2); }
202
203 // Implicitly restarted Arnoldi factorization
204 void restart(Index k, SortRule selection)
205 {
206 using std::norm;
207
208 if (k >= m_ncv)
209 return;
210
211 // Use Q to collect orthogonal transformations
212 Matrix Q = Matrix::Identity(m_ncv, m_ncv);
213 // Apply shifts and update H and Q
214 RestartArnoldi<Scalar, ArnoldiFac>::run(m_ritz_val, k, m_fac, Q);
215 // Apply orthogonal transformation to V, V <- V * Q
216 m_fac.compress_V(Q);
217 // It can be verified that the updated V and H admit a k-step
218 // Arnoldi factorization, and we expand it to m-step
219 m_fac.factorize_from(k, m_ncv, m_nmatop);
220 // Retrieve the new Ritz pairs
221 retrieve_ritzpair(selection);
222 }
223
224 // Calculates the number of converged Ritz values
225 Index num_converged(const RealScalar& tol)
226 {
227 using std::pow;
228
229 // The machine precision, ~= 1e-16 for the "double" type
230 const RealScalar eps = TypeTraits<RealScalar>::epsilon();
231 // std::pow() is not constexpr, so we do not declare eps23 to be constexpr
232 // But most compilers should be able to compute eps23 at compile time
233 const RealScalar eps23 = pow(eps, RealScalar(2) / 3);
234
235 // thresh = tol * max(eps23, abs(theta)), theta for Ritz value
236 RealArray thresh = tol * m_ritz_val.head(m_nev).array().abs().max(eps23);
237 RealArray resid = m_ritz_est.head(m_nev).array().abs() * m_fac.f_norm();
238 // Converged "wanted" Ritz values
239 m_ritz_conv = (resid < thresh);
240
241 return m_ritz_conv.count();
242 }
243
244 // Returns the adjusted nev for restarting
245 Index nev_adjusted(Index nconv)
246 {
247 using std::abs;
248
249 // A very small value, but 1.0 / near_0 does not overflow
250 // ~= 1e-307 for the "double" type
251 const RealScalar near_0 = TypeTraits<RealScalar>::min() * RealScalar(10);
252
253 Index nev_new = m_nev;
254 for (Index i = m_nev; i < m_ncv; i++)
255 if (abs(m_ritz_est[i]) < near_0)
256 nev_new++;
257
258 // Adjust nev_new, according to dnaup2.f line 660~674 in ARPACK
259 nev_new += (std::min)(nconv, (m_ncv - nev_new) / 2);
260 if (nev_new == 1 && m_ncv >= 6)
261 nev_new = m_ncv / 2;
262 else if (nev_new == 1 && m_ncv > 3)
263 nev_new = 2;
264
265 if (nev_new > m_ncv - 2)
266 nev_new = m_ncv - 2;
267
268 // Increase nev by one if ritz_val[nev - 1] and
269 // ritz_val[nev] are conjugate pairs
270 if (is_complex(m_ritz_val[nev_new - 1]) &&
271 is_conj(m_ritz_val[nev_new - 1], m_ritz_val[nev_new]))
272 {
273 nev_new++;
274 }
275
276 return nev_new;
277 }
278
279 // Retrieves and sorts Ritz values and Ritz vectors
280 void retrieve_ritzpair(SortRule selection)
281 {
282 UpperHessenbergEigen<Scalar> decomp(m_fac.matrix_H());
283 const ComplexVector& evals = decomp.eigenvalues();
284 ComplexMatrix evecs = decomp.eigenvectors();
285
286 // Sort Ritz values and put the wanted ones at the beginning
287 std::vector<Index> ind;
288 switch (selection)
289 {
291 {
292 SortEigenvalue<Complex, SortRule::LargestMagn> sorting(evals.data(), m_ncv);
293 sorting.swap(ind);
294 break;
295 }
297 {
298 SortEigenvalue<Complex, SortRule::LargestReal> sorting(evals.data(), m_ncv);
299 sorting.swap(ind);
300 break;
301 }
303 {
304 SortEigenvalue<Complex, SortRule::LargestImag> sorting(evals.data(), m_ncv);
305 sorting.swap(ind);
306 break;
307 }
309 {
310 SortEigenvalue<Complex, SortRule::SmallestMagn> sorting(evals.data(), m_ncv);
311 sorting.swap(ind);
312 break;
313 }
315 {
316 SortEigenvalue<Complex, SortRule::SmallestReal> sorting(evals.data(), m_ncv);
317 sorting.swap(ind);
318 break;
319 }
321 {
322 SortEigenvalue<Complex, SortRule::SmallestImag> sorting(evals.data(), m_ncv);
323 sorting.swap(ind);
324 break;
325 }
326 default:
327 throw std::invalid_argument("unsupported selection rule");
328 }
329
330 // Copy the Ritz values and vectors to m_ritz_val and m_ritz_vec, respectively
331 for (Index i = 0; i < m_ncv; i++)
332 {
333 m_ritz_val[i] = evals[ind[i]];
334 m_ritz_est[i] = evecs(m_ncv - 1, ind[i]);
335 }
336 for (Index i = 0; i < m_nev; i++)
337 {
338 m_ritz_vec.col(i).noalias() = evecs.col(ind[i]);
339 }
340 }
341
342protected:
343 // Sorts the first nev Ritz pairs in the specified order
344 // This is used to return the final results
345 virtual void sort_ritzpair(SortRule sort_rule)
346 {
347 std::vector<Index> ind;
348 switch (sort_rule)
349 {
351 {
352 SortEigenvalue<Complex, SortRule::LargestMagn> sorting(m_ritz_val.data(), m_nev);
353 sorting.swap(ind);
354 break;
355 }
357 {
358 SortEigenvalue<Complex, SortRule::LargestReal> sorting(m_ritz_val.data(), m_nev);
359 sorting.swap(ind);
360 break;
361 }
363 {
364 SortEigenvalue<Complex, SortRule::LargestImag> sorting(m_ritz_val.data(), m_nev);
365 sorting.swap(ind);
366 break;
367 }
369 {
370 SortEigenvalue<Complex, SortRule::SmallestMagn> sorting(m_ritz_val.data(), m_nev);
371 sorting.swap(ind);
372 break;
373 }
375 {
376 SortEigenvalue<Complex, SortRule::SmallestReal> sorting(m_ritz_val.data(), m_nev);
377 sorting.swap(ind);
378 break;
379 }
381 {
382 SortEigenvalue<Complex, SortRule::SmallestImag> sorting(m_ritz_val.data(), m_nev);
383 sorting.swap(ind);
384 break;
385 }
386 default:
387 throw std::invalid_argument("unsupported sorting rule");
388 }
389
390 ComplexVector new_ritz_val(m_ncv);
391 ComplexMatrix new_ritz_vec(m_ncv, m_nev);
392 BoolArray new_ritz_conv(m_nev);
393
394 for (Index i = 0; i < m_nev; i++)
395 {
396 new_ritz_val[i] = m_ritz_val[ind[i]];
397 new_ritz_vec.col(i).noalias() = m_ritz_vec.col(ind[i]);
398 new_ritz_conv[i] = m_ritz_conv[ind[i]];
399 }
400
401 m_ritz_val.swap(new_ritz_val);
402 m_ritz_vec.swap(new_ritz_vec);
403 m_ritz_conv.swap(new_ritz_conv);
404 }
405
406public:
408
409 GenEigsBase(OpType& op, const BOpType& Bop, Index nev, Index ncv) :
410 m_op(op),
411 m_n(m_op.rows()),
412 m_nev(nev),
413 m_ncv(ncv > m_n ? m_n : ncv),
414 m_nmatop(0),
415 m_niter(0),
416 m_fac(ArnoldiOpType(op, Bop), m_ncv),
418 {
419 if (nev < 1 || nev > m_n - 2)
420 throw std::invalid_argument("nev must satisfy 1 <= nev <= n - 2, n is the size of matrix");
421
422 if (ncv < nev + 2 || ncv > m_n)
423 throw std::invalid_argument("ncv must satisfy nev + 2 <= ncv <= n, n is the size of matrix");
424 }
425
429 virtual ~GenEigsBase() {}
430
432
442 void init(const Scalar* init_resid)
443 {
444 // Reset all matrices/vectors to zero
445 m_ritz_val.resize(m_ncv);
446 m_ritz_vec.resize(m_ncv, m_nev);
447 m_ritz_est.resize(m_ncv);
448 m_ritz_conv.resize(m_nev);
449
450 m_ritz_val.setZero();
451 m_ritz_vec.setZero();
452 m_ritz_est.setZero();
453 m_ritz_conv.setZero();
454
455 m_nmatop = 0;
456 m_niter = 0;
457
458 // Initialize the Arnoldi factorization
459 MapConstVec v0(init_resid, m_n);
460 m_fac.init(v0, m_nmatop);
461 }
462
470 void init()
471 {
472 SimpleRandom<Scalar> rng(0);
473 Vector init_resid = rng.random_vec(m_n);
474 init(init_resid.data());
475 }
476
501 Index compute(SortRule selection = SortRule::LargestMagn, Index maxit = 1000,
502 RealScalar tol = 1e-10, SortRule sorting = SortRule::LargestMagn)
503 {
504 // The m-step Arnoldi factorization
505 m_fac.factorize_from(1, m_ncv, m_nmatop);
506 retrieve_ritzpair(selection);
507 // Restarting
508 Index i, nconv = 0, nev_adj;
509 for (i = 0; i < maxit; i++)
510 {
511 nconv = num_converged(tol);
512 if (nconv >= m_nev)
513 break;
514
515 nev_adj = nev_adjusted(nconv);
516 restart(nev_adj, selection);
517 }
518 // Sorting results
519 sort_ritzpair(sorting);
520
521 m_niter += (i + 1);
522 m_info = (nconv >= m_nev) ? CompInfo::Successful : CompInfo::NotConverging;
523
524 return (std::min)(m_nev, nconv);
525 }
526
531 CompInfo info() const { return m_info; }
532
536 Index num_iterations() const { return m_niter; }
537
541 Index num_operations() const { return m_nmatop; }
542
550 ComplexVector eigenvalues() const
551 {
552 const Index nconv = m_ritz_conv.cast<Index>().sum();
553 ComplexVector res(nconv);
554
555 if (!nconv)
556 return res;
557
558 Index j = 0;
559 for (Index i = 0; i < m_nev; i++)
560 {
561 if (m_ritz_conv[i])
562 {
563 res[j] = m_ritz_val[i];
564 j++;
565 }
566 }
567
568 return res;
569 }
570
580 ComplexMatrix eigenvectors(Index nvec) const
581 {
582 const Index nconv = m_ritz_conv.cast<Index>().sum();
583 nvec = (std::min)(nvec, nconv);
584 ComplexMatrix res(m_n, nvec);
585
586 if (!nvec)
587 return res;
588
589 ComplexMatrix ritz_vec_conv(m_ncv, nvec);
590 Index j = 0;
591 for (Index i = 0; i < m_nev && j < nvec; i++)
592 {
593 if (m_ritz_conv[i])
594 {
595 ritz_vec_conv.col(j).noalias() = m_ritz_vec.col(i);
596 j++;
597 }
598 }
599
600 res.noalias() = m_fac.matrix_V() * ritz_vec_conv;
601
602 return res;
603 }
604
608 ComplexMatrix eigenvectors() const
609 {
610 return eigenvectors(m_nev);
611 }
612};
613
614} // namespace Spectra
615
616#endif // SPECTRA_GEN_EIGS_BASE_H
ComplexVector eigenvalues() const
Index compute(SortRule selection=SortRule::LargestMagn, Index maxit=1000, RealScalar tol=1e-10, SortRule sorting=SortRule::LargestMagn)
Index num_operations() const
ComplexMatrix eigenvectors() const
ComplexMatrix eigenvectors(Index nvec) const
Index num_iterations() const
CompInfo info() const
void init(const Scalar *init_resid)
@ SmallestImag
Select eigenvalues with smallest imaginary part (in magnitude). Only for general eigen solvers.
@ SmallestReal
Select eigenvalues with smallest real part. Only for general eigen solvers.
@ LargestImag
Select eigenvalues with largest imaginary part (in magnitude). Only for general eigen solvers.
@ LargestReal
Select eigenvalues with largest real part. Only for general eigen solvers.
@ Successful
Computation was successful.
Definition CompInfo.h:19