blob: 403d052e5ee52be36b04ecfff24d1d019090b156 [file]
// 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.
*
* \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.
*
* \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;
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_info = NoConvergence;
m_isInitialized = true;
return *this;
}
if (internal::is_same<Scalar, ComputeScalar>::value) {
internal::tridiagonal_bisection(m_diag, m_subdiag, range, RealScalar(0), m_eivalues);
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_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
// 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);
// 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);
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