blob: 9f1e87987e03a7aadcaadf17550a90396c79f5f7 [file]
// This file is part of Eigen, a lightweight C++ template library
// for linear algebra.
//
// 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-FileCopyrightText: The Eigen Authors
// SPDX-License-Identifier: MPL-2.0
#ifndef EIGEN_STRUCTURED_DPR1_EIGEN_SOLVER_H
#define EIGEN_STRUCTURED_DPR1_EIGEN_SOLVER_H
// IWYU pragma: private
#include "./InternalHeaderCheck.h"
namespace Eigen {
/** \ingroup StructuredMatrices_Module
* \class DPR1EigenSolver
* \brief Direct O(n^2) eigensolver for real symmetric diagonal-plus-rank-one
* matrices \f$ A = D + \rho\, z z^T \f$, via the secular equation.
*
* This is the standalone version of the kernel at the heart of the
* divide-and-conquer symmetric eigensolvers (LAPACK's xLAED2/3/4): after
* \em deflation -- entries with negligible \f$ |z_i| \f$ are eigenpairs of the
* diagonal already, and (nearly) equal diagonal entries are combined by Givens
* rotations whose dropped coupling is below a backward-stability threshold --
* the surviving eigenvalues are the roots of the secular equation
* \f[ f(\lambda) = 1 + \rho \sum_i \frac{z_i^2}{d_i - \lambda} = 0, \f]
* one in each interval between consecutive poles. Each root is bracketed and
* bisected in coordinates \em shifted to its nearest pole, so every distance
* \f$ \lambda - d_i \f$ is retained as an exact data difference plus a small
* offset instead of a cancellation-prone subtraction of close numbers.
* Eigenvectors are then built not from the original \c z but from the
* Gu-Eisenstat vector \f$ \hat z \f$ -- the one for which the computed roots
* are \em exact secular eigenvalues -- which is what makes the computed
* eigenvector matrix numerically orthogonal without any reorthogonalization.
*
* The total cost is O(n^2): O(n log(1/eps)) per bisected root in the common
* case (the iteration cap is sized to the scalar's full exponent range, so
* even roots subnormally close to their pole resolve), O(n) per Gu-Eisenstat
* weight, and O(n) per eigenvector (the deflation rotations are replayed
* instead of accumulated into a dense matrix).
*
* Both signs of \f$ \rho \f$ are supported (negative \f$ \rho \f$ is handled by
* negating the matrix), as are \f$ \rho = 0 \f$, zero \c z, repeated diagonal
* entries and any ordering of \c d. The problem is rescaled internally by
* exact powers of two chosen from the component exponents alone, so data
* anywhere in the representable range is handled without internal overflow --
* including inputs for which \f$ \rho \|z\|^2 \f$ alone exceeds the largest
* finite value while every eigenvalue is still representable. \c InvalidInput
* is reported only for non-finite input or a spectrum that is itself not
* representable. On well-scaled data the rescaling changes no bits of the
* result (rounding can occur only at the subnormal boundary, far below the
* deflation backward-error budget).
*
* \code
* DPR1EigenSolver<double> es(d, rho, z);
* VectorXd lambda = es.eigenvalues(); // ascending
* MatrixXd V = es.eigenvectors(); // orthogonal
* \endcode
*
* \tparam RealScalar_ one of \c float, \c double, or \c long \c double.
*
* References:
* - M. Gu and S. C. Eisenstat, "A stable and efficient algorithm for the
* rank-one modification of the symmetric eigenproblem," SIAM J. Matrix Anal.
* Appl., 15(4):1266-1276, 1994.
* - N. J. Higham, "Accuracy and Stability of Numerical Algorithms", 2nd ed.,
* SIAM, 2002, chapter 27. Avoiding spurious overflow by rescaling with
* powers of two: the whole-problem scale chosen from component exponents
* and the (mantissa, exponent) handling of rho*||z||^2 follow this
* technique.
* - P. H. Sterbenz, "Floating-Point Computation", Prentice-Hall, 1974.
* Scaling by a power of two is exact, the property the frexp/ldexp
* problem scaling and the full-range ArrayBase::ldexp unscaling of the
* eigenvalues rely on.
*
* \sa class SelfAdjointEigenSolver
*/
template <typename RealScalar_>
class DPR1EigenSolver {
public:
using RealScalar = RealScalar_;
using Scalar = RealScalar;
using Index = Eigen::Index;
using VectorType = Matrix<RealScalar, Dynamic, 1>;
using MatrixType = Matrix<RealScalar, Dynamic, Dynamic>;
static_assert(std::is_same<RealScalar, float>::value || std::is_same<RealScalar, double>::value ||
std::is_same<RealScalar, long double>::value,
"DPR1EigenSolver supports only float, double, and long double scalar types.");
/** Default constructor; call \ref compute before querying results. */
DPR1EigenSolver() = default;
/** Computes the eigendecomposition of \c diag(d) + \c rho*z*z^T.
* \a options is \c ComputeEigenvectors (the default) or \c EigenvaluesOnly. */
DPR1EigenSolver(const VectorType& d, RealScalar rho, const VectorType& z, int options = ComputeEigenvectors) {
compute(d, rho, z, options);
}
/** Computes the eigendecomposition of \c diag(d) + \c rho*z*z^T. \sa DPR1EigenSolver() */
DPR1EigenSolver& compute(const VectorType& d, RealScalar rho, const VectorType& z, int options = ComputeEigenvectors);
/** \returns the eigenvalues, sorted in increasing order. */
const VectorType& eigenvalues() const {
eigen_assert(m_isInitialized && "DPR1EigenSolver is not initialized.");
return m_eivalues;
}
/** \returns the orthogonal matrix of eigenvectors; column \c k matches
* \c eigenvalues()[k]. \pre \ref compute was called with \c ComputeEigenvectors. */
const MatrixType& eigenvectors() const {
eigen_assert(m_isInitialized && "DPR1EigenSolver is not initialized.");
eigen_assert(m_vectorsComputed && "eigenvectors were not computed");
return m_eivec;
}
/** \returns \c Success if the decomposition succeeded, \c NoConvergence if a
* secular root or the spectrum's finite-range boundary could not be fully
* resolved, \c InvalidInput if the input was non-finite or its spectrum is
* not representable in the scalar type. */
ComputationInfo info() const {
eigen_assert(m_isInitialized && "DPR1EigenSolver is not initialized.");
return m_info;
}
private:
// A deflation-stage Givens rotation acting on working rows (i, j).
struct Rotation {
Index i, j;
RealScalar c, s;
};
struct DoubleWord {
RealScalar hi, lo;
};
enum class SpectrumRange { Representable, ExactBoundary, Overflow, Uncertain };
static DoubleWord addDoubleWords(const DoubleWord& x, const DoubleWord& y) {
DoubleWord result;
internal::twosum(x.hi, x.lo, y.hi, y.lo, result.hi, result.lo);
return result;
}
static DoubleWord multiplyDoubleWords(const DoubleWord& x, const DoubleWord& y) {
DoubleWord result;
internal::twoprod(x.hi, x.lo, y.hi, y.lo, result.hi, result.lo);
return result;
}
static DoubleWord divideDoubleWords(const DoubleWord& x, const DoubleWord& y) {
DoubleWord quotient;
internal::doubleword_div_fp(x.hi, x.lo, y.hi, quotient.hi, quotient.lo);
const DoubleWord product = multiplyDoubleWords(quotient, y);
const DoubleWord remainder = addDoubleWords(x, DoubleWord{-product.hi, -product.lo});
DoubleWord correction;
internal::doubleword_div_fp(remainder.hi, remainder.lo, y.hi, correction.hi, correction.lo);
return addDoubleWords(quotient, correction);
}
static DoubleWord scaleDoubleWord(const DoubleWord& x, int exponent) {
EIGEN_USING_STD(ldexp)
DoubleWord result{ldexp(x.hi, exponent), ldexp(x.lo, exponent)};
if ((numext::isfinite)(result.hi)) {
DoubleWord normalized;
internal::fast_twosum(result.hi, result.lo, normalized.hi, normalized.lo);
result = normalized;
}
return result;
}
static SpectrumRange classifySpectrumRange(const VectorType& d, RealScalar rho, const VectorType& z);
/** \internal Evaluates the shifted secular function
* g(tau) = 1 + rho * sum_i zeta_i^2 / (delta_i - tau), with delta_i the pole
* offsets relative to the chosen shift. */
static RealScalar secular(const VectorType& delta, const VectorType& zeta2, RealScalar rho, RealScalar tau) {
return RealScalar(1) + rho * (zeta2.array() / (delta.array() - tau)).sum();
}
VectorType m_eivalues;
MatrixType m_eivec;
bool m_isInitialized = false;
bool m_vectorsComputed = false;
ComputationInfo m_info = InvalidInput;
};
template <typename RealScalar_>
typename DPR1EigenSolver<RealScalar_>::SpectrumRange DPR1EigenSolver<RealScalar_>::classifySpectrumRange(
const VectorType& d, RealScalar rho, const VectorType& z) {
// For rho >= 0 only the largest eigenvalue can leave the finite range; with M = highest(), the
// matrix determinant lemma gives lambda_max <= M iff rho * sum_i z_i^2 / (M - d_i) <= 1. Evaluated
// from the original data, not the normalized secular problem, which has already rounded rho*||z||^2.
// Double-word arithmetic and its O(u^2) bounds follow Joldes, Muller and Popescu, "Tight and
// rigorous error bounds for basic building blocks of double-word arithmetic", ACM TOMS 44(2), 2017.
if (rho == RealScalar(0)) return SpectrumRange::Representable;
EIGEN_USING_STD(frexp)
EIGEN_USING_STD(ldexp)
const RealScalar highest = (std::numeric_limits<RealScalar>::max)();
const RealScalar highestHalf = highest / RealScalar(2);
int rhoExponent = 0;
const RealScalar rhoFraction = frexp(rho, &rhoExponent);
DoubleWord sum{RealScalar(0), RealScalar(0)};
bool exactSumValid = true;
Index active = 0;
for (Index i = 0; i < d.size(); ++i) {
if (z[i] == RealScalar(0)) continue;
++active;
if (d[i] == highest) return SpectrumRange::Overflow;
int zExponent = 0;
const RealScalar zFraction = frexp(numext::abs(z[i]), &zExponent);
DoubleWord numerator;
internal::twoprod(rhoFraction, zFraction, numerator.hi, numerator.lo);
const bool numeratorExact = numerator.lo == RealScalar(0) || zFraction == RealScalar(0.5);
if (zFraction == RealScalar(0.5)) {
numerator.hi *= zFraction;
numerator.lo *= zFraction;
DoubleWord normalized;
internal::fast_twosum(numerator.hi, numerator.lo, normalized.hi, normalized.lo);
numerator = normalized;
} else if (numerator.lo == RealScalar(0)) {
DoubleWord product;
internal::twoprod(numerator.hi, zFraction, product.hi, product.lo);
numerator = product;
} else {
DoubleWord product;
internal::twoprod(numerator.hi, numerator.lo, zFraction, product.hi, product.lo);
numerator = product;
}
// Work with (M - d_i)/2, which cannot overflow even when d_i = -M.
const RealScalar dHalf = d[i] / RealScalar(2);
DoubleWord denominator;
internal::twosum(highestHalf, RealScalar(0), -dHalf, RealScalar(0), denominator.hi, denominator.lo);
const bool denominatorExact = d[i] == RealScalar(0) || dHalf * RealScalar(2) == d[i];
const DoubleWord unscaledDenominator = denominator;
int denominatorExponent = 0;
frexp(unscaledDenominator.hi, &denominatorExponent);
const RealScalar scaledDenominatorHi = ldexp(unscaledDenominator.hi, -denominatorExponent);
const RealScalar scaledDenominatorLo = ldexp(unscaledDenominator.lo, -denominatorExponent);
const bool denominatorScaleExact = ldexp(scaledDenominatorHi, denominatorExponent) == unscaledDenominator.hi &&
ldexp(scaledDenominatorLo, denominatorExponent) == unscaledDenominator.lo;
denominator = scaleDoubleWord(unscaledDenominator, -denominatorExponent);
const int termExponent = rhoExponent + 2 * zExponent - denominatorExponent - 1;
const DoubleWord quotient = divideDoubleWords(numerator, denominator);
const DoubleWord term = scaleDoubleWord(quotient, termExponent);
if (!(numext::isfinite)(term.hi) || term.hi > RealScalar(2)) return SpectrumRange::Overflow;
const RealScalar previousSum = sum.hi;
sum = addDoubleWords(sum, term);
if (!(numext::isfinite)(sum.hi) || sum.hi > RealScalar(2)) return SpectrumRange::Overflow;
// Certify an exact endpoint equality only from terms whose division,
// exponent scaling, and scalar accumulation are all proven exact. This is
// deliberately stricter than the O(u^2) sign estimate below: a rounded
// equality must remain Uncertain rather than become a false Success.
if (exactSumValid) {
const RealScalar normalMin = (std::numeric_limits<RealScalar>::min)();
bool exactTerm = numeratorExact && denominatorExact && denominatorScaleExact && denominator.lo == RealScalar(0) &&
quotient.lo == RealScalar(0) && term.lo == RealScalar(0) && term.hi >= normalMin &&
ldexp(term.hi, -termExponent) == quotient.hi;
if (exactTerm) {
DoubleWord recoveredNumerator;
internal::twoprod(quotient.hi, denominator.hi, recoveredNumerator.hi, recoveredNumerator.lo);
exactTerm = recoveredNumerator.hi == numerator.hi && recoveredNumerator.lo == numerator.lo;
}
if (exactTerm) {
const RealScalar larger = numext::maxi(previousSum, term.hi);
const RealScalar smaller = numext::mini(previousSum, term.hi);
// The reversible subtraction remains a proof if FTZ erases a
// subnormal low word from the double-word addition.
exactSumValid = sum.lo == RealScalar(0) && sum.hi - larger == smaller;
} else {
exactSumValid = false;
}
}
}
if (active == 0) return SpectrumRange::Representable;
if (exactSumValid && sum.hi == RealScalar(1)) return SpectrumRange::ExactBoundary;
const DoubleWord difference = addDoubleWords(sum, DoubleWord{RealScalar(-1), RealScalar(0)});
const RealScalar estimate = difference.hi + difference.lo;
const RealScalar unitRoundoff = NumTraits<RealScalar>::epsilon() / RealScalar(2);
// The factor 32 covers two products, corrected division, exponent scaling,
// and accumulation per active term. Proven exact endpoint equality is handled
// above rather than widened into the uncertainty interval.
const RealScalar error = RealScalar(32) * RealScalar(active + 1) * unitRoundoff * unitRoundoff;
if (estimate < -error) return SpectrumRange::Representable;
if (estimate > error) return SpectrumRange::Overflow;
return SpectrumRange::Uncertain;
}
template <typename RealScalar_>
DPR1EigenSolver<RealScalar_>& DPR1EigenSolver<RealScalar_>::compute(const VectorType& d, RealScalar rho,
const VectorType& z, int options) {
const Index n = d.size();
eigen_assert(z.size() == n && "d and z must have the same size");
eigen_assert((options & ~EigVecMask) == 0 && (options & EigVecMask) != EigVecMask && "invalid option parameter");
const bool computeVectors = (options & ComputeEigenvectors) == ComputeEigenvectors;
m_vectorsComputed = false;
m_info = Success;
m_eivalues.resize(n);
if (computeVectors) m_eivec.setIdentity(n, n);
// Non-finite input would break the sorting comparator (not a strict weak
// order under NaN) and silently deflate everything; reject it up front.
if (!(d.allFinite() && z.allFinite() && (numext::isfinite)(rho))) {
m_eivalues.setConstant(NumTraits<RealScalar>::quiet_NaN());
m_info = InvalidInput;
m_isInitialized = true;
return *this;
}
if (n == 0) {
m_vectorsComputed = computeVectors;
m_isInitialized = true;
return *this;
}
const bool negated = rho < RealScalar(0);
VectorType dW = negated ? VectorType(-d) : d;
RealScalar rhoW = negated ? -rho : rho;
// pi maps each sorted working index to its input row.
std::vector<Index> pi;
pi.reserve(static_cast<std::size_t>(n));
for (Index i = 0; i < n; ++i) pi.push_back(i);
std::stable_sort(pi.begin(), pi.end(), [&dW](Index a, Index b) { return dW[a] < dW[b]; });
VectorType ds(n), zs(n);
for (Index i = 0; i < n; ++i) {
ds[i] = dW[pi[static_cast<std::size_t>(i)]];
zs[i] = z[pi[static_cast<std::size_t>(i)]];
}
// Normalize z and absorb ||z||^2 into rho. Power-of-two scaling is exact away
// from the subnormal boundary, unlike the general scaling in LAPACK xLAED*.
EIGEN_USING_STD(frexp)
EIGEN_USING_STD(ldexp)
// With max|z_i| < 2^zExp, ||2^-zExp z|| is representable even if ||z|| is not.
int zExp = 0;
const RealScalar zmax = zs.cwiseAbs().maxCoeff();
if (zmax > RealScalar(0)) {
frexp(zmax, &zExp);
if (zExp != 0) zs.array() = zs.array().ldexp(-zExp);
}
const RealScalar znorm = zs.stableNorm(); // in [0.5, sqrt(n)): safe
int znormExp = 0;
const RealScalar znormFrac = frexp(znorm, &znormExp);
if (znorm > RealScalar(0)) zs /= znorm;
// Store rho ||z||^2 = rhoMant * 2^rhoTotExp without materializing a possibly
// overflowing product; each mantissa factor lies in [1/4,1).
int rhoExp = 0;
const RealScalar rhoFrac = frexp(rhoW, &rhoExp);
int rhoAdj = 0;
const RealScalar rhoMant = frexp((rhoFrac * znormFrac) * znormFrac, &rhoAdj); // in [0.5, 1), or 0
// Each frexp exponent is bounded by the scalar's exponent range, but their
// accumulation is kept in a wide integer type until the (clamped) narrowing
// to the int that ldexp takes.
const numext::int64_t rhoTotExp =
numext::int64_t(rhoExp) + 2 * (numext::int64_t(zExp) + numext::int64_t(znormExp)) + numext::int64_t(rhoAdj);
// For s = 2^-scaleExp, eig(sD + s rho zz^T) = s eig(D + rho zz^T).
// Choose s so max(||sD||_inf, s rho ||z||^2) lies in [1/2,1), comparing the
// two scales as mantissa-exponent pairs to avoid overflow.
int dExp = 0;
const RealScalar dFrac = frexp(ds.cwiseAbs().maxCoeff(), &dExp);
RealScalar scaledNorm; // max(|d|_inf, rho*||z||^2) * 2^-scaleExp, in [0.5, 1) (or 0 for a zero matrix)
numext::int64_t scaleExpWide;
if (rhoMant == RealScalar(0) ||
(dFrac > RealScalar(0) &&
(numext::int64_t(dExp) > rhoTotExp || (numext::int64_t(dExp) == rhoTotExp && dFrac >= rhoMant)))) {
scaledNorm = dFrac;
scaleExpWide = dExp;
} else {
scaledNorm = rhoMant;
scaleExpWide = rhoTotExp;
}
// Clamp before narrowing: 2^(2^30) is far beyond any scalar's exponent
// range, so the clamp never changes which values are representable.
const numext::int64_t expCap = numext::int64_t(1) << 30;
const int scaleExp = static_cast<int>(numext::maxi(-expCap, numext::mini(expCap, scaleExpWide)));
SpectrumRange spectrumRange = SpectrumRange::Representable;
if (scaleExpWide >= numext::int64_t(std::numeric_limits<RealScalar>::max_exponent) - 1) {
spectrumRange = classifySpectrumRange(dW, rhoW, z);
}
if (scaleExp != 0) ds.array() = ds.array().ldexp(-scaleExp);
// Materialize rho * ||z||^2 only in scaled form: its exponent is <= 0 by the
// choice of scaleExp, so this cannot overflow; it can only underflow when
// the update is negligible against |d|_inf, in which case it deflates below.
rhoW = ldexp(rhoMant, static_cast<int>(numext::maxi(-expCap, rhoTotExp - scaleExpWide)));
// Backward-error budget: dropping a coupling of size <= tol perturbs the
// matrix by O(tol), like LAPACK's xLAED2. Using max(|d|_inf, rho*||z||^2) as
// the scale is the unscaled-problem generalization of xLAED2's criterion: the
// perturbation stays O(eps * (||D|| + rho ||z||^2)), i.e. backward stable in
// the data.
const RealScalar tol = RealScalar(8) * NumTraits<RealScalar>::epsilon() * scaledNorm;
std::vector<Rotation> rotations;
std::vector<bool> deflated;
if (rhoW <= tol) {
deflated.assign(static_cast<std::size_t>(n), true);
} else {
// Negligible z_i leaves d_i as an eigenvalue.
deflated.reserve(static_cast<std::size_t>(n));
for (Index i = 0; i < n; ++i) deflated.push_back(rhoW * numext::abs(zs[i]) <= tol);
// The dropped off-diagonal coupling is |c*s*(ds[i]-ds[p])| <= tol.
Index p = -1;
for (Index i = 0; i < n; ++i) {
if (deflated[static_cast<std::size_t>(i)]) continue;
if (p >= 0) {
const RealScalar r = numext::hypot(zs[p], zs[i]);
const RealScalar c = zs[i] / r, s = zs[p] / r; // zeroes the earlier entry
if (numext::abs(c * s * (ds[i] - ds[p])) <= tol) {
// Rotated diagonal: the deflated position keeps one entry, the
// surviving pole the other; both stay inside [ds[p], ds[i]].
const RealScalar dp = c * c * ds[p] + s * s * ds[i];
const RealScalar di = s * s * ds[p] + c * c * ds[i];
ds[p] = dp;
ds[i] = di;
zs[i] = r;
zs[p] = RealScalar(0);
deflated[static_cast<std::size_t>(p)] = true;
if (computeVectors) rotations.push_back(Rotation{p, i, c, s});
}
}
if (!deflated[static_cast<std::size_t>(i)]) p = i;
}
}
std::vector<Index> sub; // working positions of the surviving poles
for (Index i = 0; i < n; ++i)
if (!deflated[static_cast<std::size_t>(i)]) sub.push_back(i);
const Index m = static_cast<Index>(sub.size());
VectorType lambdaW = ds;
MatrixType subVectors; // m x m secular eigenvectors (in subproblem coordinates)
if (m > 0) {
VectorType delta(m), zeta(m), zeta2(m);
std::vector<Index> shiftIndex(static_cast<std::size_t>(m));
VectorType tau(m);
for (Index a = 0; a < m; ++a) {
delta[a] = ds[sub[static_cast<std::size_t>(a)]];
zeta[a] = zs[sub[static_cast<std::size_t>(a)]];
}
zeta2.array() = zeta.array() * zeta.array();
const RealScalar zeta2sum = zeta2.sum();
// Bisection stops when the bracket has collapsed to adjacent floating-point
// numbers. The backstop covers exponent_range + 2*digits halvings, enough
// to shrink a unit-width bracket to a subnormal root offset and resolve it.
const int digits =
(std::numeric_limits<RealScalar>::digits > 0) ? static_cast<int>(std::numeric_limits<RealScalar>::digits) : 128;
const int expRange = (std::numeric_limits<RealScalar>::max_exponent > std::numeric_limits<RealScalar>::min_exponent)
? static_cast<int>(std::numeric_limits<RealScalar>::max_exponent) -
static_cast<int>(std::numeric_limits<RealScalar>::min_exponent)
: 16 * digits;
const int maxBisect = expRange + 2 * digits + 32;
VectorType lam(m), dsh(m); // dsh is refilled from scratch each root
for (Index k = 0; k < m; ++k) {
// Choose the shift pole and the bracket, entirely in shifted coordinates.
RealScalar lo, hi;
Index shift;
if (k + 1 == m) {
// Last root: it lies in (delta_m, delta_m + rho*|zeta|^2]; never form
// the unshifted right end (it can round to the pole itself).
shift = k;
lo = RealScalar(0);
hi = rhoW * zeta2sum;
} else {
// Interior root: the secular function is increasing between the poles,
// so its sign at the midpoint picks the nearer pole as the shift.
const RealScalar left = delta[k], right = delta[k + 1];
const RealScalar mid = left + (right - left) / RealScalar(2);
if (secular(delta, zeta2, rhoW, mid) > RealScalar(0)) {
shift = k;
lo = RealScalar(0);
hi = mid - left;
} else {
shift = k + 1;
lo = mid - right; // negative
hi = RealScalar(0);
}
}
const RealScalar shiftVal = delta[shift];
dsh.array() = delta.array() - shiftVal;
// Bisection: g(lo) < 0 < g(hi) by the pole signs (for shift = k the
// function tends to -inf as tau -> 0+, for shift = k+1 to +inf as
// tau -> 0-). The endpoints are never evaluated.
RealScalar a0 = lo, b0 = hi;
bool converged = false;
for (int iter = 0; iter < maxBisect; ++iter) {
const RealScalar t = a0 + (b0 - a0) / RealScalar(2);
if (t == a0 || t == b0) {
converged = true; // interval fully resolved
break;
}
if (secular(dsh, zeta2, rhoW, t) > RealScalar(0))
b0 = t;
else
a0 = t;
}
if (!converged) m_info = NoConvergence;
const RealScalar t = a0 + (b0 - a0) / RealScalar(2);
shiftIndex[static_cast<std::size_t>(k)] = shift;
tau[k] = t;
lam[k] = shiftVal + t;
}
// ---- Gu-Eisenstat weights: the z-vector for which lam are exact roots ----
// zhat_i^2 = prod_j (lam_j - delta_i) / (rho * prod_{j != i} (delta_j - delta_i)),
// with every distance lam_j - delta_i formed as (delta_shift(j) - delta_i) + tau_j.
// Numerator and denominator factors are paired so the running product stays O(1).
VectorType zhat(m);
for (Index i = 0; i < m; ++i) {
RealScalar acc = ((delta[shiftIndex[static_cast<std::size_t>(i)]] - delta[i]) + tau[i]) / rhoW;
for (Index j = 0; j < m; ++j) {
if (j == i) continue;
const RealScalar num = (delta[shiftIndex[static_cast<std::size_t>(j)]] - delta[i]) + tau[j];
acc *= num / (delta[j] - delta[i]);
}
zhat[i] = numext::abs(acc) > RealScalar(0) ? RealScalar(numext::sqrt(numext::abs(acc))) : RealScalar(0);
if (zeta[i] < RealScalar(0)) zhat[i] = -zhat[i];
}
// ---- secular eigenvectors from the Gu-Eisenstat weights ----
if (computeVectors) {
subVectors.resize(m, m);
for (Index j = 0; j < m; ++j) {
const Index sj = shiftIndex[static_cast<std::size_t>(j)];
if (tau[j] == RealScalar(0)) {
// The root coincides with its shift pole to working precision (a
// fully underflowed bracket): the eigenvector is that pole's axis.
subVectors.col(j).setZero();
subVectors(sj, j) = RealScalar(1);
continue;
}
// Divide by the pole distances delta_i - lam_j, each formed as
// (delta_i - delta_sj) - tau_j.
subVectors.col(j).array() = zhat.array() / ((delta.array() - delta[sj]) - tau[j]);
subVectors.col(j).stableNormalize();
}
}
for (Index k = 0; k < m; ++k) lambdaW[sub[static_cast<std::size_t>(k)]] = lam[k];
}
std::vector<Index> order;
order.reserve(static_cast<std::size_t>(n));
for (Index i = 0; i < n; ++i) order.push_back(i);
std::stable_sort(order.begin(), order.end(), [&lambdaW](Index a, Index b) { return lambdaW[a] < lambdaW[b]; });
std::vector<Index> subSlot(static_cast<std::size_t>(n), -1);
for (Index a = 0; a < m; ++a) subSlot[static_cast<std::size_t>(sub[static_cast<std::size_t>(a)])] = a;
for (Index t = 0; t < n; ++t) {
const Index w = order[static_cast<std::size_t>(t)];
const Index outCol = negated ? n - 1 - t : t;
m_eivalues[outCol] = negated ? -lambdaW[w] : lambdaW[w];
if (!computeVectors) continue;
VectorType wvec = VectorType::Zero(n);
const Index slot = subSlot[static_cast<std::size_t>(w)];
if (slot < 0) {
wvec[w] = RealScalar(1);
} else {
for (Index a = 0; a < m; ++a) wvec[sub[static_cast<std::size_t>(a)]] = subVectors(a, slot);
}
// w <- G*w with G = [[c, s], [-s, c]]: for exactly equal poles this maps the
// deflated unit vector e_p to (c, -s) = (z_i, -z_p)/r, the exact eigenvector
// of the 2x2 block orthogonal to the weight vector.
for (auto it = rotations.rbegin(); it != rotations.rend(); ++it) {
const RealScalar wi = wvec[it->i], wj = wvec[it->j];
wvec[it->i] = it->c * wi + it->s * wj;
wvec[it->j] = -it->s * wi + it->c * wj;
}
for (Index i = 0; i < n; ++i) m_eivec(pi[static_cast<std::size_t>(i)], outCol) = wvec[i];
}
// Undo the problem scaling in one full-range pass. ArrayBase::ldexp handles
// exponents for which 2^scaleExp is not itself representable, preserving
// representable results exactly and saturating genuine overflow to infinity
// (validated below).
if (scaleExp != 0) m_eivalues.array() = m_eivalues.array().ldexp(scaleExp);
// Range classification uses the original data. The normalized secular
// problem can round a root across the maximum-finite boundary before this
// rescaling, so finiteness of the rounded result alone is not evidence that
// the exact spectrum is (or is not) representable.
const Index extreme = negated ? 0 : n - 1;
const RealScalar highest = (std::numeric_limits<RealScalar>::max)();
if (spectrumRange == SpectrumRange::Overflow) {
m_eivalues.setConstant(NumTraits<RealScalar>::quiet_NaN());
m_info = InvalidInput;
} else {
// A proven boundary root and a root the rescaling saturated to infinity both belong at the
// largest finite value: the first is exactly it, the second rounded across it.
if (spectrumRange == SpectrumRange::ExactBoundary || !(numext::isfinite)(m_eivalues[extreme])) {
m_eivalues[extreme] = negated ? -highest : highest;
}
if (spectrumRange == SpectrumRange::Uncertain && m_info == Success) m_info = NoConvergence;
if (!m_eivalues.allFinite() && m_info == Success) m_info = NoConvergence;
}
m_vectorsComputed = computeVectors && m_info != InvalidInput;
m_isInitialized = true;
return *this;
}
} // namespace Eigen
#endif // EIGEN_STRUCTURED_DPR1_EIGEN_SOLVER_H