| // This file is part of Eigen, a lightweight C++ template library | |
| // for linear algebra. | |
| // | |
| // Copyright (C) 2026 Rasmus Munk Larsen <rmlarsen@gmail.com> | |
| // | |
| // This Source Code Form is subject to the terms of the Mozilla | |
| // Public License v. 2.0. If a copy of the MPL was not distributed | |
| // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. | |
| // SPDX-License-Identifier: MPL-2.0 | |
| #ifndef EIGEN_TRIDIAGONAL_EIGENSOLVER_H | |
| #define EIGEN_TRIDIAGONAL_EIGENSOLVER_H | |
| #include "./TridiagonalBisection.h" | |
| #include "./TridiagonalInverseIteration.h" | |
| // IWYU pragma: private | |
| #include "./InternalHeaderCheck.h" | |
| namespace Eigen { | |
| /** \eigenvalues_module \ingroup Eigenvalues_Module | |
| * | |
| * \class TridiagonalEigenSolver | |
| * | |
| * \brief Computes eigenvalues and eigenvectors of a real symmetric tridiagonal matrix | |
| * | |
| * \tparam Scalar_ the (real floating-point) scalar type of the matrix, e.g. \c float or \c double. | |
| * | |
| * This solver computes the eigenvalues of a real symmetric tridiagonal matrix \f$ T \f$, given by | |
| * its diagonal and sub-diagonal, using SIMD-accelerated, multi-threaded Sturm-sequence spectral | |
| * bisection (cf. LAPACK's \c xSTEBZ), and the corresponding eigenvectors by inverse iteration | |
| * (cf. LAPACK's \c xSTEIN). Unlike the implicit-QR path of | |
| * SelfAdjointEigenSolver::computeFromTridiagonal(), it can compute an arbitrary contiguous subset | |
| * of the spectrum -- by index range or by value range -- selected with an EigenvalueRange, and it | |
| * never pays for eigenvectors that are not requested: | |
| * | |
| * \code | |
| * TridiagonalEigenSolver<double> es; | |
| * es.compute(diag, subdiag); // full spectrum, eigenvalues + eigenvectors | |
| * es.compute(diag, subdiag, EigenvaluesOnly); // full spectrum, eigenvalues only | |
| * es.compute(diag, subdiag, ComputeEigenvectors, | |
| * EigenvalueRange::indices(0, 10)); // the 10 smallest eigenpairs | |
| * | |
| * // Staged: eigenvalues now, eigenvectors later (only if they turn out to be needed). | |
| * es.computeEigenvalues(diag, subdiag, EigenvalueRange::values(vl, vu)); | |
| * es.computeEigenvectors(); | |
| * | |
| * // Direct: eigenvectors for already-known eigenvalues. | |
| * es.computeEigenvectors(diag, subdiag, eigenvalues); | |
| * \endcode | |
| * | |
| * In every mode the computed eigenvalues are returned by eigenvalues() in non-decreasing order, | |
| * one entry per selected eigenvalue, and eigenvectors() is the \c n x \c m matrix whose column | |
| * \c j is a unit-norm eigenvector for \c eigenvalues()(j). | |
| * | |
| * Besides subset selection, bisection is typically more accurate than the QR algorithm: it | |
| * resolves each eigenvalue to about one unit in the last place of \f$ \|T\| \f$ independently of | |
| * the others, whereas the QR forward error accumulates through its O(n) sweeps and grows with the | |
| * matrix size. (Both are backward stable; this is absolute accuracy relative to \f$ \|T\| \f$, not | |
| * high relative accuracy for eigenvalues much smaller than \f$ \|T\| \f$.) | |
| * | |
| * The eigenvectors are those of the tridiagonal \f$ T \f$ itself; recovering eigenvectors of a | |
| * dense matrix additionally requires the Householder back-transform performed by | |
| * SelfAdjointEigenSolver::compute(). | |
| * | |
| * \note The eigenvalues of a complex Hermitian tridiagonal matrix depend only on the moduli of its | |
| * off-diagonal entries: it is unitarily similar (via a diagonal phase matrix) to the real symmetric | |
| * tridiagonal with the same diagonal and off-diagonals \f$ |\beta_k| \f$. So to compute them, pass | |
| * \c subdiag.cwiseAbs() as the real sub-diagonal. The eigenvectors, by contrast, differ from the | |
| * real ones by that diagonal phase and cannot be recovered this way. | |
| * | |
| * \sa EigenvalueRange, SelfAdjointEigenSolver::computeFromTridiagonal(), class Tridiagonalization | |
| */ | |
| template <typename Scalar_> | |
| class TridiagonalEigenSolver { | |
| public: | |
| /** \brief Scalar type of the matrix; must be real. */ | |
| using Scalar = Scalar_; | |
| using RealScalar = Scalar; | |
| static_assert(NumTraits<Scalar>::IsComplex == 0 && NumTraits<Scalar>::IsInteger == 0, | |
| "TridiagonalEigenSolver requires a real floating-point scalar type; for the eigenvalues of a complex " | |
| "Hermitian tridiagonal matrix, pass subdiag.cwiseAbs() as the real sub-diagonal (see the class " | |
| "documentation)."); | |
| /** \brief Type for the eigenvalues and the input diagonals: a dynamic-size column vector. */ | |
| using VectorType = Matrix<Scalar, Dynamic, 1>; | |
| /** \brief Type for the eigenvector matrix: dynamic-size, one column per selected eigenvalue. */ | |
| using MatrixType = Matrix<Scalar, Dynamic, Dynamic>; | |
| /** \brief Default constructor. Call compute() before querying any result. */ | |
| TridiagonalEigenSolver() = default; | |
| /** \brief Constructor pre-allocating room for the full spectrum of a matrix of dimension \a size. */ | |
| explicit TridiagonalEigenSolver(Index size) | |
| : m_eivalues(size), m_eivec(size, size), m_diag(size), m_subdiag(size > 1 ? size - 1 : 0) {} | |
| /** \brief Constructor; computes the eigendecomposition of the given tridiagonal matrix. | |
| * | |
| * Equivalent to default construction followed by compute(\a diag, \a subdiag, \a options, \a range). | |
| */ | |
| template <typename DiagType, typename SubdiagType> | |
| TridiagonalEigenSolver(const MatrixBase<DiagType>& diag, const MatrixBase<SubdiagType>& subdiag, | |
| int options = ComputeEigenvectors, const EigenvalueRange& range = EigenvalueRange::all()) | |
| : TridiagonalEigenSolver() { | |
| compute(diag, subdiag, options, range); | |
| } | |
| /** \brief Computes the selected eigenvalues, and optionally eigenvectors, of a real symmetric | |
| * tridiagonal matrix. | |
| * | |
| * \param[in] diag The diagonal of the matrix \f$ T \f$ (length \c n). | |
| * \param[in] subdiag The sub-diagonal of \f$ T \f$ (length \c n-1). | |
| * \param[in] options Either #ComputeEigenvectors (the default) or #EigenvaluesOnly. | |
| * \param[in] range Which eigenvalues to compute (see EigenvalueRange); defaults to the whole | |
| * spectrum. | |
| * \returns Reference to \c *this | |
| * | |
| * Equivalent to computeEigenvalues(\a diag, \a subdiag, \a range), followed -- when \a options | |
| * is #ComputeEigenvectors and the eigenvalues were computed successfully -- by | |
| * computeEigenvectors(). | |
| */ | |
| template <typename DiagType, typename SubdiagType> | |
| TridiagonalEigenSolver& compute(const MatrixBase<DiagType>& diag, const MatrixBase<SubdiagType>& subdiag, | |
| int options = ComputeEigenvectors, | |
| const EigenvalueRange& range = EigenvalueRange::all()) { | |
| eigen_assert((options & ~EigVecMask) == 0 && (options & EigVecMask) != EigVecMask && "invalid option parameter"); | |
| computeEigenvalues(diag, subdiag, range); | |
| if (m_info == Success && (options & ComputeEigenvectors) == ComputeEigenvectors) computeEigenvectors(); | |
| return *this; | |
| } | |
| /** \brief Computes the selected eigenvalues (only) of a real symmetric tridiagonal matrix. | |
| * | |
| * \param[in] diag The diagonal of the matrix \f$ T \f$ (length \c n). | |
| * \param[in] subdiag The sub-diagonal of \f$ T \f$ (length \c n-1). | |
| * \param[in] range Which eigenvalues to compute (see EigenvalueRange); defaults to the whole | |
| * spectrum. | |
| * \returns Reference to \c *this | |
| * | |
| * After the call, eigenvalues() returns the selected eigenvalues in non-decreasing order (one | |
| * entry per selected eigenvalue), and info() reports \c Success, or \c NoConvergence if the | |
| * input contains a non-finite entry. The tridiagonal is retained, so the eigenvectors can be | |
| * obtained afterwards with computeEigenvectors() -- and need never be computed if they turn out | |
| * not to be required. | |
| */ | |
| template <typename DiagType, typename SubdiagType> | |
| TridiagonalEigenSolver& computeEigenvalues(const MatrixBase<DiagType>& diag, const MatrixBase<SubdiagType>& subdiag, | |
| const EigenvalueRange& range = EigenvalueRange::all()); | |
| /** \brief Computes eigenvectors for the eigenvalues of the preceding compute step, by inverse | |
| * iteration. | |
| * | |
| * \returns Reference to \c *this | |
| * | |
| * Completes a staged solve: call it after computeEigenvalues() to compute the eigenvectors for | |
| * the eigenvalues just found, using the retained tridiagonal. See | |
| * computeEigenvectors(const MatrixBase<DiagType>&, const MatrixBase<SubdiagType>&, const | |
| * MatrixBase<EivalsType>&) for the algorithm and its guarantees. That overload trusts supplied | |
| * eigenvalues unless a Sturm count shows one misplaced; these are known to be accurate only to | |
| * about \f$ eps \|T\| \f$, so the shifts of a disconnected block too small for that error to | |
| * leave its eigenvectors orthogonal are re-bisected at the block's own scale first (in practice, | |
| * float blocks a few dozen times smaller than \f$ \|T\| \f$). eigenvalues() is unchanged. | |
| * | |
| * \pre A successful computeEigenvalues() (or compute()) call. | |
| */ | |
| TridiagonalEigenSolver& computeEigenvectors() { | |
| eigen_assert(m_isInitialized && "TridiagonalEigenSolver is not initialized."); | |
| eigen_assert(m_info == Success && "computeEigenvectors() requires a preceding successful eigenvalue computation"); | |
| computeEigenvectorsImpl(); | |
| return *this; | |
| } | |
| /** \brief Computes eigenvectors of a tridiagonal matrix by inverse iteration from known eigenvalues. | |
| * | |
| * \param[in] diag The diagonal of the symmetric tridiagonal matrix \f$ T \f$ (length \c n). | |
| * \param[in] subdiag The sub-diagonal of \f$ T \f$ (length \c n-1). | |
| * \param[in] eigenvalues The eigenvalues whose eigenvectors are wanted, in non-decreasing order | |
| * (e.g. the output of computeEigenvalues()). Length \c m \f$\le\f$ \c n. | |
| * \returns Reference to \c *this | |
| * | |
| * Computes an eigenvector of \f$ T \f$ for each supplied eigenvalue by inverse iteration (LAPACK's | |
| * xSTEIN, built on the xLAGTF / xLAGTS factorization and overflow-safe solve of the deliberately | |
| * near-singular \f$ T - \lambda I \f$), reorthogonalizing within tight clusters so that a degenerate | |
| * cluster yields an orthonormal basis. After the call, eigenvectors() returns the \c n x \c m matrix | |
| * whose column \c j is a unit-norm eigenvector for \c eigenvalues[j], and eigenvalues() returns the | |
| * supplied eigenvalues. | |
| * Shifts that are insufficiently accurate at their disconnected block's scale are refined | |
| * internally before iteration, without changing the eigenvalues returned by eigenvalues(). | |
| * | |
| * \b Subsets. This is the natural companion to the subset-selecting eigenvalue path (see | |
| * EigenvalueRange): pass that subset as \a eigenvalues and you get back exactly those \c m | |
| * eigenvectors -- the result has one column per supplied eigenvalue and costs one | |
| * inverse-iteration solve each, so you never pay for vectors you did not ask for. | |
| * | |
| * \note The eigenvalues must be sorted in non-decreasing order, as the cluster | |
| * reorthogonalization relies on it. | |
| * | |
| * \note If any eigenvector fails to converge within the internal inverse-iteration step limit, the | |
| * vectors are still returned but info() reports \c NoConvergence (cf. LAPACK xSTEIN). | |
| * | |
| * \note The inverse iteration is bit-identical for any number of threads. The eigenvectors of a | |
| * numerically degenerate cluster may nonetheless differ in their last bits between a single- and a | |
| * multi-threaded run, because the cluster refinement uses parallel (non-associative) matrix products; | |
| * each result is an equally valid orthonormal basis of the same invariant subspace. | |
| * | |
| * \warning Cluster reorthogonalization is performed only \e among the supplied eigenvalues. The | |
| * returned eigenvectors are always mutually orthonormal, but they are not orthogonalized against | |
| * eigenvectors of a degenerate cluster that lie outside \a eigenvalues (those are never computed). If | |
| * a requested subset slices through the middle of a numerically degenerate cluster, supply the whole | |
| * cluster (widen the range) to obtain the correct invariant subspace. For well-separated eigenvalues, | |
| * or when the subset covers each cluster entirely, this never matters. | |
| * | |
| * \sa computeEigenvalues(), eigenvectors() | |
| */ | |
| template <typename DiagType, typename SubdiagType, typename EivalsType> | |
| TridiagonalEigenSolver& computeEigenvectors(const MatrixBase<DiagType>& diag, const MatrixBase<SubdiagType>& subdiag, | |
| const MatrixBase<EivalsType>& eigenvalues); | |
| /** \brief Returns the computed eigenvalues, in non-decreasing order. | |
| * | |
| * \pre compute(), computeEigenvalues() or computeEigenvectors() has been called. | |
| * | |
| * The returned vector has one entry per \e selected eigenvalue, so a subset request yields a | |
| * vector shorter than the matrix dimension. Eigenvalues are repeated according to their | |
| * algebraic multiplicity. | |
| */ | |
| const VectorType& eigenvalues() const { | |
| eigen_assert(m_isInitialized && "TridiagonalEigenSolver is not initialized."); | |
| return m_eivalues; | |
| } | |
| /** \brief Returns the computed eigenvectors, one column per eigenvalue. | |
| * | |
| * \pre The eigenvectors have been computed (compute() with #ComputeEigenvectors, or one of the | |
| * computeEigenvectors() overloads). | |
| * | |
| * Column \c j of the returned \c n x \c m matrix is a unit-norm eigenvector of \f$ T \f$ for | |
| * eigenvalues()(j); the columns are mutually orthonormal. | |
| */ | |
| const MatrixType& eigenvectors() const { | |
| eigen_assert(m_isInitialized && "TridiagonalEigenSolver is not initialized."); | |
| eigen_assert(m_eigenvectorsOk && "The eigenvectors have not been computed."); | |
| return m_eivec; | |
| } | |
| /** \brief Reports whether the computation was successful. | |
| * | |
| * \returns \c Success if the computation was successful, \c NoConvergence otherwise. | |
| */ | |
| ComputationInfo info() const { | |
| eigen_assert(m_isInitialized && "TridiagonalEigenSolver is not initialized."); | |
| return m_info; | |
| } | |
| protected: | |
| // Sturm counts, bisection targets, and inverse-iteration thresholds are carried in the scalar type | |
| // itself, so scalars narrower than float (half, bfloat16) compute in float internally: their | |
| // epsilon cannot represent consecutive integer counts beyond ~1/eps (a 257-element bfloat16 | |
| // identity would report an eigenvalue of "5.19"), and intermediates such as the coincident-shift | |
| // perturbation quantize away. Results are rounded back to Scalar on output. | |
| using ComputeScalar = std::conditional_t<(NumTraits<Scalar>::digits() < NumTraits<float>::digits()), float, Scalar>; | |
| void computeEigenvectorsImpl(); | |
| VectorType m_eivalues; | |
| MatrixType m_eivec; | |
| // The tridiagonal retained by the last compute step, so the staged computeEigenvectors() can | |
| // re-form T - lambda*I without the caller having to keep diag/subdiag alive. | |
| VectorType m_diag; | |
| VectorType m_subdiag; | |
| // Narrow scalars only: the unrounded float eigenvalues of the last computeEigenvalues(), kept so | |
| // the staged eigenvector pass shifts by them rather than by the Scalar-rounded m_eivalues -- | |
| // shifts quantized to Scalar collapse neighbouring eigenvalues and yield near-parallel vectors. | |
| // Empty whenever m_eivalues did not come from this solver's own bisection. | |
| Matrix<ComputeScalar, Dynamic, 1> m_eivaluesc; | |
| // Absolute error bound of the eigenvalues of the last computeEigenvalues(), as the staged eigenvector | |
| // pass receives them: the bisection tolerance, about eps * ||T||. Zero for caller-supplied eigenvalues, | |
| // which inverse iteration takes as exact. A disconnected block far below ||T|| may need its shifts | |
| // re-bisected at its own scale (see internal::tridiagonal_inverse_iteration()). | |
| ComputeScalar m_shiftTol = ComputeScalar(0); | |
| ComputationInfo m_info = InvalidInput; | |
| bool m_isInitialized = false; | |
| bool m_eigenvectorsOk = false; | |
| }; | |
| template <typename Scalar_> | |
| template <typename DiagType, typename SubdiagType> | |
| TridiagonalEigenSolver<Scalar_>& TridiagonalEigenSolver<Scalar_>::computeEigenvalues( | |
| const MatrixBase<DiagType>& diag, const MatrixBase<SubdiagType>& subdiag, const EigenvalueRange& range) { | |
| static_assert(internal::is_same<typename DiagType::Scalar, Scalar>::value && | |
| internal::is_same<typename SubdiagType::Scalar, Scalar>::value, | |
| "diag and subdiag must have the solver's scalar type"); | |
| const Index n = diag.size(); | |
| EIGEN_UNUSED_VARIABLE(n); | |
| eigen_assert(subdiag.size() == (n > 0 ? n - 1 : 0) && "sub-diagonal must have one fewer entry than the diagonal"); | |
| m_diag = diag; | |
| m_subdiag = subdiag; | |
| m_eigenvectorsOk = false; | |
| // Reject non-finite input up front (cf. SelfAdjointEigenSolver::computeFromTridiagonal()): the | |
| // Gershgorin bracketing below would otherwise iterate on NaN brackets and return garbage with | |
| // info() == Success. allFinite() rather than a max-reduction, whose default PropagateFast | |
| // semantics may not surface a NaN. | |
| if (!(m_diag.allFinite() && m_subdiag.allFinite())) { | |
| m_eivalues.resize(0); | |
| m_eivaluesc.resize(0); | |
| m_shiftTol = ComputeScalar(0); | |
| m_info = NoConvergence; | |
| m_isInitialized = true; | |
| return *this; | |
| } | |
| if (internal::is_same<Scalar, ComputeScalar>::value) { | |
| RealScalar shift_tol(0); | |
| internal::tridiagonal_bisection(m_diag, m_subdiag, range, RealScalar(0), m_eivalues, &shift_tol); | |
| m_shiftTol = ComputeScalar(shift_tol); | |
| m_eivaluesc.resize(0); | |
| } else { | |
| // Narrow scalar: bisect in float and round the eigenvalues back (see ComputeScalar). The | |
| // unrounded values are retained for the staged eigenvector pass (see m_eivaluesc). | |
| const Matrix<ComputeScalar, Dynamic, 1> cdiag = m_diag.template cast<ComputeScalar>(); | |
| const Matrix<ComputeScalar, Dynamic, 1> csubdiag = m_subdiag.template cast<ComputeScalar>(); | |
| internal::tridiagonal_bisection(cdiag, csubdiag, range, ComputeScalar(0), m_eivaluesc, &m_shiftTol); | |
| m_eivalues = m_eivaluesc.template cast<Scalar>(); | |
| } | |
| m_info = Success; | |
| m_isInitialized = true; | |
| return *this; | |
| } | |
| template <typename Scalar_> | |
| template <typename DiagType, typename SubdiagType, typename EivalsType> | |
| TridiagonalEigenSolver<Scalar_>& TridiagonalEigenSolver<Scalar_>::computeEigenvectors( | |
| const MatrixBase<DiagType>& diag, const MatrixBase<SubdiagType>& subdiag, | |
| const MatrixBase<EivalsType>& eigenvalues) { | |
| static_assert(internal::is_same<typename DiagType::Scalar, Scalar>::value && | |
| internal::is_same<typename SubdiagType::Scalar, Scalar>::value && | |
| internal::is_same<typename EivalsType::Scalar, Scalar>::value, | |
| "diag, subdiag and eigenvalues must have the solver's scalar type"); | |
| const Index n = diag.size(); | |
| EIGEN_UNUSED_VARIABLE(n); | |
| eigen_assert(subdiag.size() == (n > 0 ? n - 1 : 0) && "sub-diagonal must have one fewer entry than the diagonal"); | |
| eigen_assert(eigenvalues.size() <= n && "cannot request more eigenvectors than the size of the matrix"); | |
| m_diag = diag; | |
| m_subdiag = subdiag; | |
| m_eivalues = eigenvalues; | |
| m_eivaluesc.resize(0); // the caller's Scalar eigenvalues are authoritative here | |
| m_shiftTol = ComputeScalar(0); | |
| // Reject non-finite input up front, mirroring computeEigenvalues(): inverse iteration on NaN/Inf | |
| // data (or NaN shifts) would return NaN vectors with info() == Success. | |
| if (!(m_diag.allFinite() && m_subdiag.allFinite() && m_eivalues.allFinite())) { | |
| m_eivalues.resize(0); | |
| m_eivec.resize(m_diag.size(), 0); | |
| m_info = NoConvergence; | |
| m_isInitialized = true; | |
| m_eigenvectorsOk = false; | |
| return *this; | |
| } | |
| computeEigenvectorsImpl(); | |
| return *this; | |
| } | |
| template <typename Scalar_> | |
| void TridiagonalEigenSolver<Scalar_>::computeEigenvectorsImpl() { | |
| const Index n = m_diag.size(); | |
| const Index m = m_eivalues.size(); | |
| m_eivec.resize(n, m); | |
| Index nonconv; | |
| if (internal::is_same<Scalar, ComputeScalar>::value) { | |
| nonconv = internal::tridiagonal_inverse_iteration(m_diag, m_subdiag, m_eivalues, m_eivec, RealScalar(m_shiftTol)); | |
| // Refine the eigenvectors of any genuinely degenerate cluster (Rayleigh-Ritz). The eigenvalues | |
| // are left unchanged, and a non-degenerate spectrum is untouched. | |
| internal::tridiagonal_rayleigh_ritz_refine(m_diag, m_subdiag, m_eivalues, m_eivec); | |
| } else { | |
| // Narrow scalar: iterate and refine in float, then round the vectors back (see ComputeScalar). | |
| // Shift by the retained unrounded eigenvalues when they exist (the staged path): shifts rounded | |
| // to Scalar collapse neighbouring eigenvalues and produce near-parallel vectors. | |
| const Matrix<ComputeScalar, Dynamic, 1> cdiag = m_diag.template cast<ComputeScalar>(); | |
| const Matrix<ComputeScalar, Dynamic, 1> csubdiag = m_subdiag.template cast<ComputeScalar>(); | |
| const Matrix<ComputeScalar, Dynamic, 1> cw = | |
| (m_eivaluesc.size() == m) ? m_eivaluesc : m_eivalues.template cast<ComputeScalar>(); | |
| Matrix<ComputeScalar, Dynamic, Dynamic> cvec(n, m); | |
| nonconv = internal::tridiagonal_inverse_iteration(cdiag, csubdiag, cw, cvec, m_shiftTol); | |
| internal::tridiagonal_rayleigh_ritz_refine(cdiag, csubdiag, cw, cvec); | |
| m_eivec = cvec.template cast<Scalar>(); | |
| } | |
| // Like LAPACK xSTEIN, report NoConvergence if any eigenvector failed the inverse-iteration growth | |
| // test (the columns are still returned, best effort, so eigenvectors() remains usable). | |
| m_info = (nonconv == 0) ? Success : NoConvergence; | |
| m_isInitialized = true; | |
| m_eigenvectorsOk = true; | |
| } | |
| } // namespace Eigen | |
| #endif // EIGEN_TRIDIAGONAL_EIGENSOLVER_H |