blob: 7150f1317add3607b4c9beef0c420dc595106a29 [file] [edit]
// 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>
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 && underflowProbe<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()));
// Deflating equal poles leaves the pole itself: c has multiplicity n - 1 exactly, for either
// sign of rho (rho < 0 puts c + rho*||z||^2 first).
for (Index i = 0; i + 1 < n; ++i) VERIFY_IS_EQUAL(es.eigenvalues()[i], Scalar(3));
DPR1EigenSolver<Scalar> esn(dc, Scalar(-1), z, EigenvaluesOnly);
for (Index i = 1; i < n; ++i) VERIFY_IS_EQUAL(esn.eigenvalues()[i], Scalar(3));
// Exact triples among distinct poles: a pole of multiplicity k stays an eigenvalue >= k - 1 times.
Vec dt(n);
for (Index i = 0; i < n; ++i) {
const Index pole = 1 + i / 3;
dt[i] = Scalar(pole);
}
check_dpr1<Scalar>(dt, Scalar(2), z);
DPR1EigenSolver<Scalar> est(dt, Scalar(2), z, EigenvaluesOnly);
for (Index i = 0; i < n; i += 3) {
const Index multiplicity = numext::mini<Index>(3, n - i);
VERIFY((est.eigenvalues().array() == dt[i]).count() >= multiplicity - 1);
}
}
// A Givens deflation with c = O(eps), s ~ 1 across a wide gap: rho*|z_2| = 10.8 eps and
// |c s (d_2 - d_1)| ~ 3 eps bracket tol = 7.2 eps. The secular equation puts an eigenvalue at
// d_2 - O(eps^2), which the deflation must form as a correction to d_2, not to the far pole d_1:
// d_1 + s^2 (d_2 - d_1) inherits the rounding of d_2 - d_1, up to eps/8 >> eps * d_2.
template <typename Scalar>
void test_dpr1_skewed_deflation() {
using Vec = Matrix<Scalar, Dynamic, 1>;
const Scalar eps = NumTraits<Scalar>::epsilon();
const Scalar small = numext::sqrt(eps) / Scalar(3); // not a power of two, so d_2 - d_1 rounds
Vec d(2), z(2);
d << Scalar(-0.25), small;
z << Scalar(1), Scalar(12) * eps;
check_dpr1<Scalar>(d, Scalar(0.9), z);
DPR1EigenSolver<Scalar> es(d, Scalar(0.9), z, EigenvaluesOnly);
VERIFY(numext::abs(es.eigenvalues()[0] - small) <= Scalar(4) * eps * small);
}
// 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.
template <typename = void>
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.
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).
template <typename = void>
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);
}
template <typename = void>
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): 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).
template <typename = void>
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);
}
// Finite 1x1 input whose only eigenvalue d + rho*z^2 = 2*DBL_MAX overflows: the
// contract for a non-finite computed eigenvalue is InvalidInput with NaN
// eigenvalues, not Success carrying infinity.
template <typename = void>
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), where
// rho*||z||^2 itself overflows.
z << 2.0;
DPR1EigenSolver<double> es2(d, dmax, z);
VERIFY(es2.info() == InvalidInput);
VERIFY((numext::isnan)(es2.eigenvalues()[0]));
// An overflowing rho*||z||^2 is reported even when the spectrum {-1e308, 1.5e308}
// is representable, rather than deflated away into diag(d) with Success.
Vec d2(2), z2(2);
d2 << -1.5e308, 0.0;
z2 << 1e154, 1e154;
DPR1EigenSolver<double> es3(d2, 1.0, z2);
VERIFY(es3.info() == InvalidInput);
// rho == 0 leaves diag(d) whatever the size of z.
Vec d3(2), z3(2);
d3 << 1.0, 2.0;
z3 << 1e308, 1e308;
DPR1EigenSolver<double> es4(d3, 0.0, z3);
VERIFY(es4.info() == Success);
VERIFY_IS_EQUAL(es4.eigenvalues(), d3);
}
// d and rho near DBL_MIN: the problem scaling scales *up* (scaleExp < 0), which
// is exact from anywhere at or above the normal range, and the eigenvalues here
// are all normal. Verified against a dense solve of the exactly upscaled problem.
template <typename = void>
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);
}
}
// A huge diagonal spread makes every z entry individually negligible even though
// rho itself is not: the whole update deflates and m == 0.
template <typename = void>
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_2((test_dpr1_skewed_deflation<float>()));
CALL_SUBTEST_2((test_dpr1_skewed_deflation<double>()));
CALL_SUBTEST_2((test_dpr1_skewed_deflation<long double>()));
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<>());
// Deep bisection, an overflowing spectrum and the bottom of the range.
CALL_SUBTEST_5(test_dpr1_near_deflation_root<>());
CALL_SUBTEST_5(test_dpr1_overflowing_spectrum<>());
CALL_SUBTEST_5(test_dpr1_tiny_scale<>());
}
}