| // 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 |