blob: 2c915f15336a784e59c755ec66272b6412b9884d [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 <unsupported/Eigen/StructuredMatrices>
using namespace Eigen;
// Well-conditioned random operator: diagonal bounded away from zero and a
// contractive correction, so the capacitance matrix is safely invertible.
template <typename Scalar>
void random_dplr(Index n, Index k, Matrix<Scalar, Dynamic, 1>& d, Matrix<Scalar, Dynamic, Dynamic>& U,
Matrix<Scalar, Dynamic, Dynamic>& V) {
typedef typename NumTraits<Scalar>::Real RealScalar;
d = Matrix<Scalar, Dynamic, 1>::Random(n);
d.array() += Scalar(RealScalar(3));
U = Scalar(RealScalar(0.5)) * Matrix<Scalar, Dynamic, Dynamic>::Random(n, k);
V = Scalar(RealScalar(0.5)) * Matrix<Scalar, Dynamic, Dynamic>::Random(n, k);
}
// Reference dense matrix built independently, entry by entry.
template <typename Scalar>
Matrix<Scalar, Dynamic, Dynamic> reference_dplr(const Matrix<Scalar, Dynamic, 1>& d,
const Matrix<Scalar, Dynamic, Dynamic>& U,
const Matrix<Scalar, Dynamic, Dynamic>& V) {
const Index n = d.size();
Matrix<Scalar, Dynamic, Dynamic> dense(n, n);
for (Index j = 0; j < n; ++j)
for (Index i = 0; i < n; ++i) {
Scalar s = (i == j) ? d[i] : Scalar(0);
for (Index t = 0; t < U.cols(); ++t) s += U(i, t) * numext::conj(V(j, t));
dense(i, j) = s;
}
return dense;
}
template <typename Scalar>
void test_dplr_product(Index n, Index k) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec d;
Mat U, V;
random_dplr<Scalar>(n, k, d, U, V);
DiagonalPlusLowRank<Scalar> A(d, U, V);
Mat dense = reference_dplr<Scalar>(d, U, V);
VERIFY_IS_EQUAL(A.correctionRank(), k);
Mat Ad = A;
VERIFY_IS_APPROX(Ad, dense);
for (Index t = 0; t < 5; ++t) {
Index i = internal::random<Index>(0, n - 1), j = internal::random<Index>(0, n - 1);
VERIFY_IS_APPROX(A.coeff(i, j), dense(i, j));
}
Vec x = Vec::Random(n);
VERIFY_IS_APPROX((A * x).eval(), (dense * x).eval());
Mat X = Mat::Random(n, 3);
VERIFY_IS_APPROX((A * X).eval(), (dense * X).eval());
// Accumulation form exercised by the iterative solvers.
Vec y = Vec::Random(n);
Vec y0 = y;
y.noalias() += A * x;
VERIFY_IS_APPROX(y, (y0 + dense * x).eval());
// Dimension mismatches are caught when the product expression is formed.
VERIFY_RAISES_ASSERT(A * Vec(Vec::Random(n + 1)));
}
template <typename Scalar>
void test_dplr_transpose(Index n, Index k) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec d;
Mat U, V;
random_dplr<Scalar>(n, k, d, U, V);
DiagonalPlusLowRank<Scalar> A(d, U, V);
Mat dense = reference_dplr<Scalar>(d, U, V);
Mat Td = A.transpose();
VERIFY_IS_APPROX(Td, Mat(dense.transpose()));
Mat Ad = A.adjoint();
VERIFY_IS_APPROX(Ad, Mat(dense.adjoint()));
Mat Kd = A.conjugate();
VERIFY_IS_APPROX(Kd, Mat(dense.conjugate()));
Vec x = Vec::Random(n);
VERIFY_IS_APPROX((A.transpose() * x).eval(), (dense.transpose() * x).eval());
VERIFY_IS_APPROX((A.adjoint() * x).eval(), (dense.adjoint() * x).eval());
}
template <typename Scalar>
void test_dplr_solve(Index n, Index k) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec d;
Mat U, V;
random_dplr<Scalar>(n, k, d, U, V);
DiagonalPlusLowRank<Scalar> A(d, U, V);
Mat dense = reference_dplr<Scalar>(d, U, V);
Vec b = Vec::Random(n);
Vec x = A.solve(b);
VERIFY_IS_APPROX((dense * x).eval(), b);
VERIFY_IS_APPROX(x, dense.partialPivLu().solve(b).eval());
Mat B = Mat::Random(n, 3);
Mat X = A.solve(B);
VERIFY_IS_APPROX(X, dense.partialPivLu().solve(B).eval());
Mat empty_rhs(n, 0);
Mat empty_solution = A.solve(empty_rhs);
VERIFY_IS_EQUAL(empty_solution.rows(), n);
VERIFY_IS_EQUAL(empty_solution.cols(), 0);
}
template <typename Scalar>
void test_dplr_inverse_determinant(Index n, Index k) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec d;
Mat U, V;
random_dplr<Scalar>(n, k, d, U, V);
DiagonalPlusLowRank<Scalar> A(d, U, V);
Mat dense = reference_dplr<Scalar>(d, U, V);
// The inverse is itself diagonal-plus-low-rank; materialize and check.
DiagonalPlusLowRank<Scalar> Ainv = A.inverse();
VERIFY_IS_EQUAL(Ainv.correctionRank(), k);
Mat invd = Ainv;
VERIFY_IS_APPROX((invd * dense).eval(), Mat(Mat::Identity(n, n)));
// Inverting twice returns to the original operator.
Mat back = Ainv.inverse();
VERIFY_IS_APPROX(back, dense);
VERIFY_IS_APPROX(A.determinant(), dense.determinant());
}
template <typename Scalar>
void test_dplr_matrix_free_gmres(Index n, Index k) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec d;
Mat U, V;
random_dplr<Scalar>(n, k, d, U, V);
DiagonalPlusLowRank<Scalar> A(d, U, V);
Mat dense = reference_dplr<Scalar>(d, U, V);
Vec b = Vec::Random(n);
GMRES<DiagonalPlusLowRank<Scalar>, IdentityPreconditioner> gmres;
gmres.setTolerance(RealScalar(1e-12));
gmres.compute(A);
Vec x = gmres.solve(b);
VERIFY(gmres.info() == Success);
const RealScalar tol = RealScalar(5e6) * NumTraits<RealScalar>::epsilon(); // ~1e-9 in double
VERIFY((dense * x - b).norm() <= tol * b.norm());
}
// The products carry the default product tag, so an assignment whose right-hand
// side aliases the destination materializes the product into a temporary exactly
// like a dense product would -- including the aliasing that the former
// is_same_dense check could not see: overlapping views and right-hand-side
// expressions referencing the destination.
template <typename Scalar>
void test_dplr_aliased_product(Index n, Index k) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
// Pin the routing: the structured products must dispatch through the shared
// structured_product_impl.
STATIC_CHECK(
(std::is_base_of<internal::structured_product_impl<DiagonalPlusLowRank<Scalar>, Vec>,
internal::generic_product_impl<DiagonalPlusLowRank<Scalar>, Vec, internal::StructuredShape,
DenseShape, GemvProduct>>::value));
Vec d;
Mat U, V;
random_dplr<Scalar>(n, k, d, U, V);
DiagonalPlusLowRank<Scalar> A(d, U, V);
Mat dense = reference_dplr<Scalar>(d, U, V);
Vec x = Vec::Random(n);
Vec y = x;
y = A * y;
VERIFY_IS_APPROX(y, (dense * x).eval());
y = x;
y += A * y;
VERIFY_IS_APPROX(y, (x + dense * x).eval());
// Right-hand-side expression referencing the destination.
y = x;
y = A * (y + Vec::Ones(n));
VERIFY_IS_APPROX(y, (dense * (x + Vec::Ones(n))).eval());
// Overlapping views of a shared buffer, in both directions.
Vec w = Vec::Random(n + 1);
Vec w0 = w;
w.tail(n) = A * w.head(n);
VERIFY_IS_APPROX(w.tail(n).eval(), (dense * w0.head(n)).eval());
w = w0;
w.head(n) = A * w.tail(n);
VERIFY_IS_APPROX(w.head(n).eval(), (dense * w0.tail(n)).eval());
}
// Product expressions must nest the structured operand by value: a
// delayed-evaluated expression has to outlive the temporary operator it was
// built from. The static check pins the value nesting; the behavioral check
// would read freed memory if the product held a reference instead.
template <typename Scalar>
void test_dplr_delayed_product(Index n, Index k) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
STATIC_CHECK(!std::is_reference<typename internal::ref_selector<DiagonalPlusLowRank<Scalar>>::type>::value);
Vec d;
Mat U, V;
random_dplr<Scalar>(n, k, d, U, V);
DiagonalPlusLowRank<Scalar> A(d, U, V);
Mat dense = reference_dplr<Scalar>(d, U, V);
Vec x = Vec::Random(n);
auto expr = A.adjoint() * x; // the adjoint temporary dies with the full expression
Vec scribble = Vec::Random(2 * n); // reuses the temporary's freed heap storage
Vec y = expr;
VERIFY_IS_APPROX(y, (dense.adjoint() * x).eval());
VERIFY_IS_EQUAL(scribble.size(), 2 * n); // keep the scribble alive across the evaluation
}
// Mixed-scalar products: a real operator applied to a complex right-hand side
// (and a complex operator applied to a real one) promotes to the complex product
// scalar, so alpha, the workspaces and the accumulation must run in the promoted
// type rather than the operator scalar.
template <typename RealScalar>
void test_dplr_mixed_scalar(Index n, Index k) {
typedef std::complex<RealScalar> Complex;
typedef Matrix<RealScalar, Dynamic, 1> RVec;
typedef Matrix<RealScalar, Dynamic, Dynamic> RMat;
typedef Matrix<Complex, Dynamic, 1> CVec;
typedef Matrix<Complex, Dynamic, Dynamic> CMat;
RVec d;
RMat U, V;
random_dplr<RealScalar>(n, k, d, U, V);
DiagonalPlusLowRank<RealScalar> A(d, U, V);
CMat dense = reference_dplr<RealScalar>(d, U, V).template cast<Complex>();
CVec x = CVec::Random(n);
CVec y = A * x;
VERIFY_IS_APPROX(y, (dense * x).eval());
CVec y0 = CVec::Random(n);
y = y0;
y.noalias() += A * x;
VERIFY_IS_APPROX(y, (y0 + dense * x).eval());
CVec dc;
CMat Uc, Vc;
random_dplr<Complex>(n, k, dc, Uc, Vc);
DiagonalPlusLowRank<Complex> Ac(dc, Uc, Vc);
CMat denseC = reference_dplr<Complex>(dc, Uc, Vc);
RVec xr = RVec::Random(n);
CVec z = Ac * xr;
VERIFY_IS_APPROX(z, (denseC * xr).eval());
}
// Wide-dynamic-range determinants: the diagonal product is accumulated in the
// balanced form m * 2^e, so partial products neither overflow nor underflow
// when the determinant itself is representable, whatever the ordering of large
// and small diagonal entries. Genuinely non-representable determinants must
// still saturate to infinity / zero.
void test_dplr_determinant_scaled() {
typedef Matrix<double, Dynamic, 1> Vec;
typedef Matrix<double, Dynamic, Dynamic> Mat;
typedef std::complex<double> Complex;
typedef Matrix<Complex, Dynamic, 1> CVec;
typedef Matrix<Complex, Dynamic, Dynamic> CMat;
{
// The reviewer's magnitude regime: det(D) = 1e200 * 1e200 * 1e-100 = 1e300 is
// finite, but the plain running product hits 1e400 partway. k = 0 at runtime.
Vec d(3);
d << 1e200, 1e200, 1e-100;
DiagonalPlusLowRank<double> A(d, Mat::Zero(3, 0), Mat::Zero(3, 0));
const double det = A.determinant();
VERIFY((numext::isfinite)(det));
VERIFY_IS_APPROX(det, 1e300);
}
{
// Underflow-side analogue: the partial product flushes to zero at 1e-400
// while det(D) = 1e-300 is representable.
Vec d(3);
d << 1e-200, 1e-200, 1e100;
DiagonalPlusLowRank<double> A(d, Mat::Zero(3, 0), Mat::Zero(3, 0));
const double det = A.determinant();
VERIFY(det != 0.0);
VERIFY_IS_APPROX(det, 1e-300);
}
{
// The capacitance determinant enters the same balanced accumulation: with
// 450 entries of 10 first, the running diagonal product tops out at 1e450
// while det(D) = 10^450 * 10^-150 = 1e300; the rank-1 correction
// U = d[0] e_0, V = e_0 gives the capacitance 1 + d[0]/d[0] = 2, so
// det(A) = 2e300 by the determinant lemma.
const Index n = 600;
Vec d(n);
d.head(450).setConstant(10.0);
d.tail(150).setConstant(0.1);
Mat U = Mat::Zero(n, 1), V = Mat::Zero(n, 1);
U(0, 0) = d[0];
V(0, 0) = 1.0;
DiagonalPlusLowRank<double> A(d, U, V);
const double det = A.determinant();
// One rounding multiply per balanced factor, and the decimal constants 0.1
// and their 150-fold product are themselves inexact: a few n eps total.
const double det_tol = 4.0 * double(n) * NumTraits<double>::epsilon();
VERIFY((numext::isfinite)(det));
VERIFY(numext::abs(det / 2e300 - 1.0) <= det_tol);
}
{
// Complex scalars balance on max(|re|, |im|): det = i * 1e200 * 1e100 = 1e300 i.
CVec d(3);
d << Complex(0, 1e200), Complex(1e200, 0), Complex(1e-100, 0);
DiagonalPlusLowRank<Complex> A(d, CMat::Zero(3, 0), CMat::Zero(3, 0));
VERIFY_IS_APPROX(A.determinant(), Complex(0, 1e300));
}
{
// A genuinely overflowing determinant still reports infinity...
Vec d(2);
d << 1e200, 1e200;
DiagonalPlusLowRank<double> A(d, Mat::Zero(2, 0), Mat::Zero(2, 0));
VERIFY((numext::isinf)(A.determinant()));
}
{
// ... and a genuinely vanishing one an exact zero.
Vec d(2);
d << 1e-200, 1e-200;
DiagonalPlusLowRank<double> A(d, Mat::Zero(2, 0), Mat::Zero(2, 0));
VERIFY_IS_EQUAL(A.determinant(), 0.0);
}
}
// The two factors of the determinant lemma can sit at opposite ends of the exponent range even when
// the determinant is ~1, since D^{-1} scales the capacitance entries by the reciprocal diagonal. Both
// det(D) and det(capacitance) must therefore accumulate in the balanced mantissa * 2^e form, the
// latter from the LU pivots rather than the plain pivot product .determinant() would form.
void test_dplr_determinant_capacitance() {
typedef Matrix<double, Dynamic, 1> Vec;
typedef Matrix<double, Dynamic, Dynamic> Mat;
typedef std::complex<double> Complex;
typedef Matrix<Complex, Dynamic, Dynamic> CMat;
const double eps = NumTraits<double>::epsilon();
{
// The reviewer's reproducer: D + I I^H = diag(1 + 1e-200) is essentially the
// identity, but det(D) = 1e-400 underflows and det(capacitance) ~ 1e400
// overflows when either is formed on its own.
Vec d(2);
d << 1e-200, 1e-200;
DiagonalPlusLowRank<double> A(d, Mat::Identity(2, 2), Mat::Identity(2, 2));
const double det = A.determinant();
VERIFY(numext::abs(det - 1.0) <= 16.0 * eps);
}
{
// Mirrored, in exact powers of two: d = 2^60 and U = -(2^60 - 2^8) I with
// V = I represent diag(2^8), so det(A) = 2^176 exactly. Every intermediate
// is a power-of-two computation (the capacitance is exactly 2^-52 I by
// Sterbenz subtraction), yet det(D) = 2^1342-ish overflows and the plain
// capacitance pivot product 2^-1144 underflows.
const Index n = 22;
Vec d = Vec::Constant(n, std::ldexp(1.0, 60));
Mat U = (std::ldexp(1.0, 8) - std::ldexp(1.0, 60)) * Mat::Identity(n, n);
Mat V = Mat::Identity(n, n);
DiagonalPlusLowRank<double> A(d, U, V);
VERIFY_IS_EQUAL(A.determinant(), std::ldexp(1.0, 176));
}
{
// Complex analogue of the reproducer: the mantissa stays complex and is
// balanced on max(|re|, |im|). D + I I^H = diag(1 + 1e-200 i), det ~ 1.
Matrix<Complex, Dynamic, 1> d(2);
d << Complex(0, 1e-200), Complex(0, 1e-200);
DiagonalPlusLowRank<Complex> A(d, CMat::Identity(2, 2), CMat::Identity(2, 2));
VERIFY_IS_APPROX(A.determinant(), Complex(1, 0));
}
{
// Sign correctness through the capacitance path at extreme scales:
// D + (-I) I^H = diag(1e-200 - 1) ~ -I_3, det ~ -1; the balanced pivot
// product of the capacitance ~ -1e200 I_3 carries the sign.
Vec d = Vec::Constant(3, 1e-200);
DiagonalPlusLowRank<double> A(d, (-Mat::Identity(3, 3)).eval(), Mat::Identity(3, 3));
VERIFY_IS_APPROX(A.determinant(), -1.0);
}
{
// Sign correctness across mixed extreme diagonal scales (rank 0).
Vec d(4);
d << -1e200, 1e-200, 1e200, 1e-200;
DiagonalPlusLowRank<double> A(d, Mat::Zero(4, 0), Mat::Zero(4, 0));
VERIFY_IS_APPROX(A.determinant(), -1.0);
}
{
// Genuine overflow saturates to infinity of the correct sign...
Vec d(3);
d << -1e200, 1e200, 1e200;
DiagonalPlusLowRank<double> A(d, Mat::Zero(3, 0), Mat::Zero(3, 0));
const double det = A.determinant();
VERIFY((numext::isinf)(det) && det < 0.0);
}
{
// ... and genuine underflow to a zero of the correct sign.
Vec d(3);
d << -1e-200, 1e-200, 1e-200;
DiagonalPlusLowRank<double> A(d, Mat::Zero(3, 0), Mat::Zero(3, 0));
const double det = A.determinant();
VERIFY_IS_EQUAL(det, 0.0);
VERIFY(std::signbit(det));
}
}
// The capacitance triple product V^H D^{-1} U can overflow in its plain association while the
// capacitance itself is representable: D^{-1} pairs the reciprocal of a tiny diagonal with a huge
// factor before the other, tiny factor pulls the product back down. The Woodbury kernels form these
// products from exactly rescaled factors so no spurious Inf/NaN reaches determinant(), solve() or
// inverse().
void test_dplr_capacitance_overflow() {
typedef Matrix<double, Dynamic, 1> Vec;
typedef std::complex<double> Complex;
typedef Matrix<Complex, Dynamic, 1> CVec;
const double eps = NumTraits<double>::epsilon();
{
// The reviewer's case: D + UV^H = 1e-200 + 1e-200 * 1e200 ~ 1 and the
// capacitance 1 + 1e200 is representable, but V^H D^{-1} = 1e400 is not;
// determinant() used to return Inf and solve() NaN.
Vec d(1), u(1), v(1);
d << 1e-200;
u << 1e-200;
v << 1e200;
DiagonalPlusLowRank<double> A(d, u, v);
VERIFY((numext::isfinite)(A.capacitance()(0, 0)));
VERIFY_IS_APPROX(A.capacitance()(0, 0), 1e200);
VERIFY(numext::abs(A.determinant() - 1.0) <= 8.0 * eps);
// The Woodbury splitting routes this solve through D^{-1} b ~ 1e200 and
// cancels it back down to x ~ 1, so the amplified digits are lost to any
// association (see the solve() documentation); the fix is that the solve
// stays finite instead of turning into NaN through an infinite capacitance.
Vec b(1);
b << 1.0;
VERIFY(A.solve(b).allFinite());
// D^{-1} b is 1e400 here, though the operator is essentially the identity and the solution 1e200
// is representable; without the power-of-two rescaling the first term is Inf and the correction
// turns it into NaN, which is what this pins. It cannot pin finiteness: A ~ 1 makes the correction
// cancel D^{-1} b over ~200 digits, and whether that leaves 0 or an ulp the undone scaling lifts
// back to Inf is a codegen detail (x86-64 gives the first, aarch64 the second).
Vec big_b(1);
big_b << 1e200;
VERIFY(!A.solve(big_b).array().isNaN().any());
}
{
// The same overflow pattern in exact powers of two, where the Woodbury
// arithmetic is exact end to end: d = 2^-522, U = 2^-1025, V = 2^503
// represent A = [2^-521] exactly; V^H D^{-1} = 2^1025 still overflows, and
// the capacitance is exactly 2. Determinant and solve must match the dense
// results for the represented matrix exactly.
Vec d(1), u(1), v(1);
d << std::ldexp(1.0, -522);
u << std::ldexp(1.0, -1025);
v << std::ldexp(1.0, 503);
DiagonalPlusLowRank<double> A(d, u, v);
VERIFY_IS_EQUAL(A.capacitance()(0, 0), 2.0);
VERIFY_IS_EQUAL(A.determinant(), std::ldexp(1.0, -521));
Vec b(1);
b << 1.0;
VERIFY_IS_EQUAL(A.solve(b)(0), std::ldexp(1.0, 521));
}
{
// Symmetric mirror (U huge, V tiny): the plain capacitance association is
// safe here, but the inverse's D^{-1} U product overflows spuriously while
// the inverse factor -1e400 / (1 + 1e200) ~ -1e200 is representable.
Vec d(1), u(1), v(1);
d << 1e-200;
u << 1e200;
v << 1e-200;
DiagonalPlusLowRank<double> A(d, u, v);
VERIFY(numext::abs(A.determinant() - 1.0) <= 8.0 * eps);
DiagonalPlusLowRank<double> Ainv = A.inverse();
VERIFY(Ainv.factorU().allFinite());
VERIFY_IS_APPROX(Ainv.factorU()(0, 0), -1e200);
Vec b(1);
b << 1.0;
VERIFY(A.solve(b).allFinite());
}
{
// Exact-power-of-two mirror: d = 2^-522, U = 2^502, V = 2^-1025 represent
// A = [1.5 * 2^-522]; D^{-1} U = 2^1024 overflows, yet the inverse factor
// -2^1024 / 1.5 is representable and the solve is exact to rounding.
Vec d(1), u(1), v(1);
d << std::ldexp(1.0, -522);
u << std::ldexp(1.0, 502);
v << std::ldexp(1.0, -1025);
DiagonalPlusLowRank<double> A(d, u, v);
VERIFY_IS_EQUAL(A.capacitance()(0, 0), 1.5);
VERIFY_IS_EQUAL(A.determinant(), 1.5 * std::ldexp(1.0, -522));
DiagonalPlusLowRank<double> Ainv = A.inverse();
VERIFY_IS_APPROX(Ainv.factorU()(0, 0), -std::ldexp(1.0 / 1.5, 1024));
Vec b(1);
b << 3.0;
VERIFY_IS_EQUAL(A.solve(b)(0), std::ldexp(1.0, 523));
}
{
// Complex analogue of the reviewer's case: the operator is essentially
// [i], the capacitance 1 + 1e200 is representable, and the balancing and
// rescaling act on component magnitudes.
CVec d(1), u(1), v(1);
d << Complex(0, 1e-200);
u << Complex(1e-200, 0);
v << Complex(0, -1e200);
DiagonalPlusLowRank<Complex> A(d, u, v);
VERIFY((numext::isfinite)(numext::real(A.capacitance()(0, 0))));
VERIFY_IS_APPROX(A.capacitance()(0, 0), Complex(1e200, 0));
VERIFY_IS_APPROX(A.determinant(), Complex(0, 1));
}
}
// Moderate data must keep the plain product association: the normalized
// Woodbury kernels only engage when a conservative exponent bound says a
// product could overflow, so for well-scaled operators the capacitance and the
// solve are bit-identical to the unnormalized evaluation.
template <typename Scalar>
void test_dplr_capacitance_fast_path(Index n, Index k) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec d;
Mat U, V;
random_dplr<Scalar>(n, k, d, U, V);
DiagonalPlusLowRank<Scalar> A(d, U, V);
Mat capRef = Mat::Identity(k, k);
capRef.noalias() += V.adjoint() * d.cwiseInverse().asDiagonal() * U;
VERIFY_IS_CWISE_EQUAL(A.capacitance(), capRef);
Vec b = Vec::Random(n);
PartialPivLU<Mat> capLU(capRef);
Vec xRef = d.cwiseInverse().asDiagonal() * b;
xRef.noalias() -= d.cwiseInverse().asDiagonal() * (U * capLU.solve(V.adjoint() * xRef));
VERIFY_IS_CWISE_EQUAL(A.solve(b), xRef);
}
// A fixed correction rank of zero is a compile-level regression: the operator
// must instantiate -- the capacitance solve is compile-time dispatched away, so
// PartialPivLU<Matrix<Scalar, 0, 0>> is never formed -- and behave as the plain
// diagonal it is.
template <typename Scalar, int N>
void test_dplr_fixed_rank0() {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, N, 1> VecN;
typedef Matrix<Scalar, N, 0> FacN0;
typedef Matrix<Scalar, N, N> MatN;
VecN d = VecN::Random();
d.array() += Scalar(RealScalar(3));
FacN0 U, V;
DiagonalPlusLowRank<Scalar, N, 0> A(d, U, V);
VERIFY_IS_EQUAL(A.correctionRank(), Index(0));
MatN dense = MatN::Zero();
dense.diagonal() = d;
MatN Ad = A;
VERIFY_IS_APPROX(Ad, dense);
VecN x = VecN::Random();
VecN y = A * x;
VERIFY_IS_APPROX(y, (d.asDiagonal() * x).eval());
VecN b = VecN::Random();
VecN xs = A.solve(b);
VERIFY_IS_APPROX(xs, (b.array() / d.array()).matrix().eval());
VERIFY_IS_APPROX(A.determinant(), d.prod());
DiagonalPlusLowRank<Scalar, N, 0> Ainv = A.inverse();
MatN invd = Ainv;
VERIFY_IS_APPROX((invd * dense).eval(), MatN(MatN::Identity()));
// Dynamic size with the rank still fixed at zero takes the same dispatch.
Matrix<Scalar, Dynamic, 1> dd = d;
Matrix<Scalar, Dynamic, 0> Ud(N, 0), Vd(N, 0);
DiagonalPlusLowRank<Scalar, Dynamic, 0> B(dd, Ud, Vd);
Matrix<Scalar, Dynamic, 1> yb = B * Matrix<Scalar, Dynamic, 1>(x);
VERIFY_IS_APPROX(yb, (d.asDiagonal() * x).eval());
VERIFY_IS_APPROX(B.solve(Matrix<Scalar, Dynamic, 1>(b)).eval(), (b.array() / d.array()).matrix().eval());
VERIFY_IS_APPROX(B.determinant(), d.prod());
}
template <typename Scalar, int N, int K>
void test_dplr_fixed() {
typedef Matrix<Scalar, N, 1> VecN;
typedef Matrix<Scalar, N, K> FacNK;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
typedef typename NumTraits<Scalar>::Real RealScalar;
VecN d = VecN::Random();
d.array() += Scalar(RealScalar(3));
FacNK U = FacNK::Random(), V = FacNK::Random();
DiagonalPlusLowRank<Scalar, N, K> A(d, U, V);
STATIC_CHECK((DiagonalPlusLowRank<Scalar, N, K>::RowsAtCompileTime == N));
STATIC_CHECK((internal::remove_all_t<decltype(makeDiagonalPlusLowRank(d, U, V))>::RowsAtCompileTime == N));
Mat dense = reference_dplr<Scalar>(Vec(d), Mat(U), Mat(V));
Matrix<Scalar, N, N> Adense = A;
VERIFY_IS_APPROX(Mat(Adense), dense);
VecN x = VecN::Random();
VecN y = A * x;
VERIFY_IS_APPROX(y, (dense * x).eval());
VecN b = VecN::Random();
VecN xs = A.solve(b);
VERIFY_IS_APPROX((dense * xs).eval(), b);
// A dynamic right-hand side against the fixed-size operator: the compile-time
// product-dimension check must accept the Dynamic/fixed mix.
Vec xd = Vec(x);
Vec yd = A * xd;
VERIFY_IS_APPROX(yd, (dense * xd).eval());
}
EIGEN_DECLARE_TEST(structured_dplr) {
for (int i = 0; i < g_repeat; ++i) {
// Products, dense assignment, coefficient access; k = 0 is a plain diagonal
// and k = n a full-rank correction.
CALL_SUBTEST_1((test_dplr_product<double>(1, 0)));
CALL_SUBTEST_1((test_dplr_product<double>(10, 0)));
CALL_SUBTEST_1((test_dplr_product<double>(10, 1)));
CALL_SUBTEST_1((test_dplr_product<double>(20, 3)));
CALL_SUBTEST_1((test_dplr_product<double>(8, 8)));
CALL_SUBTEST_1((test_dplr_product<float>(12, 2)));
CALL_SUBTEST_1((test_dplr_product<std::complex<double>>(9, 2)));
CALL_SUBTEST_1((test_dplr_product<std::complex<float>>(7, 3)));
CALL_SUBTEST_1((test_dplr_transpose<double>(10, 2)));
CALL_SUBTEST_1((test_dplr_transpose<std::complex<double>>(8, 3)));
// Woodbury solves against the dense LU.
CALL_SUBTEST_2((test_dplr_solve<double>(1, 0)));
CALL_SUBTEST_2((test_dplr_solve<double>(10, 0)));
CALL_SUBTEST_2((test_dplr_solve<double>(10, 1)));
CALL_SUBTEST_2((test_dplr_solve<double>(30, 4)));
CALL_SUBTEST_2((test_dplr_solve<double>(8, 8)));
CALL_SUBTEST_2((test_dplr_solve<float>(12, 2)));
CALL_SUBTEST_2((test_dplr_solve<std::complex<double>>(14, 3)));
CALL_SUBTEST_2((test_dplr_solve<std::complex<float>>(8, 2)));
// Inverse closure, determinant lemma, matrix-free GMRES, fixed sizes.
CALL_SUBTEST_3((test_dplr_inverse_determinant<double>(10, 2)));
CALL_SUBTEST_3((test_dplr_inverse_determinant<double>(6, 0)));
CALL_SUBTEST_3((test_dplr_inverse_determinant<std::complex<double>>(8, 3)));
CALL_SUBTEST_3((test_dplr_matrix_free_gmres<double>(24, 3)));
CALL_SUBTEST_3((test_dplr_fixed<double, 6, 2>()));
CALL_SUBTEST_3((test_dplr_fixed<std::complex<float>, 5, 1>()));
// Review regressions: aliased and value-nested (owning) delayed products,
// mixed-scalar products, the balanced determinant accumulation, and the
// fixed rank-0 instantiation.
CALL_SUBTEST_4((test_dplr_aliased_product<double>(11, 3)));
CALL_SUBTEST_4((test_dplr_aliased_product<std::complex<double>>(9, 2)));
CALL_SUBTEST_4((test_dplr_delayed_product<double>(13, 2)));
CALL_SUBTEST_4((test_dplr_delayed_product<std::complex<double>>(10, 3)));
CALL_SUBTEST_4((test_dplr_mixed_scalar<double>(12, 3)));
CALL_SUBTEST_4((test_dplr_mixed_scalar<float>(9, 2)));
CALL_SUBTEST_4(test_dplr_determinant_scaled());
CALL_SUBTEST_4(test_dplr_determinant_capacitance());
CALL_SUBTEST_4(test_dplr_capacitance_overflow());
CALL_SUBTEST_4((test_dplr_capacitance_fast_path<double>(11, 3)));
CALL_SUBTEST_4((test_dplr_capacitance_fast_path<std::complex<double>>(9, 2)));
CALL_SUBTEST_4((test_dplr_fixed_rank0<double, 5>()));
CALL_SUBTEST_4((test_dplr_fixed_rank0<std::complex<float>, 4>()));
}
}