blob: 68f7685e6fbee55c3a7e27fc6552da595c375a9c [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 <contrib/Eigen/StructuredMatrices>
using namespace Eigen;
// Reference dense Cauchy built entry-wise from the node vectors.
template <typename Scalar>
Matrix<Scalar, Dynamic, Dynamic> reference_cauchy(const Matrix<Scalar, Dynamic, 1>& x,
const Matrix<Scalar, Dynamic, 1>& y) {
Matrix<Scalar, Dynamic, Dynamic> dense(x.size(), y.size());
for (Index j = 0; j < y.size(); ++j)
for (Index i = 0; i < x.size(); ++i) dense(i, j) = Scalar(1) / (x[i] - y[j]);
return dense;
}
// Reference dense Cauchy built entry-wise through the guarded reciprocal, the
// single helper every coefficient evaluation must agree with at the overflow
// boundary.
template <typename Scalar>
Matrix<Scalar, Dynamic, Dynamic> reference_cauchy_guarded(const Matrix<Scalar, Dynamic, 1>& x,
const Matrix<Scalar, Dynamic, 1>& y) {
Matrix<Scalar, Dynamic, Dynamic> dense(x.size(), y.size());
for (Index j = 0; j < y.size(); ++j)
for (Index i = 0; i < x.size(); ++i) dense(i, j) = internal::cauchy_reciprocal_diff(x[i], y[j]);
return dense;
}
// Separated node sets: x in [2,3], y in [0,1], so all denominators are in [1,3].
template <typename Scalar>
void separated_nodes(Index m, Index n, Matrix<Scalar, Dynamic, 1>& x, Matrix<Scalar, Dynamic, 1>& y) {
typedef typename NumTraits<Scalar>::Real RealScalar;
x = Matrix<Scalar, Dynamic, 1>::Random(m);
y = Matrix<Scalar, Dynamic, 1>::Random(n);
x = (x * Scalar(RealScalar(0.5))).array() + Scalar(RealScalar(2.5));
y = (y * Scalar(RealScalar(0.5))).array() + Scalar(RealScalar(0.5));
}
template <typename Scalar>
void test_cauchy_product(Index m, Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec x, y;
separated_nodes<Scalar>(m, n, x, y);
Cauchy<Scalar> C(x, y);
Mat dense = reference_cauchy<Scalar>(x, y);
Mat Cd = C;
VERIFY_IS_APPROX(Cd, dense);
Mat accumulated = Mat::Random(m, n);
const Mat initial = accumulated;
accumulated += C;
VERIFY_IS_APPROX(accumulated, initial + dense);
accumulated -= C;
VERIFY_IS_APPROX(accumulated, initial);
for (Index t = 0; t < 5; ++t) {
Index i = internal::random<Index>(0, m - 1), j = internal::random<Index>(0, n - 1);
VERIFY_IS_APPROX(C.coeff(i, j), dense(i, j));
}
Vec v = Vec::Random(n);
VERIFY_IS_APPROX((C * v).eval(), (dense * v).eval());
Mat V = Mat::Random(n, 3);
VERIFY_IS_APPROX((C * V).eval(), (dense * V).eval());
// Accumulation form exercised by the iterative solvers.
Vec w = Vec::Random(m);
Vec w0 = w;
w.noalias() += C * v;
VERIFY_IS_APPROX(w, (w0 + dense * v).eval());
}
template <typename Scalar>
void test_cauchy_transpose(Index m, Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec x, y;
separated_nodes<Scalar>(m, n, x, y);
Cauchy<Scalar> C(x, y);
Mat dense = reference_cauchy<Scalar>(x, y);
Mat Td = C.transpose();
VERIFY_IS_APPROX(Td, Mat(dense.transpose()));
Mat Ad = C.adjoint();
VERIFY_IS_APPROX(Ad, Mat(dense.adjoint()));
Mat Kd = C.conjugate();
VERIFY_IS_APPROX(Kd, Mat(dense.conjugate()));
Vec w = Vec::Random(m);
VERIFY_IS_APPROX((C.transpose() * w).eval(), (dense.transpose() * w).eval());
VERIFY_IS_APPROX((C.adjoint() * w).eval(), (dense.adjoint() * w).eval());
}
// GKO solve on separated random nodes, verified through residuals (Cauchy
// matrices are exponentially ill-conditioned, so forward-error comparisons
// between different algorithms would not be meaningful).
template <typename Scalar>
void test_cauchy_lu(Index n) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec x, y;
separated_nodes<Scalar>(n, n, x, y);
Cauchy<Scalar> C(x, y);
Mat dense = reference_cauchy<Scalar>(x, y);
CauchyLU<Scalar> lu(C);
VERIFY(lu.info() == Success);
// Backward-stability-style residual bound, scaled by the scalar's epsilon (the
// separated node sets bound the GKO generators by 1, so no growth term).
const RealScalar tol = RealScalar(1000) * RealScalar(n) * NumTraits<RealScalar>::epsilon();
Vec b = Vec::Random(n);
Vec u = lu.solve(b);
VERIFY((dense * u - b).norm() <= tol * (dense.norm() * u.norm() + b.norm()));
// Multiple right-hand sides.
Mat B = Mat::Random(n, 3);
Mat U = lu.solve(B);
VERIFY((dense * U - B).norm() <= tol * (dense.norm() * U.norm() + B.norm()));
// Transposed and adjoint systems reuse the same factorization.
Vec vt = lu.transpose().solve(b);
VERIFY((dense.transpose() * vt - b).norm() <= tol * (dense.norm() * vt.norm() + b.norm()));
Vec va = lu.adjoint().solve(b);
VERIFY((dense.adjoint() * va - b).norm() <= tol * (dense.norm() * va.norm() + b.norm()));
}
// The Hilbert matrix as a Cauchy matrix (x_i = i+1, y_j = -j): deterministic
// residual bounds far below what the astronomical conditioning would allow a
// forward-error test, plus the closed-form determinant against the dense LU one.
void test_cauchy_hilbert() {
typedef Matrix<double, Dynamic, 1> Vec;
typedef Matrix<double, Dynamic, Dynamic> Mat;
auto residual = [](Index n) {
Vec x(n), y(n);
for (Index i = 0; i < n; ++i) {
x[i] = double(i + 1);
y[i] = -double(i);
}
Cauchy<double> C(x, y);
Mat dense = reference_cauchy<double>(x, y);
Vec b = dense * Vec::Ones(n);
CauchyLU<double> lu(C);
VERIFY(lu.info() == Success);
Vec u = lu.solve(b);
return (dense * u - b).norm() / b.norm();
};
// GKO partial pivoting is backward stable, so the residual stays a small
// multiple of epsilon even as the Hilbert conditioning explodes with n.
const double eps = NumTraits<double>::epsilon();
VERIFY(residual(8) <= 5e5 * eps); // ~1e-10
VERIFY(residual(12) <= 5e7 * eps); // ~1e-8
Vec x(5), y(5);
for (Index i = 0; i < 5; ++i) {
x[i] = double(i + 1);
y[i] = -double(i);
}
Cauchy<double> H5(x, y);
Mat dense = reference_cauchy<double>(x, y);
VERIFY_IS_APPROX(H5.determinant(), dense.determinant());
}
// Clustered row nodes make leading minors nearly singular: partial pivoting must
// keep the factorization backward stable (residual check).
void test_cauchy_lu_pivoting(Index n) {
typedef Matrix<double, Dynamic, 1> Vec;
typedef Matrix<double, Dynamic, Dynamic> Mat;
Vec x(n), y(n);
for (Index i = 0; i < n; ++i) {
x[i] = 2.0 + 1e-13 * double(i * i); // tight cluster
y[i] = double(i) / double(n); // spread out
}
Cauchy<double> C(x, y);
Mat dense = reference_cauchy<double>(x, y);
CauchyLU<double> lu(C);
VERIFY(lu.info() == Success);
Vec b = Vec::Random(n);
Vec u = lu.solve(b);
const double tol = 5e7 * NumTraits<double>::epsilon(); // ~1e-8
VERIFY((dense * u - b).norm() <= tol * (dense.norm() * u.norm() + b.norm()));
}
// A duplicated row node makes two rows identical, hence the matrix exactly
// singular; a zero pivot must survive partial pivoting and be reported.
void test_cauchy_lu_singular() {
typedef Matrix<double, Dynamic, 1> Vec;
Vec x(5), y(5);
x << 2.0, 2.5, 2.0, 2.75, 2.25; // x[2] duplicates x[0]
y << 0.1, 0.3, 0.5, 0.7, 0.9;
Cauchy<double> C(x, y);
CauchyLU<double> lu(C);
VERIFY(lu.info() == NumericalIssue);
}
// Generator mantissas can overflow even when every matrix entry and LU factor
// is finite. Exponent-tracked generators must preserve the cancellation against
// the node reciprocal, and still expose an exact duplicate-row singularity.
template <typename Scalar>
void test_cauchy_lu_scaled_generators() {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec x(2), y(2), b(2);
const RealScalar P = std::ldexp(RealScalar(1), 1000);
const RealScalar t = std::ldexp(RealScalar(1), -1000);
x << Scalar(0), Scalar(-P);
y << Scalar(P), Scalar(-t);
b << Scalar(1), Scalar(0.25);
Cauchy<Scalar> C(x, y);
Mat dense = C;
VERIFY(dense.allFinite());
CauchyLU<Scalar> lu(C);
VERIFY(lu.info() == Success);
Vec u = lu.solve(b);
VERIFY(u.allFinite());
const RealScalar tol = RealScalar(64) * NumTraits<RealScalar>::epsilon();
VERIFY((dense * u - b).cwiseAbs().maxCoeff() <= tol * b.cwiseAbs().maxCoeff());
Vec xs(2);
xs << Scalar(0), Scalar(0);
Cauchy<Scalar> Cs(xs, y);
Mat denseSingular = Cs;
VERIFY(denseSingular.allFinite());
CauchyLU<Scalar> singularLu(Cs);
VERIFY(singularLu.info() == NumericalIssue);
Vec xi(1), yi(1);
xi << Scalar(0);
yi << Scalar(-std::numeric_limits<RealScalar>::denorm_min());
Cauchy<Scalar> Ci(xi, yi);
CauchyLU<Scalar> nonfiniteLu(Ci);
VERIFY(nonfiniteLu.info() == NumericalIssue);
}
// y = -x gives the symmetric generalized Hilbert matrix 1/(x_i + x_j): verify
// the symmetry closure of the transpose through the node identity C^T = C(-y,-x).
template <typename Scalar>
void test_cauchy_symmetric(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec x(n);
for (Index i = 0; i < n; ++i) x[i] = Scalar(1) + Scalar(i);
Vec negx = -x;
Cauchy<Scalar> C(x, negx);
Mat dense = reference_cauchy<Scalar>(x, negx);
VERIFY_IS_APPROX(dense, Mat(dense.transpose()));
Cauchy<Scalar> Ct = C.transpose();
VERIFY_IS_EQUAL(Ct.rowNodes(), Vec(x)); // -(-x) round-trips exactly
Mat Ctd = Ct;
VERIFY_IS_APPROX(Ctd, dense);
}
// The closed form is a product of exact node differences (relative error
// O(n^2 eps)); the dense LU determinant it is compared against carries a
// cond(C)*eps error, so keep n small and the tolerance loose: the reference is
// the less accurate side of this comparison.
template <typename Scalar>
void test_cauchy_determinant(Index n) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec x, y;
separated_nodes<Scalar>(n, n, x, y);
Cauchy<Scalar> C(x, y);
Mat dense = reference_cauchy<Scalar>(x, y);
// The closed form is accurate to O(n^2 eps); the dense LU reference is the less
// accurate side, its determinant carrying a relative error that grows like
// cond(C)*eps (empirically ~0.1*cond*eps). Scale the bound by the SVD condition
// number so the test is robust across random node draws.
JacobiSVD<Mat> svd(dense);
const RealScalar cond = svd.singularValues()(0) / svd.singularValues()(svd.singularValues().size() - 1);
const RealScalar tol = RealScalar(100) * cond * NumTraits<RealScalar>::epsilon();
VERIFY(numext::abs(C.determinant() - dense.determinant()) <= tol * numext::abs(dense.determinant()));
}
// The products carry the default product tag, so assignment materializes a
// temporary exactly like a dense product: x = C * x and x += C * x must see the
// pre-assignment right-hand side. Without the temporary, x = C * x would read a
// zeroed right-hand side and x += C * x would interleave destination writes
// with right-hand-side reads.
template <typename Scalar>
void test_cauchy_aliased_product(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec x, y;
separated_nodes<Scalar>(n, n, x, y);
Cauchy<Scalar> C(x, y);
Mat dense = reference_cauchy<Scalar>(x, y);
Vec v = Vec::Random(n);
Vec w = v;
w = C * w;
VERIFY_IS_APPROX(w, (dense * v).eval());
w = v;
w += C * w;
VERIFY_IS_APPROX(w, (v + dense * v).eval());
w = v;
w -= C * w;
VERIFY_IS_APPROX(w, (v - dense * v).eval());
Mat V = Mat::Random(n, 3);
Mat W = V;
W = C * W;
VERIFY_IS_APPROX(W, (dense * V).eval());
}
// Aliasing beyond the same-object case: the default-product temporary must also
// resolve right-hand-side expressions that reference the destination,
// overlapping views of one buffer, and rectangular self-assignment where the
// destination is resized by the assignment.
template <typename Scalar>
void test_cauchy_aliased_expression(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec xn, yn;
separated_nodes<Scalar>(n, n, xn, yn);
Cauchy<Scalar> C(xn, yn);
Mat dense = reference_cauchy<Scalar>(xn, yn);
// Right-hand-side expression referencing the destination.
Vec v = Vec::Random(n), v0 = v;
v = C * (v + Vec::Ones(n));
VERIFY_IS_APPROX(v, (dense * (v0 + Vec::Ones(n))).eval());
// Overlapping (shifted) segments of one buffer.
Vec buf = Vec::Random(n + 1);
Vec expected = dense * buf.tail(n);
buf.head(n) = C * buf.tail(n);
VERIFY_IS_APPROX(buf.head(n).eval(), expected);
// Rectangular self-assignment: z = R * z resizes the destination from n to m,
// so the product must be captured before the destination storage is touched.
const Index m = n + 3;
Vec xr, yr;
separated_nodes<Scalar>(m, n, xr, yr);
Cauchy<Scalar> R(xr, yr);
Mat denseR = reference_cauchy<Scalar>(xr, yr);
Vec z = Vec::Random(n), z0 = z;
z = R * z;
VERIFY_IS_EQUAL(z.size(), m);
VERIFY_IS_APPROX(z, (denseR * z0).eval());
}
// transpose()/conjugate()/adjoint() return owning temporaries, so the product
// expression 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_cauchy_delayed_product(Index m, Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
STATIC_CHECK(!std::is_reference<typename internal::ref_selector<Cauchy<Scalar>>::type>::value);
Vec x, y;
separated_nodes<Scalar>(m, n, x, y);
Cauchy<Scalar> C(x, y);
Mat dense = reference_cauchy<Scalar>(x, y);
Vec w = Vec::Random(m);
auto expr = C.adjoint() * w; // the adjoint temporary dies with the full expression
Vec scribble = Vec::Random(m + n); // reuses the temporary's freed heap storage
Vec u = expr;
VERIFY_IS_APPROX(u, (dense.adjoint() * w).eval());
VERIFY_IS_EQUAL(scribble.size(), m + 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 and the accumulation must run in the promoted type rather than
// the operator scalar.
template <typename RealScalar>
void test_cauchy_mixed_scalar(Index m, Index n) {
typedef std::complex<RealScalar> Complex;
typedef Matrix<RealScalar, Dynamic, 1> RVec;
typedef Matrix<Complex, Dynamic, 1> CVec;
typedef Matrix<Complex, Dynamic, Dynamic> CMat;
RVec x, y;
separated_nodes<RealScalar>(m, n, x, y);
Cauchy<RealScalar> C(x, y);
CMat dense = reference_cauchy<RealScalar>(x, y).template cast<Complex>();
CVec v = CVec::Random(n);
CVec w = C * v;
VERIFY_IS_APPROX(w, (dense * v).eval());
CVec w0 = CVec::Random(m);
w = w0;
w.noalias() += C * v;
VERIFY_IS_APPROX(w, (w0 + dense * v).eval());
CVec xc, yc;
separated_nodes<Complex>(m, n, xc, yc);
Cauchy<Complex> Cc(xc, yc);
CMat denseC = reference_cauchy<Complex>(xc, yc);
RVec vr = RVec::Random(n);
CVec z = Cc * vr;
VERIFY_IS_APPROX(z, (denseC * vr).eval());
}
// Wide-dynamic-range determinants: the closed-form factors overflow or underflow
// individually while the determinant itself is representable, so the balanced
// m * 2^e accumulation must carry the exponent past the intermediate extremes.
void test_cauchy_determinant_range() {
typedef Matrix<double, Dynamic, 1> Vec;
typedef Matrix<double, Dynamic, Dynamic> Mat;
// Reviewer repro: x = [2s, 3s], y = [0, s] with s = 1e160. The determinant is
// -1/(12 s^2) ~ -8.35e-322, a subnormal, while the naive numerator product
// -s^2 = -1e320 already overflows. Both the closed form and the dense LU
// reference round once into the subnormal range (spacing denorm_min), so the
// comparison needs an absolute term of a few subnormal spacings on top of a
// relative term; the relative term alone would be far below denorm_min.
{
const double s = 1e160;
Vec x(2), y(2);
x << 2 * s, 3 * s;
y << 0.0, s;
Cauchy<double> C(x, y);
Mat dense = reference_cauchy<double>(x, y);
const double det = C.determinant();
const double ref = dense.partialPivLu().determinant();
const double kRelTol = 16 * NumTraits<double>::epsilon();
const double kAbsTol = 4 * std::numeric_limits<double>::denorm_min();
VERIFY((numext::isfinite)(det));
VERIFY(numext::abs(det - ref) <= kRelTol * numext::abs(ref) + kAbsTol);
}
// Overflow-side analogue: x = [2s, 3s, 4s], y = [0, s/2, s] with s = 1e-60.
// The determinant is -1/(3780 s^3) ~ -2.65e176, huge but representable, while
// the naive numerator product -s^6/2 = -5e-361 underflows to zero. The dense
// LU reference is the less accurate side (its error carries the conditioning
// of the matrix, measured ~64 eps here); the deterministic nodes make 1000 eps
// comfortable headroom.
{
const double s = 1e-60;
Vec x(3), y(3);
x << 2 * s, 3 * s, 4 * s;
y << 0.0, s / 2, s;
Cauchy<double> C(x, y);
Mat dense = reference_cauchy<double>(x, y);
const double det = C.determinant();
const double ref = dense.partialPivLu().determinant();
const double kRelTol = 1000 * NumTraits<double>::epsilon();
VERIFY((numext::isfinite)(det));
VERIFY(numext::abs(det - ref) <= kRelTol * numext::abs(ref));
}
// Genuinely infinite determinant: x[0] == y[0] makes entry (0,0) infinite (a
// zero denominator factor with a non-zero numerator); the signed limit as
// x[0] -> y[0] from above is -inf, and division by the exact zero factor must
// produce it rather than a balanced-away finite value.
{
Vec x(2), y(2);
x << 1.0, 2.0;
y << 1.0, 3.0;
Cauchy<double> C(x, y);
const double det = C.determinant();
VERIFY((numext::isinf)(det));
VERIFY(det < 0.0);
}
// Coincident row nodes: two identical rows, so the matrix is exactly singular
// and the zero numerator factor must propagate to an exact zero.
{
Vec x(3), y(3);
x << 2.0, 3.0, 2.0; // x[2] duplicates x[0]
y << 0.0, 0.5, 1.0;
Cauchy<double> C(x, y);
VERIFY_IS_EQUAL(C.determinant(), 0.0);
}
}
// Node differences at the overflow boundary: forming x_j - x_i or x_i - y_j can
// overflow to Inf even though every matrix entry is finite and unexceptional.
// Such factors must enter the balanced accumulation through the exact
// halved-operand recomputation, so only the determinant's own overflow or
// underflow is visible in the result -- saturated to a zero or infinity of the
// mathematically correct sign.
void test_cauchy_determinant_overflow_boundary() {
typedef Matrix<double, Dynamic, 1> Vec;
const double M = 0.6 * (std::numeric_limits<double>::max)();
// Reviewer reproducer: every coefficient 1/(x_i - y_j) is finite and the exact
// determinant (2M)(-0.8M) / (0.7056 M^4) underflows, but the numerator
// difference x_1 - x_0 = 1.2 * DBL_MAX overflows if formed naively (the old
// code returned -Inf). The result must be a zero of the correct sign: one
// negative numerator factor against two negative denominator factors.
{
Vec x(2), y(2);
x << -M, M;
y << -0.4 * M, 0.4 * M;
const double det = Cauchy<double>(x, y).determinant();
VERIFY(det == 0.0 && std::signbit(det));
}
// Sign flip of the same configuration: swapping the y nodes negates the
// determinant, so the underflow must land on +0.
{
Vec x(2), y(2);
x << -M, M;
y << 0.4 * M, -0.4 * M;
const double det = Cauchy<double>(x, y).determinant();
VERIFY(det == 0.0 && !std::signbit(det));
}
// Purely imaginary nodes of the same magnitudes: the halved-operand
// recomputation applies componentwise to complex nodes. Scaling every node by
// i multiplies the 2x2 determinant by i^2 / i^4 = -1, so this underflow lands
// on a real part of +0 (the value is real: the four denominator divisions
// rotate the accumulation back onto the real axis).
{
typedef std::complex<double> Cplx;
Matrix<Cplx, Dynamic, 1> x(2), y(2);
x << Cplx(0.0, -M), Cplx(0.0, M);
y << Cplx(0.0, -0.4 * M), Cplx(0.0, 0.4 * M);
const Cplx det = Cauchy<Cplx>(x, y).determinant();
VERIFY(numext::real(det) == 0.0 && numext::imag(det) == 0.0);
VERIFY(!std::signbit(numext::real(det)));
}
// A representable determinant whose evaluation crosses the boundary, guard on
// a denominator factor (accumulated exponent decremented): with P = 2^1023,
// x = [-P, 0], y = [P, c], the difference x_0 - y_0 = -2^1024 overflows, yet
// det = (P - c) / (2 P c (P + c)), which for c = 2^-60 is 2^-964 up to a
// relative correction c/P ~ 2^-1083, far below roundoff. Every rounded factor
// is a power of two, so the balanced accumulation is exact here.
{
const double P = std::ldexp(1.0, 1023);
const double c = std::ldexp(1.0, -60);
Vec x(2), y(2);
x << -P, 0.0;
y << P, c;
const double det = Cauchy<double>(x, y).determinant();
VERIFY_IS_APPROX(det, std::ldexp(1.0, -964));
}
// Guard on a numerator factor (accumulated exponent incremented):
// x = [-P, P, 0], y = [c, -c, d] with c = 2^1000 and d = 2^-1050. The
// numerator difference x_1 - x_0 = 2^1024 overflows; the determinant
// -4 P^3 (c^2 - d^2) / ((P^2 - c^2)^2 (P^2 - d^2) c d) equals
// -2^-1017 / (1 - 2^-46)^2 up to relative corrections of order 2^-2050.
{
const double P = std::ldexp(1.0, 1023);
const double c = std::ldexp(1.0, 1000);
const double d = std::ldexp(1.0, -1050);
Vec x(3), y(3);
x << -P, P, 0.0;
y << c, -c, d;
const double det = Cauchy<double>(x, y).determinant();
const double r = 1.0 - std::ldexp(1.0, -46); // 1 - (c/P)^2
VERIFY_IS_APPROX(det, -std::ldexp(1.0, -1017) / (r * r));
}
// Genuine overflow: clustered tiny nodes push the determinant past the
// representable range while every factor stays finite; the accumulated
// exponent must saturate to a correctly signed infinity, in both signs.
{
const double t = std::ldexp(1.0, -537);
Vec x(2), y(2);
x << 0.0, 3.0 * t;
y << t, 2.0 * t; // det = -3 / (4 t^2) ~ -1.6e323
const double det = Cauchy<double>(x, y).determinant();
VERIFY((numext::isinf)(det) && std::signbit(det));
}
{
const double t = std::ldexp(1.0, -537);
Vec x(2), y(2);
x << 0.0, 3.0 * t;
y << 2.0 * t, t; // swapped y nodes: det = +3 / (4 t^2)
const double det = Cauchy<double>(x, y).determinant();
VERIFY((numext::isinf)(det) && !std::signbit(det));
}
}
// Boundary nodes in every coefficient evaluation: when x_i - y_j overflows for
// finite nodes, a naively formed coefficient collapses to 1/Inf = 0 while the
// true value is a representable subnormal. coeff(), the dense materialization,
// the products and the CauchyLU factorization must all produce the guarded
// value -- and agree with determinant(), which derives the same quantity
// through its balanced accumulation.
void test_cauchy_boundary_coefficients() {
typedef Matrix<double, Dynamic, 1> Vec;
typedef Matrix<double, Dynamic, Dynamic> Mat;
const double X = (std::numeric_limits<double>::max)();
const double e = std::ldexp(1.0, -1025); // 1/(2*DBL_MAX), correctly rounded (subnormal)
// 1x1 reviewer case: the single coefficient is 1/(2*DBL_MAX) = 2^-1025
// (2.7813423231340017e-309). Every step is correctly rounded IEEE arithmetic
// with a power-of-two result, so the checks are exact equalities.
{
Vec x(1), y(1);
x << X;
y << -X;
Cauchy<double> C(x, y);
VERIFY_IS_EQUAL(C.coeff(0, 0), e);
Mat d = C;
VERIFY_IS_EQUAL(d(0, 0), e);
Mat acc = Mat::Zero(1, 1);
acc += C; // addTo
VERIFY_IS_EQUAL(acc(0, 0), e);
acc -= C; // subTo
VERIFY_IS_EQUAL(acc(0, 0), 0.0);
Vec p = C * Vec::Ones(1);
VERIFY_IS_EQUAL(p[0], e);
// determinant() reaches 2^-1025 through the balanced accumulation; the
// guarded coefficient matches it exactly.
VERIFY_IS_EQUAL(C.determinant(), e);
// The old code materialized a zero coefficient and reported the (regular)
// matrix as singular; the guarded pivot is subnormal but non-zero.
CauchyLU<double> lu(C);
VERIFY(lu.info() == Success);
Vec u = lu.solve(p); // p = C * [1]; recovered exactly (e / e = 1)
VERIFY_IS_EQUAL(u[0], 1.0);
}
// A non-power-of-two boundary difference catches double rounding: computing
// (1/t)*0.5 is one subnormal ULP above the correctly rounded 0.5/t. MPFR gives
// the expected value below for x=1.1777865466541931e308 and
// y=-1.7435684109415042e308, represented by their exact double bit patterns.
{
Vec x(1), y(1);
x << numext::bit_cast<double>(numext::uint64_t(0x7fe4f71daafe5a86ull));
y << numext::bit_cast<double>(numext::uint64_t(0xffef095b3493b755ull));
const double expected = numext::bit_cast<double>(numext::uint64_t(0x00027621a9baa547ull));
Cauchy<double> C(x, y);
VERIFY_IS_EQUAL(C.coeff(0, 0), expected);
Mat dense = C;
VERIFY_IS_EQUAL(dense(0, 0), expected);
Vec product = C * Vec::Ones(1);
VERIFY_IS_EQUAL(product[0], expected);
VERIFY_IS_EQUAL(C.determinant(), expected);
CauchyLU<double> lu(C);
VERIFY(lu.info() == Success);
Vec rhs(1);
rhs << expected;
Vec solution = lu.solve(rhs);
VERIFY_IS_EQUAL(solution[0], 1.0);
}
// 2x2 with boundary pairs among moderate ones: three of the four differences
// overflow (2X and two 1.5X), one stays finite (X). All APIs agree with the
// reference built from the guarded reciprocal; scaled by its magnitude the
// matrix is well conditioned (cond ~ 38), so the GKO solve -- whose column
// generation and generator updates cross the boundary too -- recovers the
// solution accurately.
{
Vec x(2), y(2);
x << X, X / 2;
y << -X, -X / 2;
Cauchy<double> C(x, y);
Mat ref = reference_cauchy_guarded<double>(x, y);
VERIFY_IS_EQUAL(ref(0, 0), e); // 1/(2*DBL_MAX)
VERIFY_IS_EQUAL(ref(1, 1), std::ldexp(1.0, -1024)); // 1/DBL_MAX
VERIFY(ref.allFinite());
VERIFY((ref.array() != 0.0).all());
for (Index j = 0; j < 2; ++j)
for (Index i = 0; i < 2; ++i) VERIFY_IS_EQUAL(C.coeff(i, j), ref(i, j));
Mat d = C;
for (Index j = 0; j < 2; ++j)
for (Index i = 0; i < 2; ++i) VERIFY_IS_EQUAL(d(i, j), ref(i, j));
Vec v(2);
v << 0.75, -0.5;
VERIFY_IS_APPROX((C * v).eval(), (ref * v).eval());
CauchyLU<double> lu(C);
VERIFY(lu.info() == Success);
Vec b = C * Vec::Ones(2);
Vec u = lu.solve(b);
VERIFY_IS_APPROX(u, Vec::Ones(2).eval());
}
// Complex nodes: purely imaginary boundary nodes overflow in the imaginary
// component of the difference; the guard applies componentwise. The paths
// sharing the helper agree exactly; the value is -i/(2*DBL_MAX) and the
// determinant derives it independently, both to within a few subnormal
// spacings (complex division rounds per component).
{
typedef std::complex<double> Cplx;
const double tiny = 4.0 * std::numeric_limits<double>::denorm_min();
Matrix<Cplx, Dynamic, 1> xc(1), yc(1);
xc << Cplx(0.0, X);
yc << Cplx(0.0, -X);
Cauchy<Cplx> C(xc, yc);
const Cplx cval = C.coeff(0, 0);
Matrix<Cplx, Dynamic, Dynamic> dc = C;
VERIFY_IS_EQUAL(dc(0, 0), cval);
Matrix<Cplx, Dynamic, 1> pc = C * Matrix<Cplx, Dynamic, 1>::Ones(1);
VERIFY_IS_EQUAL(pc[0], cval);
VERIFY(numext::abs(numext::real(cval)) <= tiny);
VERIFY(numext::abs(numext::imag(cval) + e) <= tiny);
const Cplx det = C.determinant();
VERIFY(numext::abs(numext::real(det) - numext::real(cval)) <= tiny);
VERIFY(numext::abs(numext::imag(det) - numext::imag(cval)) <= tiny);
}
}
template <typename Scalar, int M, int N>
void test_cauchy_fixed() {
typedef Matrix<Scalar, M, 1> XVec;
typedef Matrix<Scalar, N, 1> YVec;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, M, N> MatMN;
Vec xd, yd;
separated_nodes<Scalar>(M, N, xd, yd);
XVec x = xd;
YVec y = yd;
typedef Cauchy<Scalar, M, N> CauchyType;
CauchyType C(x, y);
STATIC_CHECK((Cauchy<Scalar, M, N>::RowsAtCompileTime == M));
STATIC_CHECK((Cauchy<Scalar, M, N>::ColsAtCompileTime == N));
STATIC_CHECK((internal::remove_all_t<decltype(makeCauchy(x, y))>::ColsAtCompileTime == N));
STATIC_CHECK((internal::remove_all_t<decltype(C.transpose())>::RowsAtCompileTime == N));
STATIC_CHECK((internal::remove_all_t<decltype(C.transpose())>::ColsAtCompileTime == M));
MatMN dense = C;
VERIFY_IS_APPROX(dense, MatMN(reference_cauchy<Scalar>(xd, yd)));
CauchyType* heapC = new CauchyType(x, y);
VERIFY(std::uintptr_t(heapC) % std::alignment_of<CauchyType>::value == 0);
MatMN heapDense = *heapC;
delete heapC;
VERIFY_IS_APPROX(heapDense, dense);
#if EIGEN_MAX_ALIGN_BYTES > 0 && !EIGEN_HAS_CXX17_OVERALIGN
void* raw = (CauchyType::operator new)(sizeof(CauchyType));
VERIFY(std::uintptr_t(raw) % std::alignment_of<CauchyType>::value == 0);
(CauchyType::operator delete)(raw);
#endif
YVec v = YVec::Random();
Matrix<Scalar, M, 1> w = C * v;
VERIFY_IS_APPROX(w, (dense * v).eval());
// .noalias() keeps the direct (temporary-free) path of the default product tag;
// the matching fixed dimensions also pin the compile-time product check.
Matrix<Scalar, M, 1> w2;
w2.noalias() = C * v;
VERIFY_IS_APPROX(w2, (dense * v).eval());
}
EIGEN_DECLARE_TEST(structured_cauchy) {
for (int i = 0; i < g_repeat; ++i) {
// Products, dense assignment, coefficient access.
CALL_SUBTEST_1((test_cauchy_product<double>(1, 1)));
CALL_SUBTEST_1((test_cauchy_product<double>(8, 8)));
CALL_SUBTEST_1((test_cauchy_product<double>(20, 12))); // tall
CALL_SUBTEST_1((test_cauchy_product<double>(12, 20))); // wide
CALL_SUBTEST_1((test_cauchy_product<float>(10, 10)));
CALL_SUBTEST_1((test_cauchy_product<std::complex<double>>(9, 7)));
CALL_SUBTEST_1((test_cauchy_product<std::complex<float>>(7, 9)));
CALL_SUBTEST_1((test_cauchy_transpose<double>(10, 14)));
CALL_SUBTEST_1((test_cauchy_transpose<std::complex<double>>(8, 6)));
// GKO pivoted LU solves.
CALL_SUBTEST_2((test_cauchy_lu<double>(1)));
CALL_SUBTEST_2((test_cauchy_lu<double>(2)));
CALL_SUBTEST_2((test_cauchy_lu<double>(12)));
CALL_SUBTEST_2((test_cauchy_lu<double>(30)));
CALL_SUBTEST_2((test_cauchy_lu<std::complex<double>>(16)));
CALL_SUBTEST_2((test_cauchy_lu<float>(10)));
CALL_SUBTEST_2(test_cauchy_hilbert());
CALL_SUBTEST_2(test_cauchy_lu_pivoting(20));
CALL_SUBTEST_2(test_cauchy_lu_singular());
CALL_SUBTEST_2((test_cauchy_lu_scaled_generators<double>()));
CALL_SUBTEST_2((test_cauchy_lu_scaled_generators<std::complex<double>>()));
// Closed-form determinant, symmetric generalized Hilbert, fixed sizes.
CALL_SUBTEST_3((test_cauchy_determinant<double>(4)));
CALL_SUBTEST_3((test_cauchy_determinant<std::complex<double>>(4)));
CALL_SUBTEST_3((test_cauchy_symmetric<double>(9)));
CALL_SUBTEST_3((test_cauchy_fixed<double, 6, 4>()));
CALL_SUBTEST_3((test_cauchy_fixed<std::complex<float>, 4, 5>()));
// Numerical and lifetime boundaries: aliased and value-nested (owning)
// delayed products, mixed-scalar products, wide-dynamic-range determinants.
CALL_SUBTEST_4((test_cauchy_aliased_product<double>(11)));
CALL_SUBTEST_4((test_cauchy_aliased_product<std::complex<double>>(8)));
CALL_SUBTEST_4((test_cauchy_aliased_expression<double>(11)));
CALL_SUBTEST_4((test_cauchy_aliased_expression<std::complex<double>>(8)));
CALL_SUBTEST_4((test_cauchy_delayed_product<double>(12, 9)));
CALL_SUBTEST_4((test_cauchy_delayed_product<std::complex<double>>(7, 10)));
CALL_SUBTEST_4((test_cauchy_mixed_scalar<double>(10, 13)));
CALL_SUBTEST_4((test_cauchy_mixed_scalar<float>(9, 6)));
CALL_SUBTEST_4(test_cauchy_determinant_range());
CALL_SUBTEST_4(test_cauchy_determinant_overflow_boundary());
CALL_SUBTEST_4(test_cauchy_boundary_coefficients());
}
}