blob: 79f084d8434e72c0887c8cea5c66f71ee6c0cfb6 [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
#include "main.h"
#include "fp_control.h"
#include <contrib/Eigen/StructuredMatrices>
using namespace Eigen;
template <typename Scalar>
EIGEN_DONT_INLINE Scalar dpr1_subnormal_input_probe() {
volatile Scalar denorm = (std::numeric_limits<Scalar>::denorm_min)();
volatile Scalar highest = (std::numeric_limits<Scalar>::max)();
return denorm * highest;
}
template <typename Scalar>
EIGEN_DONT_INLINE Scalar dpr1_underflow_probe() {
volatile Scalar normalMin = (std::numeric_limits<Scalar>::min)();
volatile Scalar oneHalf = Scalar(0.5);
return normalMin * oneHalf;
}
template <typename Scalar>
bool dpr1_preserves_subnormal_inputs() {
return std::numeric_limits<Scalar>::has_denorm == std::denorm_present &&
dpr1_subnormal_input_probe<Scalar>() != Scalar(0);
}
template <typename Scalar>
bool dpr1_has_gradual_underflow() {
return std::numeric_limits<Scalar>::has_denorm == std::denorm_present && dpr1_underflow_probe<Scalar>() != Scalar(0);
}
// Full quality check of one decomposition: eigenvalues against a dense
// reference, residual ||A*V - V*Lambda||, and -- the litmus test for the
// Gu-Eisenstat machinery -- orthogonality ||V^T V - I||, all relative to
// epsilon-scaled bounds.
template <typename Scalar>
void check_dpr1(const Matrix<Scalar, Dynamic, 1>& d, Scalar rho, const Matrix<Scalar, Dynamic, 1>& z) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
const Index n = d.size();
Mat dense = Mat(d.asDiagonal()) + rho * z * z.transpose();
// The natural backward-error scale of the secular formulation is the data
// scale ||D|| + |rho| ||z||^2 (as for LAPACK's merged D&C problems), which can
// exceed ||A|| when d + rho*z*z^T cancels; bounds are relative to it.
Scalar scale = d.cwiseAbs().maxCoeff() + numext::abs(rho) * z.squaredNorm();
if (scale == Scalar(0)) scale = Scalar(1); // zero matrix: absolute bounds
const Scalar tol = Scalar(100) * Scalar(n) * NumTraits<Scalar>::epsilon();
DPR1EigenSolver<Scalar> es(d, rho, z);
VERIFY(es.info() == Success);
const Vec& lambda = es.eigenvalues();
const Mat& V = es.eigenvectors();
// Eigenvalues ascending and matching the dense reference to eps * scale.
for (Index i = 0; i + 1 < n; ++i) VERIFY(lambda[i] <= lambda[i + 1]);
SelfAdjointEigenSolver<Mat> ref(dense, EigenvaluesOnly);
VERIFY((lambda - ref.eigenvalues()).cwiseAbs().maxCoeff() <= tol * scale);
// Residual and orthogonality (the latter is scale-free and holds at O(n*eps)).
VERIFY((dense * V - V * lambda.asDiagonal()).norm() <= tol * scale * Scalar(numext::sqrt(Scalar(n))));
VERIFY((V.transpose() * V - Mat::Identity(n, n)).norm() <= tol);
// The eigenvalues-only path agrees exactly with the full path.
DPR1EigenSolver<Scalar> esv(d, rho, z, EigenvaluesOnly);
VERIFY_IS_EQUAL(esv.eigenvalues(), lambda);
}
template <typename Scalar>
void test_dpr1_random(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
Vec d = Vec::Random(n), z = Vec::Random(n);
check_dpr1<Scalar>(d, Scalar(1), z);
check_dpr1<Scalar>(d, Scalar(-1), z); // negative rho
check_dpr1<Scalar>(d, Scalar(1e-3), z); // weak update
check_dpr1<Scalar>(d, Scalar(50) * Scalar(n), z); // dominant update
}
// Tight clusters and exact duplicates in the diagonal force the Givens
// deflation path (the secular equation requires strictly separated poles).
template <typename Scalar>
void test_dpr1_clustered(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
Vec d(n), z = Vec::Random(n);
for (Index i = 0; i < n; ++i) {
const Index cluster = i / 3, offsetInCluster = i % 3;
d[i] = Scalar(1) + Scalar(cluster) + Scalar(1e-15) * Scalar(offsetInCluster);
}
check_dpr1<Scalar>(d, Scalar(2), z);
// All diagonal entries exactly equal: A = c*I + rho*z*z^T has closed-form
// spectrum {c, ..., c, c + rho*||z||^2}.
Vec dc = Vec::Constant(n, Scalar(3));
check_dpr1<Scalar>(dc, Scalar(1), z);
DPR1EigenSolver<Scalar> es(dc, Scalar(1), z);
VERIFY(numext::abs(es.eigenvalues()[n - 1] - (Scalar(3) + z.squaredNorm())) <=
Scalar(100) * Scalar(n) * NumTraits<Scalar>::epsilon() * (Scalar(3) + z.squaredNorm()));
}
// Zero and tiny z entries exercise the z-deflation path.
template <typename Scalar>
void test_dpr1_sparse_z(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
Vec d = Vec::Random(n);
d.array() += Scalar(2);
Vec z = Vec::Zero(n);
z[1] = Scalar(0.7);
z[n / 2] = Scalar(-0.3);
z[n - 1] = Scalar(1e-30); // far below the deflation threshold
check_dpr1<Scalar>(d, Scalar(1), z);
check_dpr1<Scalar>(d, Scalar(1), Vec(Vec::Zero(n))); // pure diagonal
check_dpr1<Scalar>(d, Scalar(0), Vec(Vec::Random(n))); // rho = 0
}
// Edge sizes and the classic textbook case.
template <typename Scalar>
void test_dpr1_edges() {
typedef Matrix<Scalar, Dynamic, 1> Vec;
Vec d1(1), z1(1);
d1 << Scalar(2);
z1 << Scalar(3);
DPR1EigenSolver<Scalar> es1(d1, Scalar(2), z1);
VERIFY(numext::abs(es1.eigenvalues()[0] - Scalar(20)) <= Scalar(100) * NumTraits<Scalar>::epsilon() * Scalar(20));
Vec d2(2), z2(2);
d2 << Scalar(1), Scalar(2);
z2 << Scalar(1), Scalar(1);
check_dpr1<Scalar>(d2, Scalar(1), z2);
// d_i = i, z = ones/sqrt(n): every interval holds exactly one root.
const Index n = 24;
Vec d(n), z(n);
for (Index i = 0; i < n; ++i) {
d[i] = Scalar(i);
z[i] = Scalar(1);
}
z /= z.norm();
check_dpr1<Scalar>(d, Scalar(1), z);
check_dpr1<Scalar>(d, Scalar(-1), z);
}
// Unsorted input: the solver must sort internally and undo the permutation.
template <typename Scalar>
void test_dpr1_unsorted(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
Vec d = Vec::Random(n), z = Vec::Random(n);
d = d.reverse().eval();
check_dpr1<Scalar>(d, Scalar(1.5), z);
}
// Rank-one matrix (all-zero diagonal): spectrum {0,...,0, rho*||z||^2}.
template <typename Scalar>
void test_dpr1_rank_one(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
Vec z = Vec::Random(n);
check_dpr1<Scalar>(Vec(Vec::Zero(n)), Scalar(2), z);
}
// Wide dynamic range in d and rho scaling.
template <typename Scalar>
void test_dpr1_scaling(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
Vec d = Vec::Random(n), z = Vec::Random(n);
d = d.cwiseProduct(Vec::LinSpaced(n, Scalar(1e-8), Scalar(1e8)));
check_dpr1<Scalar>(d, Scalar(1e6), z);
check_dpr1<Scalar>(d, Scalar(-1e-6), z);
}
// The Gu-Eisenstat litmus test: poles close enough that naive eigenvectors lose
// orthogonality, but far enough apart (well above the deflation threshold) that
// the secular solver must handle them. Gaps span eps^1 to eps^(1/2) scales.
template <typename Scalar>
void test_dpr1_close_undeflated(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
const Scalar eps = NumTraits<Scalar>::epsilon();
Vec d(n), z(n);
Scalar gap = Scalar(100) * eps;
for (Index i = 0; i < n; ++i) {
d[i] = Scalar(1) + Scalar(i) * gap;
gap *= Scalar(4); // gaps sweep 1e2*eps ... ~1e6*eps
if (gap > numext::sqrt(eps)) gap = Scalar(100) * eps;
z[i] = Scalar(0.3) + Scalar(0.5) * Scalar(i % 7) / Scalar(7);
}
check_dpr1<Scalar>(d, Scalar(1), z);
check_dpr1<Scalar>(d, Scalar(-1), z);
}
// stableNorm keeps huge z representable: with float z ~ 2e19, z.squaredNorm()
// would overflow but rho*||z||^2 is a perfectly ordinary number.
void test_dpr1_huge_z() {
typedef Matrix<float, Dynamic, 1> Vec;
Vec d(1), z(1);
d << 1.0f;
z << 2e19f;
DPR1EigenSolver<float> es(d, 1e-30f, z);
VERIFY(es.info() == Success);
// Form rho*z^2 = 4e8 one factor at a time: the literal 4e38 already overflows
// float (an infinite `expected` would make the bound below vacuous).
const float expected = 1.0f + (1e-30f * 2e19f) * 2e19f;
VERIFY((numext::isfinite)(expected));
// Loose bound: expected ~ 4e8 is dominated by the rank-one term, so only its
// leading digits survive in float.
const float tol = 1000.0f * NumTraits<float>::epsilon(); // ~1.2e-4
VERIFY(numext::abs(es.eigenvalues()[0] - expected) <= tol * expected);
// A spectrum that genuinely overflows (top eigenvalue ~1e320 here) must be
// reported, not silently deflated. Note rho*||z||^2 overflowing is *not* by
// itself invalid (see test_dpr1_overflowing_update_representable_spectrum);
// this input is rejected because the eigenvalue itself is not representable.
DPR1EigenSolver<double> ov((Matrix<double, Dynamic, 1>(1) << 1.0).finished(), 1e200,
(Matrix<double, Dynamic, 1>(1) << 1e60).finished());
VERIFY(ov.info() == InvalidInput);
}
// Non-finite input must be rejected up front (it would otherwise break the
// sorting comparator and silently deflate everything).
void test_dpr1_nonfinite() {
typedef Matrix<double, Dynamic, 1> Vec;
Vec d = Vec::Random(6), z = Vec::Random(6);
Vec dn = d;
dn[2] = std::numeric_limits<double>::quiet_NaN();
DPR1EigenSolver<double> e1(dn, 1.0, z);
VERIFY(e1.info() == InvalidInput);
Vec zn = z;
zn[4] = std::numeric_limits<double>::infinity();
DPR1EigenSolver<double> e2(d, 1.0, zn);
VERIFY(e2.info() == InvalidInput);
DPR1EigenSolver<double> e3(d, std::numeric_limits<double>::quiet_NaN(), z);
VERIFY(e3.info() == InvalidInput);
const Vec empty;
DPR1EigenSolver<double> e4(empty, std::numeric_limits<double>::quiet_NaN(), empty);
VERIFY(e4.info() == InvalidInput);
DPR1EigenSolver<double> e5(empty, std::numeric_limits<double>::infinity(), empty);
VERIFY(e5.info() == InvalidInput);
}
void test_dpr1_invalid_options() {
typedef Matrix<double, Dynamic, 1> Vec;
const Vec d = Vec::LinSpaced(3, 1.0, 3.0);
const Vec z = Vec::Ones(3);
DPR1EigenSolver<double> es;
VERIFY_RAISES_ASSERT((es.compute(d, 1.0, z, ComputeEigenvectors | EigenvaluesOnly)));
VERIFY_RAISES_ASSERT((es.compute(d, 1.0, z, ComputeFullU)));
}
// Regression (MR 2694 review): poles spanning the full exponent range with a huge but representable
// update. Pole differences (d_1 - d_0 = 1.5*DBL_MAX) overflow unless the problem is rescaled by an
// exact power of two; the solver used to report Success with an infinite eigenvalue. The reference is
// a dense solve of the rescaled problem 2^-e * (d, rho), whose spectrum is exactly 2^-e times the
// original.
void test_dpr1_full_range_poles() {
typedef Matrix<double, Dynamic, 1> Vec;
typedef Matrix<double, Dynamic, Dynamic> Mat;
const double dmax = (std::numeric_limits<double>::max)();
Vec d(2), z(2);
d << -0.75 * dmax, 0.75 * dmax;
z << 1.0, 1.0;
const double rho = 1e300;
DPR1EigenSolver<double> es(d, rho, z);
VERIFY(es.info() == Success);
VERIFY(es.eigenvalues().allFinite());
const int e = 1024; // 2^-e maps 0.75*DBL_MAX into [0.5, 1)
Vec dS(2);
dS << std::ldexp(d[0], -e), std::ldexp(d[1], -e);
const double rhoS = std::ldexp(rho, -e);
Mat dense = Mat(dS.asDiagonal()) + rhoS * z * z.transpose();
SelfAdjointEigenSolver<Mat> ref(dense, EigenvaluesOnly);
const double scaleS = dS.cwiseAbs().maxCoeff() + rhoS * z.squaredNorm();
const double tol = 100.0 * NumTraits<double>::epsilon();
Vec lambdaS(2);
lambdaS << std::ldexp(es.eigenvalues()[0], -e), std::ldexp(es.eigenvalues()[1], -e);
VERIFY((lambdaS - ref.eigenvalues()).cwiseAbs().maxCoeff() <= tol * scaleS);
// Eigenvectors are scale-invariant: check them on the rescaled matrix.
const Mat& V = es.eigenvectors();
VERIFY((dense * V - V * lambdaS.asDiagonal()).norm() <= tol * scaleS);
VERIFY((V.transpose() * V - Mat::Identity(2, 2)).norm() <= tol);
}
// Regression (MR 2694 review): z entries at the top of the range make ||z||
// itself overflow, but the update rho*z*z^T (rank-one eigenvalue ~6.46e306) is
// perfectly representable; the solver used to return InvalidInput. It must
// instead normalize z by an exact power of two, 2^-p z with rho 2^(2p), which
// leaves the matrix identical -- the same rewrite gives the dense reference.
void test_dpr1_huge_z_norm() {
if (!dpr1_preserves_subnormal_inputs<double>()) return;
typedef Matrix<double, Dynamic, 1> Vec;
typedef Matrix<double, Dynamic, Dynamic> Mat;
const double dmax = (std::numeric_limits<double>::max)();
Vec d(2), z(2);
d << 0.0, 0.0;
z << dmax, dmax;
const double rho = 1e-310; // subnormal, so rho * 2^(2p) stays finite
DPR1EigenSolver<double> es(d, rho, z);
VERIFY(es.info() == Success);
const int p = 1024; // 2^-p maps DBL_MAX into [0.5, 1)
Vec zS(2);
zS << std::ldexp(z[0], -p), std::ldexp(z[1], -p);
const double rhoS = std::ldexp(rho, 2 * p); // exact: subnormal scaled up
Mat dense = Mat(d.asDiagonal()) + rhoS * zS * zS.transpose();
SelfAdjointEigenSolver<Mat> ref(dense, EigenvaluesOnly);
const double scale = rhoS * zS.squaredNorm(); // the rank-one eigenvalue, ~6.46e306
const double tol = 100.0 * NumTraits<double>::epsilon();
VERIFY((es.eigenvalues() - ref.eigenvalues()).cwiseAbs().maxCoeff() <= tol * scale);
VERIFY(numext::abs(es.eigenvalues()[1] - scale) <= tol * scale);
}
// Regression (MR 2694 review): a z entry just above the deflation threshold
// puts the secular root ~rho*z_0^2 ~ 6.3e-30 from its pole; bisecting the O(1)
// bracket down to it takes ~150 halvings, beyond the old 2*digits+32 iteration
// cap, and used to report NoConvergence (check_dpr1 requires Success).
void test_dpr1_near_deflation_root() {
typedef Matrix<double, Dynamic, 1> Vec;
Vec d(2), z(2);
d << 0.0, 1.0;
z << 16.0 * NumTraits<double>::epsilon(), 1.0;
check_dpr1<double>(d, 1.0, z);
}
// Regression (MR 2694 review): finite 1x1 input whose only eigenvalue
// d + rho*z^2 = 2*DBL_MAX overflows. The documented contract for a spectrum
// that is not representable is InvalidInput, not Success carrying infinity.
void test_dpr1_overflowing_spectrum() {
typedef Matrix<double, Dynamic, 1> Vec;
const double dmax = (std::numeric_limits<double>::max)();
Vec d(1), z(1);
d << dmax;
z << 1.0;
DPR1EigenSolver<double> es(d, dmax, z);
VERIFY(es.info() == InvalidInput);
VERIFY((numext::isnan)(es.eigenvalues()[0]));
// Same with the overflow amplified through z (eigenvalue = 5*DBL_MAX): the
// internal rescaling saturates the unrepresentable eigenvalue to infinity,
// which the contract maps to InvalidInput + NaN rather than Success + inf.
z << 2.0;
DPR1EigenSolver<double> es2(d, dmax, z);
VERIFY(es2.info() == InvalidInput);
VERIFY((numext::isnan)(es2.eigenvalues()[0]));
}
// Regression (MR 2694 review): the normalized secular problem can round a root across the
// maximum-finite boundary before it is scaled back, so that root's finiteness cannot classify the
// exact spectrum. The first update has lambda = DBL_MAX - 19/256 ulp, which rounds to DBL_MAX; the
// second has lambda = DBL_MAX + 3739/2048 ulp and genuinely overflows. Constants are assembled from
// their exact significands, hexadecimal float literals not being in the C++14 baseline.
void test_dpr1_maximum_range_rounding() {
typedef Matrix<double, Dynamic, 1> Vec;
const double dmax = (std::numeric_limits<double>::max)();
Vec d(1), z(1);
d << -dmax;
z << 31.0 / 8.0;
const double rhoRepresentable = std::ldexp(4798840810018093.0, 969);
DPR1EigenSolver<double> representable(d, rhoRepresentable, z);
VERIFY(representable.info() == Success);
VERIFY_IS_EQUAL(representable.eigenvalues()[0], dmax);
d << dmax;
DPR1EigenSolver<double> representableNegative(d, -rhoRepresentable, z);
VERIFY(representableNegative.info() == Success);
VERIFY_IS_EQUAL(representableNegative.eigenvalues()[0], -dmax);
d << -dmax;
z << 65.0 / 2.0;
const double rhoOverflow = std::ldexp(8732186543767835.0, 962);
DPR1EigenSolver<double> overflow(d, rhoOverflow, z);
VERIFY(overflow.info() == InvalidInput);
VERIFY((numext::isnan)(overflow.eigenvalues()[0]));
d << dmax;
DPR1EigenSolver<double> overflowNegative(d, -rhoOverflow, z);
VERIFY(overflowNegative.info() == InvalidInput);
VERIFY((numext::isnan)(overflowNegative.eigenvalues()[0]));
}
// Multiple exactly representable endpoint terms must be certifiable together:
// rho*z_i^2/(max-d_i) = 1/2 for each i, so the spectrum is exactly {0, max}.
template <typename Scalar>
void test_dpr1_exact_endpoint_sum() {
typedef Matrix<Scalar, Dynamic, 1> Vec;
using std::nextafter;
const Scalar highest = (std::numeric_limits<Scalar>::max)();
const Scalar boundaryRho = highest / Scalar(2);
const Vec d = Vec::Zero(2);
const Vec z = Vec::Ones(2);
DPR1EigenSolver<Scalar> positive(d, boundaryRho, z);
VERIFY(positive.info() == Success);
VERIFY_IS_EQUAL(positive.eigenvalues()[0], Scalar(0));
VERIFY_IS_EQUAL(positive.eigenvalues()[1], highest);
DPR1EigenSolver<Scalar> negative(d, -boundaryRho, z);
VERIFY(negative.info() == Success);
VERIFY_IS_EQUAL(negative.eigenvalues()[0], -highest);
VERIFY_IS_EQUAL(negative.eigenvalues()[1], Scalar(0));
const Vec d4 = Vec::Zero(4);
const Vec z4 = Vec::Ones(4);
DPR1EigenSolver<Scalar> fourTerms(d4, highest / Scalar(4), z4);
VERIFY(fourTerms.info() == Success);
VERIFY_IS_EQUAL(fourTerms.eigenvalues()[3], highest);
DPR1EigenSolver<Scalar> below(d, nextafter(boundaryRho, Scalar(0)), z);
VERIFY(below.info() == Success);
VERIFY(below.eigenvalues().allFinite());
DPR1EigenSolver<Scalar> above(d, nextafter(boundaryRho, highest), z);
VERIFY(above.info() == InvalidInput);
// The exact top eigenvalue is highest + min. Normalizing the endpoint
// denominator drops that tiny difference, so this case must remain
// conservatively unresolved instead of reusing the exact-equality path.
const Vec minDiagonal = Vec::Constant(2, (std::numeric_limits<Scalar>::min)());
DPR1EigenSolver<Scalar> unresolved(minDiagonal, boundaryRho, z);
VERIFY(unresolved.info() != Success);
}
// Regression (MR 2694 review): rho*||z||^2 = 2*max overflows, but the single eigenvalue
// d + rho*z^2 = -max + 2*max = max is representable. The rescaling must come from component exponents
// alone, never materializing rho*||z||^2 at full scale. The secular root sits exactly on the far end
// of its bisection bracket, so the computed eigenvalue lands within a couple of ulps of max rather
// than exactly at it.
template <typename Scalar>
void test_dpr1_overflowing_update_representable_spectrum() {
typedef Matrix<Scalar, Dynamic, 1> Vec;
const Scalar dmax = (std::numeric_limits<Scalar>::max)();
const Scalar eps = NumTraits<Scalar>::epsilon();
Vec d(1), z(1);
d << -dmax;
z << Scalar(2);
DPR1EigenSolver<Scalar> es(d, dmax / Scalar(2), z);
VERIFY(es.info() == Success);
const Scalar lam = es.eigenvalues()[0];
VERIFY((numext::isfinite)(lam));
VERIFY(numext::abs(lam - dmax) <= Scalar(4) * eps * dmax);
// n = 1: the eigenvector is the coordinate axis, exactly (up to sign).
VERIFY(numext::abs(es.eigenvectors()(0, 0)) == Scalar(1));
}
// Regression (MR 2694 review), n > 1 version: poles near -0.75*DBL_MAX with
// rho*||z||^2 ~ 1.2*DBL_MAX (overflowing if materialized) but every eigenvalue
// representable -- the top root lands near +0.4*DBL_MAX, the rest interlace
// the poles. Verified against a dense solve of the exactly rescaled problem,
// whose spectrum is exactly 2^-e times the original.
void test_dpr1_huge_representable_spectrum() {
typedef Matrix<double, Dynamic, 1> Vec;
typedef Matrix<double, Dynamic, Dynamic> Mat;
const Index n = 8;
const double dmax = (std::numeric_limits<double>::max)();
Vec d(n), z(n);
for (Index i = 0; i < n; ++i) {
d[i] = -dmax * (0.70 + 0.01 * double(i));
z[i] = 1.0 + double(i) / 8.0; // ||z||^2 ~ 18.6
}
const double rho = dmax / 16;
DPR1EigenSolver<double> es(d, rho, z);
VERIFY(es.info() == Success);
VERIFY(es.eigenvalues().allFinite());
const int e = 1025; // 2^-e maps every |d_i| and rho*||z||^2 below 1
Vec dS(n), lambdaS(n);
for (Index i = 0; i < n; ++i) {
dS[i] = std::ldexp(d[i], -e);
lambdaS[i] = std::ldexp(es.eigenvalues()[i], -e);
}
const double rhoS = std::ldexp(rho, -e);
Mat dense = Mat(dS.asDiagonal()) + rhoS * z * z.transpose();
SelfAdjointEigenSolver<Mat> ref(dense, EigenvaluesOnly);
const double scaleS = dS.cwiseAbs().maxCoeff() + rhoS * z.squaredNorm();
const double tol = 100.0 * double(n) * NumTraits<double>::epsilon();
VERIFY((lambdaS - ref.eigenvalues()).cwiseAbs().maxCoeff() <= tol * scaleS);
// Eigenvectors are scale-invariant: check them on the rescaled matrix.
const Mat& V = es.eigenvectors();
VERIFY((dense * V - V * lambdaS.asDiagonal()).norm() <= tol * scaleS * numext::sqrt(double(n)));
VERIFY((V.transpose() * V - Mat::Identity(n, n)).norm() <= tol);
}
// Mirror case at the bottom of the range: d and rho near DBL_MIN. The same
// exact power-of-two machinery scales *up* (scaleExp < 0), which needs no
// separate underflow handling -- scaling up from anywhere at or above the
// normal range is exact, and the eigenvalues here are all normal. Verified
// against a dense solve of the exactly upscaled problem.
void test_dpr1_tiny_scale() {
typedef Matrix<double, Dynamic, 1> Vec;
typedef Matrix<double, Dynamic, Dynamic> Mat;
const Index n = 8;
const double dmin = (std::numeric_limits<double>::min)();
Vec d(n), z(n);
for (Index i = 0; i < n; ++i) {
d[i] = dmin * double(1 + i);
z[i] = 1.0 + double(i) / 8.0;
}
const double rho = 3 * dmin;
DPR1EigenSolver<double> es(d, rho, z);
VERIFY(es.info() == Success);
const int e = -1020; // upscale by 2^1020: exact, brings the data to O(1)
Vec dS(n), lambdaS(n);
for (Index i = 0; i < n; ++i) {
dS[i] = std::ldexp(d[i], -e);
lambdaS[i] = std::ldexp(es.eigenvalues()[i], -e); // exact: all normal
}
const double rhoS = std::ldexp(rho, -e);
Mat dense = Mat(dS.asDiagonal()) + rhoS * z * z.transpose();
SelfAdjointEigenSolver<Mat> ref(dense, EigenvaluesOnly);
const double scaleS = dS.cwiseAbs().maxCoeff() + rhoS * z.squaredNorm();
const double tol = 100.0 * double(n) * NumTraits<double>::epsilon();
VERIFY((lambdaS - ref.eigenvalues()).cwiseAbs().maxCoeff() <= tol * scaleS);
VERIFY((dense * es.eigenvectors() - es.eigenvectors() * lambdaS.asDiagonal()).norm() <=
tol * scaleS * numext::sqrt(double(n)));
if (dpr1_preserves_subnormal_inputs<double>() && dpr1_has_gradual_underflow<double>()) {
// Deep-subnormal rank-one update: spectrum {0 x7, rho*||z||^2 = 2^-1065}.
// The result is rounded onto the subnormal grid on the way back down, so the
// bound is a few subnormal ulps (denorm_min), not a relative one.
const double dn = (std::numeric_limits<double>::denorm_min)();
Vec z1 = Vec::Ones(n);
const double rho1 = 64 * dn;
DPR1EigenSolver<double> sub(Vec(Vec::Zero(n)), rho1, z1);
VERIFY(sub.info() == Success);
VERIFY(numext::abs(sub.eigenvalues()[n - 1] - double(n) * rho1) <= 8 * dn);
VERIFY(sub.eigenvalues().head(n - 1).cwiseAbs().maxCoeff() == 0.0);
}
}
void test_dpr1_ftz_mode() {
const double underflowBefore = dpr1_underflow_probe<double>();
bool flushToZeroSupported = false;
{
ScopedFlushToZero flushToZero;
flushToZeroSupported = flushToZero.isSupported();
if (flushToZeroSupported) {
VERIFY_IS_EQUAL(dpr1_underflow_probe<double>(), 0.0);
test_dpr1_edges<double>();
}
}
if (flushToZeroSupported) VERIFY_IS_EQUAL(dpr1_underflow_probe<double>(), underflowBefore);
}
// A huge diagonal spread makes every z entry individually negligible even though
// rho itself is not: the whole update deflates and m == 0.
void test_dpr1_all_deflated() {
typedef Matrix<double, Dynamic, 1> Vec;
Vec d(4), z(4);
d << 0.0, 1e17, 2e17, 3e17;
z << 0.5, 0.5, 0.5, 0.5;
check_dpr1<double>(d, 1.0, z);
}
EIGEN_DECLARE_TEST(structured_dpr1) {
for (int i = 0; i < g_repeat; ++i) {
CALL_SUBTEST_1((test_dpr1_random<double>(1)));
CALL_SUBTEST_1((test_dpr1_random<double>(2)));
CALL_SUBTEST_1((test_dpr1_random<double>(3)));
CALL_SUBTEST_1((test_dpr1_random<double>(10)));
CALL_SUBTEST_1((test_dpr1_random<double>(33)));
CALL_SUBTEST_1((test_dpr1_random<double>(64)));
CALL_SUBTEST_1((test_dpr1_random<float>(12)));
CALL_SUBTEST_1((test_dpr1_random<float>(40)));
CALL_SUBTEST_1((test_dpr1_random<long double>(12)));
CALL_SUBTEST_2((test_dpr1_clustered<double>(12)));
CALL_SUBTEST_2((test_dpr1_clustered<double>(30)));
CALL_SUBTEST_2((test_dpr1_clustered<float>(15)));
CALL_SUBTEST_2((test_dpr1_sparse_z<double>(16)));
CALL_SUBTEST_2((test_dpr1_sparse_z<float>(9)));
CALL_SUBTEST_2((test_dpr1_rank_one<double>(20)));
CALL_SUBTEST_2((test_dpr1_rank_one<float>(11)));
CALL_SUBTEST_2((test_dpr1_clustered<long double>(12)));
CALL_SUBTEST_2((test_dpr1_sparse_z<long double>(9)));
CALL_SUBTEST_3((test_dpr1_edges<double>()));
CALL_SUBTEST_3((test_dpr1_edges<float>()));
CALL_SUBTEST_3((test_dpr1_unsorted<double>(17)));
CALL_SUBTEST_3((test_dpr1_scaling<double>(14)));
CALL_SUBTEST_3((test_dpr1_edges<long double>()));
CALL_SUBTEST_3((test_dpr1_scaling<long double>(14)));
CALL_SUBTEST_3(test_dpr1_invalid_options());
// Gu-Eisenstat litmus (close but undeflated poles), orthogonality growth at
// larger n, extreme scales, non-finite rejection, full-deflation pin.
CALL_SUBTEST_4((test_dpr1_close_undeflated<double>(24)));
CALL_SUBTEST_4((test_dpr1_close_undeflated<float>(16)));
CALL_SUBTEST_4((test_dpr1_random<double>(128)));
CALL_SUBTEST_4((test_dpr1_close_undeflated<long double>(16)));
CALL_SUBTEST_4(test_dpr1_huge_z());
CALL_SUBTEST_4(test_dpr1_nonfinite());
CALL_SUBTEST_4(test_dpr1_all_deflated());
// MR 2694 review regressions: exponent-range extremes and deep bisection.
CALL_SUBTEST_5(test_dpr1_full_range_poles());
CALL_SUBTEST_5(test_dpr1_huge_z_norm());
CALL_SUBTEST_5(test_dpr1_near_deflation_root());
CALL_SUBTEST_5(test_dpr1_overflowing_spectrum());
CALL_SUBTEST_5(test_dpr1_maximum_range_rounding());
CALL_SUBTEST_5((test_dpr1_exact_endpoint_sum<float>()));
CALL_SUBTEST_5((test_dpr1_exact_endpoint_sum<double>()));
CALL_SUBTEST_5((test_dpr1_exact_endpoint_sum<long double>()));
CALL_SUBTEST_5((test_dpr1_overflowing_update_representable_spectrum<double>()));
CALL_SUBTEST_5((test_dpr1_overflowing_update_representable_spectrum<float>()));
CALL_SUBTEST_5((test_dpr1_overflowing_update_representable_spectrum<long double>()));
CALL_SUBTEST_5(test_dpr1_huge_representable_spectrum());
CALL_SUBTEST_5(test_dpr1_tiny_scale());
CALL_SUBTEST_5(test_dpr1_ftz_mode());
}
}