blob: 5a3ae49391d43dd43c4e773db7bdf09ded88e64b [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 <contrib/Eigen/StructuredMatrices>
using namespace Eigen;
// Reference dense circulant built straight from the generating column, so that the
// fast (FFT) product is validated against an independent construction of the matrix.
template <typename Scalar>
Matrix<Scalar, Dynamic, Dynamic> reference_circulant(const Matrix<Scalar, Dynamic, 1>& c) {
const Index n = c.size();
Matrix<Scalar, Dynamic, Dynamic> dense(n, n);
for (Index j = 0; j < n; ++j)
for (Index i = 0; i < n; ++i) {
Index k = i - j;
if (k < 0) k += n;
dense(i, j) = c[k];
}
return dense;
}
template <typename Scalar>
Matrix<Scalar, Dynamic, Dynamic> reference_toeplitz(const Matrix<Scalar, Dynamic, 1>& c,
const Matrix<Scalar, Dynamic, 1>& r) {
const Index m = c.size(), n = r.size();
Matrix<Scalar, Dynamic, Dynamic> dense(m, n);
for (Index j = 0; j < n; ++j)
for (Index i = 0; i < m; ++i) dense(i, j) = (i >= j) ? c[i - j] : r[j - i];
return dense;
}
template <typename Scalar>
void test_circulant_product(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec c = Vec::Random(n);
Circulant<Scalar> C(c);
Mat dense = reference_circulant<Scalar>(c);
// The operator agrees with the independently-built dense matrix, both through
// coeff access and assigned to a dense matrix via its evaluator.
Mat Cd = C;
VERIFY_IS_APPROX(Cd, dense);
for (Index t = 0; t < (std::min)(n, Index(5)); ++t) {
Index i = internal::random<Index>(0, n - 1), j = internal::random<Index>(0, n - 1);
VERIFY_IS_APPROX(C.coeff(i, j), dense(i, j));
}
// Fast matrix-vector and matrix-matrix products.
Vec x = Vec::Random(n);
VERIFY_IS_APPROX((C * x).eval(), (dense * x).eval());
Mat X = Mat::Random(n, 3);
VERIFY_IS_APPROX((C * X).eval(), (dense * X).eval());
// Accumulation forms exercised by the iterative solvers.
Vec y = Vec::Random(n);
Vec y0 = y;
y.noalias() += C * x;
VERIFY_IS_APPROX(y, (y0 + dense * x).eval());
}
template <typename Scalar>
void test_circulant_solve(Index n) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
// Diagonally dominant => well conditioned, so the direct FFT solve is accurate.
Vec c = Vec::Random(n);
c[0] += Scalar(RealScalar(2 * n));
Circulant<Scalar> C(c);
Mat dense = reference_circulant<Scalar>(c);
Vec b = Vec::Random(n);
Vec x = C.solve(b);
VERIFY_IS_APPROX((dense * x).eval(), b);
// Multiple right-hand sides at once.
Mat B = Mat::Random(n, 4);
Mat Xs = C.solve(B);
VERIFY_IS_APPROX((dense * Xs).eval(), B);
}
// The precomputed symbol holds the eigenvalues of the circulant matrix:
// C * f_k = symbol[k] * f_k, with f_k the k-th column of the inverse DFT matrix.
template <typename Scalar>
void test_circulant_symbol(Index n) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef std::complex<RealScalar> Complex;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Complex, Dynamic, 1> ComplexVec;
Vec c = Vec::Random(n);
Circulant<Scalar> C(c);
VERIFY_IS_EQUAL(C.symbol().size(), n);
Matrix<Complex, Dynamic, Dynamic> denseC = reference_circulant<Scalar>(c).template cast<Complex>();
const Index step = numext::maxi<Index>(n / 4, 1);
for (Index k = 0; k < n; k += step) {
ComplexVec f(n);
for (Index j = 0; j < n; ++j) f[j] = std::polar(RealScalar(1), RealScalar(2 * EIGEN_PI * j * k) / RealScalar(n));
VERIFY_IS_APPROX((denseC * f).eval(), (C.symbol()[k] * f).eval());
}
}
template <typename Scalar>
void test_toeplitz_product(Index m, Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec c = Vec::Random(m), r = Vec::Random(n);
r[0] = c[0]; // diagonal entry; r[0] is ignored anyway
Toeplitz<Scalar> T(c, r);
Mat dense = reference_toeplitz<Scalar>(c, r);
Mat Td = T;
VERIFY_IS_APPROX(Td, dense);
Vec x = Vec::Random(n);
VERIFY_IS_APPROX((T * x).eval(), (dense * x).eval());
Mat X = Mat::Random(n, 3);
VERIFY_IS_APPROX((T * X).eval(), (dense * X).eval());
}
template <typename Scalar>
Matrix<Scalar, Dynamic, Dynamic> reference_hankel(const Matrix<Scalar, Dynamic, 1>& h, Index m, Index n) {
Matrix<Scalar, Dynamic, Dynamic> dense(m, n);
for (Index j = 0; j < n; ++j)
for (Index i = 0; i < m; ++i) dense(i, j) = h[i + j];
return dense;
}
template <typename Scalar>
void test_hankel_product(Index m, Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec h = Vec::Random(m + n - 1);
Hankel<Scalar> H(h.head(m), h.tail(n));
Mat dense = reference_hankel<Scalar>(h, m, n);
// The stored generating sequence is rebuilt exactly from column + last row.
VERIFY_IS_EQUAL(H.generator(), h);
VERIFY_IS_EQUAL(Vec(H.column()), Vec(h.head(m)));
VERIFY_IS_EQUAL(Vec(H.lastRow()), Vec(h.tail(n)));
Mat Hd = H;
VERIFY_IS_APPROX(Hd, dense);
for (Index t = 0; t < (std::min)(m, Index(5)); ++t) {
Index i = internal::random<Index>(0, m - 1), j = internal::random<Index>(0, n - 1);
VERIFY_IS_APPROX(H.coeff(i, j), dense(i, j));
}
Vec x = Vec::Random(n);
VERIFY_IS_APPROX((H * x).eval(), (dense * x).eval());
Mat X = Mat::Random(n, 3);
VERIFY_IS_APPROX((H * X).eval(), (dense * X).eval());
// Accumulation forms exercised by the iterative solvers.
Vec y = Vec::Random(m);
Vec y0 = y;
y.noalias() += H * x;
VERIFY_IS_APPROX(y, (y0 + dense * x).eval());
}
// Reviewer regression (MR 2688): applying an exact 40x40 exchange matrix (as a
// Hankel operator, FFT tier) to a vector of huge finite entries must return the
// unchanged finite values, not NaNs: the FFT intermediates overflow unless each
// column is scaled by an exact power of two derived from the column's and the
// symbol's magnitudes.
void test_hankel_fft_overflow() {
typedef Matrix<double, Dynamic, 1> Vec;
const Index n = 40;
const double huge = (std::numeric_limits<double>::max)() / 16;
// H(i,j) = h[i+j] with h = e_{n-1} is the exchange matrix (anti-identity).
Vec h = Vec::Zero(2 * n - 1);
h[n - 1] = 1.0;
Hankel<double> H(h.head(n), h.tail(n));
Vec x = Vec::Constant(n, huge);
Vec y = H * x;
VERIFY(y.allFinite());
VERIFY_IS_APPROX((y / huge).eval(), Vec::Ones(n).eval()); // y == x.reverse() == x
// A huge generator makes a huge symbol; its exponent must be scaled out too.
Vec hh = Vec::Zero(2 * n - 1);
hh[n - 1] = huge;
Hankel<double> Hh(hh.head(n), hh.tail(n));
Vec z = Hh * Vec::Ones(n);
VERIFY(z.allFinite());
VERIFY_IS_APPROX((z / huge).eval(), Vec::Ones(n).eval());
// A genuine NaN input still propagates -- through the direct kernel, entrywise
// (see test_hankel_nonfinite_product); the scaling must not launder non-finite
// inputs into finite outputs.
Vec xn = Vec::Random(n);
xn[n / 2] = std::numeric_limits<double>::quiet_NaN();
Vec yn = H * xn;
VERIFY(yn.hasNaN());
}
// The scaling exponents are derived from component-wise magnitudes: a finite
// complex value near the overflow threshold has a non-representable modulus,
// which would otherwise disable the scaling and turn an exactly representable
// product into NaN.
template <typename RealScalar>
void test_hankel_fft_complex_boundary(Index n) {
typedef std::complex<RealScalar> Complex;
typedef Matrix<RealScalar, Dynamic, 1> RVec;
typedef Matrix<Complex, Dynamic, 1> CVec;
const RealScalar kFftRoundTripTol = RealScalar(100) * NumTraits<RealScalar>::epsilon();
const RealScalar big = RealScalar(0.75) * (std::numeric_limits<RealScalar>::max)();
// Exchange matrix with a complex generator: the product returns the reversed
// right-hand side -- the input itself, being constant -- even though |x_k|
// overflows.
CVec h = CVec::Zero(2 * n - 1);
h[n - 1] = Complex(1);
Hankel<Complex> H(h.head(n), h.tail(n));
CVec x = CVec::Constant(n, Complex(big, big));
CVec y = H * x;
VERIFY(y.allFinite());
VERIFY(((y - x).cwiseAbs() / big).maxCoeff() <= kFftRoundTripTol);
// A real exchange operator applied to the same complex right-hand side takes
// the mixed-scalar product path.
RVec hr = RVec::Zero(2 * n - 1);
hr[n - 1] = RealScalar(1);
Hankel<RealScalar> Hr(hr.head(n), hr.tail(n));
y = Hr * x;
VERIFY(y.allFinite());
VERIFY(((y - x).cwiseAbs() / big).maxCoeff() <= kFftRoundTripTol);
}
// transpose()/conjugate()/adjoint() return owning temporaries (and so does
// makeHankel), 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 checks
// would read freed memory if the product held a reference instead.
template <typename Scalar>
void test_hankel_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<Hankel<Scalar>>::type>::value);
Vec h = Vec::Random(m + n - 1);
Hankel<Scalar> H(h.head(m), h.tail(n));
Mat dense = reference_hankel<Scalar>(h, m, n);
Vec y = Vec::Random(m);
auto expr = H.adjoint() * y; // the adjoint temporary dies with the full expression
Vec scribble = Vec::Random(m + n); // reuses the temporary's freed heap storage
Vec x = expr;
VERIFY_IS_APPROX(x, (dense.adjoint() * y).eval());
VERIFY_IS_EQUAL(scribble.size(), m + n);
Vec x2 = Vec::Random(n);
auto expr2 = makeHankel(h.head(m).eval(), h.tail(n).eval()) * x2; // factory-returned temporary
Vec scribble2 = Vec::Random(m + n);
Vec y2 = expr2;
VERIFY_IS_APPROX(y2, (dense * x2).eval());
VERIFY_IS_EQUAL(scribble2.size(), m + n);
}
// The products carry the default product tag, so plain assignment materializes a
// temporary exactly like a dense product and an aliased right-hand side is safe:
// without it, x = H * x would read a zeroed right-hand side and x += H * x would
// interleave destination writes with right-hand-side reads.
template <typename Scalar>
void test_hankel_aliased_product(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
// Square so that the destination and the right-hand side can alias.
Vec h = Vec::Random(2 * n - 1);
Hankel<Scalar> H(h.head(n), h.tail(n));
Mat dense = reference_hankel<Scalar>(h, n, n);
Vec x = Vec::Random(n);
Vec y = x;
y = H * y;
VERIFY_IS_APPROX(y, (dense * x).eval());
y = x;
y += H * y;
VERIFY_IS_APPROX(y, (x + dense * x).eval());
y = x;
y -= H * y;
VERIFY_IS_APPROX(y, (x - dense * x).eval());
Mat X = Mat::Random(n, 3);
Mat Y = X;
Y = H * Y;
VERIFY_IS_APPROX(Y, (dense * X).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-assignments where the destination is
// resized by the assignment.
template <typename Scalar>
void test_hankel_aliased_expression(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec h = Vec::Random(2 * n - 1);
Hankel<Scalar> H(h.head(n), h.tail(n));
Mat dense = reference_hankel<Scalar>(h, n, n);
// Right-hand-side expression referencing the destination.
Vec x = Vec::Random(n), x0 = x;
x = H * (x + Vec::Ones(n));
VERIFY_IS_APPROX(x, (dense * (x0 + 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) = H * buf.tail(n);
VERIFY_IS_APPROX(buf.head(n).eval(), expected);
// Rectangular self-assignment: z = H * z resizes the destination, so the
// product must be captured before the destination storage is touched.
const Index m = n + 3;
Vec hr = Vec::Random(m + n - 1);
Hankel<Scalar> Hr(hr.head(m), hr.tail(n));
Mat denseR = reference_hankel<Scalar>(hr, m, n);
Vec z = Vec::Random(n), z0 = z;
z = Hr * z;
VERIFY_IS_EQUAL(z.size(), m);
VERIFY_IS_APPROX(z, (denseR * z0).eval());
}
// 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_hankel_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 h = RVec::Random(m + n - 1);
Hankel<RealScalar> H(h.head(m), h.tail(n));
CMat dense = reference_hankel<RealScalar>(h, m, n).template cast<Complex>();
CVec x = CVec::Random(n);
CVec y = H * x;
VERIFY_IS_APPROX(y, (dense * x).eval());
CVec y0 = CVec::Random(m);
y = y0;
y.noalias() += H * x;
VERIFY_IS_APPROX(y, (y0 + dense * x).eval());
CVec hc = CVec::Random(m + n - 1);
Hankel<Complex> Hc(hc.head(m), hc.tail(n));
CMat denseC = reference_hankel<Complex>(hc, m, n);
RVec xr = RVec::Random(n);
CVec z = Hc * xr;
VERIFY_IS_APPROX(z, (denseC * xr).eval());
}
// Entrywise IEEE comparison for the non-finite tests: NaNs match NaNs,
// infinities match by value (sign included), finite entries match to roundoff.
// VERIFY_IS_APPROX would reject any output containing NaN.
template <typename D1, typename D2>
bool ieee_entrywise_match(const D1& a, const D2& b) {
if (a.rows() != b.rows() || a.cols() != b.cols()) return false;
for (Index j = 0; j < a.cols(); ++j)
for (Index i = 0; i < a.rows(); ++i) {
const typename D1::Scalar x = a(i, j), y = b(i, j);
if (x == y) continue; // finite match or same-signed infinities
if ((x != x) && (y != y)) continue; // both NaN
if (!test_isApprox(x, y)) return false; // finite roundoff
}
return true;
}
// Scalar-loop product: the mathematically transparent IEEE reference for the
// non-finite tests. Eigen's own vectorized complex kernels can smear a single
// infinity into NaN (Inf - Inf across the split real/imaginary accumulators), so
// the dense product is not a faithful entrywise reference for non-finite data.
template <typename Scalar>
Matrix<Scalar, Dynamic, 1> reference_product_ieee(const Matrix<Scalar, Dynamic, Dynamic>& A,
const Matrix<Scalar, Dynamic, 1>& x) {
Matrix<Scalar, Dynamic, 1> y(A.rows());
for (Index i = 0; i < A.rows(); ++i) {
Scalar acc(0);
for (Index j = 0; j < A.cols(); ++j) acc += A(i, j) * x[j];
y[i] = acc;
}
return y;
}
// A single Inf or NaN in the data must propagate like the reference product --
// through the dot products that touch it -- instead of being smeared into NaNs
// across the whole output by the transforms. The reviewer reproducer (MR 2688):
// the 40x40 exchange matrix, as a Hankel operator on the FFT tier, applied to a
// vector with one +Inf must return +Inf in the mirrored entry; the remaining
// entries are NaN through the IEEE 0*Inf terms of the dense dot products.
template <typename Scalar>
void test_hankel_nonfinite_product(Index n) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
const RealScalar inf = std::numeric_limits<RealScalar>::infinity();
const RealScalar nan = std::numeric_limits<RealScalar>::quiet_NaN();
// The exchange matrix is the Hankel operator generated by h = e_{n-1}.
Vec h = Vec::Zero(2 * n - 1);
h[n - 1] = Scalar(1);
Hankel<Scalar> H(h.head(n), h.tail(n));
Mat dense = reference_hankel<Scalar>(h, n, n);
// Inf in the right-hand side: the mirrored entry must be +Inf (for a complex
// operator its real part; the 0*Inf cross term makes the imaginary part NaN).
Vec x = Vec::Random(n);
x[n / 4] = Scalar(inf);
Vec y = H * x;
VERIFY(ieee_entrywise_match(y, reference_product_ieee(dense, x)));
VERIFY(numext::real(y[n - 1 - n / 4]) == inf);
// NaN in the right-hand side.
Vec xn = Vec::Random(n);
xn[n - 1] = Scalar(nan);
VERIFY(ieee_entrywise_match((H * xn).eval(), reference_product_ieee(dense, xn)));
// Mixed multi-column right-hand side: the non-finite column falls back to the
// direct kernel individually while the finite column keeps the FFT path.
Mat Xm(n, 2);
Xm.col(0) = Vec::Random(n);
Xm.col(1) = x;
Mat Ym = H * Xm;
VERIFY_IS_APPROX(Ym.col(0).eval(), (dense * Xm.col(0)).eval());
VERIFY(ieee_entrywise_match(Ym.col(1).eval(), reference_product_ieee(dense, Vec(Xm.col(1)))));
// Inf in the generating sequence: the operator itself is non-finite, whatever
// the right-hand side.
Vec h2 = Vec::Random(2 * n - 1);
h2[1] = Scalar(-inf);
Hankel<Scalar> H2(h2.head(n), h2.tail(n));
Mat dense2 = reference_hankel<Scalar>(h2, n, n);
Vec x2 = Vec::Random(n);
VERIFY(ieee_entrywise_match((H2 * x2).eval(), reference_product_ieee(dense2, x2)));
// The Circulant and Toeplitz operators share the fallback pattern.
Vec c = Vec::Random(n);
Circulant<Scalar> C(c);
Mat denseC = reference_circulant<Scalar>(c);
VERIFY(ieee_entrywise_match((C * x).eval(), reference_product_ieee(denseC, x)));
Vec tc = Vec::Random(n), tr = Vec::Random(n);
tr[0] = tc[0];
tr[n / 2] = Scalar(inf);
Toeplitz<Scalar> T(tc, tr);
Mat denseT = reference_toeplitz<Scalar>(tc, tr);
VERIFY(ieee_entrywise_match((T * x2).eval(), reference_product_ieee(denseT, x2)));
// Non-finite right-hand sides of Circulant::solve take the direct inverse
// application; on the 1x1 operator this is a single scalar multiply by the
// inverse coefficient, checked against the same multiply on the dense inverse.
Vec b1(1);
b1[0] = Scalar(inf);
Circulant<Scalar> C1(Vec(Vec::Constant(1, Scalar(2))));
Mat inv1(1, 1);
inv1(0, 0) = Scalar(1) / Scalar(2);
VERIFY(ieee_entrywise_match(C1.solve(b1), reference_product_ieee(inv1, b1)));
}
// The transposed / adjoint / conjugated Hankel operators agree with the dense
// references, both materialized and through their fast products. For rectangular
// operators on the FFT tier this validates the phase-multiplication symbol reuse
// (the DFT shift theorem), including its sign.
template <typename Scalar>
void test_hankel_transpose(Index m, Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec h = Vec::Random(m + n - 1);
Hankel<Scalar> H(h.head(m), h.tail(n));
Mat dense = reference_hankel<Scalar>(h, m, n);
Mat Td = H.transpose();
VERIFY_IS_APPROX(Td, Mat(dense.transpose()));
Mat Ad = H.adjoint();
VERIFY_IS_APPROX(Ad, Mat(dense.adjoint()));
Mat Kd = H.conjugate();
VERIFY_IS_APPROX(Kd, Mat(dense.conjugate()));
Vec y = Vec::Random(m);
VERIFY_IS_APPROX((H.transpose() * y).eval(), (dense.transpose() * y).eval());
VERIFY_IS_APPROX((H.adjoint() * y).eval(), (dense.adjoint() * y).eval());
Vec x = Vec::Random(n);
VERIFY_IS_APPROX((H.conjugate() * x).eval(), (dense.conjugate() * x).eval());
// The generating sequence round-trips exactly; the symbol round-trips to
// rounding (the phase factors cancel only approximately when m != n).
Hankel<Scalar> Htt = H.transpose().transpose();
VERIFY_IS_EQUAL(Htt.generator(), h);
VERIFY_IS_APPROX(Htt.symbol(), H.symbol());
Mat Httd = Htt;
VERIFY_IS_APPROX(Httd, dense);
}
// A real square Hankel matrix is symmetric: transpose() is an exact fixed point.
template <typename Scalar>
void test_hankel_symmetry(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec h = Vec::Random(2 * n - 1);
Hankel<Scalar> H(h.head(n), h.tail(n));
Hankel<Scalar> Ht = H.transpose();
VERIFY_IS_EQUAL(Ht.generator(), h);
VERIFY_IS_EQUAL(Ht.symbol(), H.symbol()); // square: no phase multiplication at all
Mat dense = H;
VERIFY_IS_APPROX(dense, Mat(dense.transpose()));
}
// The column-reversed Toeplitz equivalent: toToeplitz() == H * E.
template <typename Scalar>
void test_hankel_to_toeplitz(Index m, Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec h = Vec::Random(m + n - 1);
Hankel<Scalar> H(h.head(m), h.tail(n));
Mat dense = reference_hankel<Scalar>(h, m, n);
Mat Td = H.toToeplitz();
VERIFY_IS_APPROX(Td, Mat(dense.rowwise().reverse()));
}
// Direct O(n^2) solve of a square Hankel system through the Toeplitz equivalent.
template <typename Scalar>
void test_hankel_solve(Index n) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
// h[n-1] is the constant anti-diagonal that becomes the diagonal of the
// Toeplitz equivalent; boosting it makes the system well conditioned.
Vec h = Vec::Random(2 * n - 1);
h[n - 1] += Scalar(RealScalar(2 * n));
Hankel<Scalar> H(h.head(n), h.tail(n));
Mat dense = reference_hankel<Scalar>(h, n, n);
Vec b = Vec::Random(n);
Vec x = H.solve(b);
VERIFY_IS_APPROX((dense * x).eval(), b);
VERIFY_IS_APPROX(x, dense.fullPivLu().solve(b).eval());
Mat B = Mat::Random(n, 3);
Mat X = H.solve(B);
VERIFY_IS_APPROX(X, dense.fullPivLu().solve(B).eval());
}
// A zero constant anti-diagonal makes the Toeplitz equivalent's diagonal zero, so
// its 1x1 leading minor is singular and the solve must go through the look-ahead
// init/block-step machinery. Verified through the residual (the look-ahead
// Levinson algorithm is weakly stable).
template <typename Scalar>
void test_hankel_solve_lookahead(Index n) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec h = Vec::Random(2 * n - 1);
h[n - 1] = Scalar(0);
Hankel<Scalar> H(h.head(n), h.tail(n));
Mat dense = reference_hankel<Scalar>(h, n, n);
Vec b = dense * Vec::Ones(n);
Vec x = H.solve(b);
const RealScalar tol = RealScalar(5e6) * NumTraits<RealScalar>::epsilon(); // ~1e-9 in double
VERIFY((dense * x - b).norm() <= tol * b.norm());
}
// The Hilbert matrix is the canonical ill-conditioned Hankel matrix: H(i,j) =
// 1/(i+j+1), i.e. h[k] = 1/(k+1). Deterministic, so the tolerances below are
// calibrated with a ~100x margin against measured values. The look-ahead Levinson
// solver is weakly stable: the forward error grows like eps * cond, but the
// residual stays small far beyond the point where the solution itself is lost.
void test_hankel_hilbert() {
typedef Matrix<double, Dynamic, 1> Vec;
typedef Matrix<double, Dynamic, Dynamic> Mat;
auto residual = [](Index n) {
Vec h(2 * n - 1);
for (Index k = 0; k < 2 * n - 1; ++k) h[k] = 1.0 / double(k + 1);
Hankel<double> H(h.head(n), h.tail(n));
Mat dense = reference_hankel<double>(h, n, n);
Vec b = dense * Vec::Ones(n);
Vec x = H.solve(b);
return (dense * x - b).norm() / b.norm();
};
const double eps = NumTraits<double>::epsilon();
VERIFY(residual(8) <= 5e6 * eps); // ~1e-9; cond ~ 1.5e10, measured residual ~ 1.4e-11
VERIFY(residual(12) <= 5e10 * eps); // ~1e-5; cond ~ 1.7e16, measured residual ~ 1.1e-7
// The condition estimate of the Toeplitz equivalent must see the
// ill-conditioning (measured ~ 2.7e15 at n = 12).
Vec h(23);
for (Index k = 0; k < 23; ++k) h[k] = 1.0 / double(k + 1);
Hankel<double> H(h.head(12), h.tail(12));
LookAheadLevinson<double> lev(H.toToeplitz());
VERIFY(lev.conditionEstimate() >= 1e14);
}
// With adjoint() available, a rectangular Hankel feeds the matrix-free
// least-squares solvers end to end.
template <typename Scalar>
void test_hankel_least_squares(Index m, Index n) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec h = Vec::Random(m + n - 1);
Hankel<Scalar> H(h.head(m), h.tail(n));
Mat dense = reference_hankel<Scalar>(h, m, n);
Vec b = Vec::Random(m);
LSMR<Hankel<Scalar>, IdentityPreconditioner> lsmr;
lsmr.setTolerance(RealScalar(1e-12)).setMaxIterations(20 * n);
lsmr.compute(H);
Vec x = lsmr.solve(b);
VERIFY(lsmr.info() == Success);
// Check LSMR's own convergence criterion -- the normal-equations residual --
// rather than the forward error, whose accuracy degrades with cond^2 and would
// make the test flaky for unlucky random operators.
Vec r = b - dense * x;
const RealScalar tol = RealScalar(5e7) * NumTraits<RealScalar>::epsilon(); // ~1e-8, relative
const RealScalar absTol = RealScalar(5e3) * NumTraits<RealScalar>::epsilon(); // ~1e-12, floor
VERIFY((dense.adjoint() * r).norm() <= tol * dense.norm() * r.norm() + absTol * b.norm());
}
template <typename Scalar, int M, int N>
void test_hankel_fixed() {
typedef Matrix<Scalar, M, 1> ColVec;
typedef Matrix<Scalar, N, 1> RowVec;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, M, N> MatMN;
Vec h = Vec::Random(M + N - 1);
ColVec c = h.template head<M>();
RowVec r = h.template tail<N>();
Hankel<Scalar, M, N> H(c, r);
STATIC_CHECK((Hankel<Scalar, M, N>::RowsAtCompileTime == M));
STATIC_CHECK((Hankel<Scalar, M, N>::ColsAtCompileTime == N));
STATIC_CHECK((internal::remove_all_t<decltype(makeHankel(c, r))>::ColsAtCompileTime == N));
STATIC_CHECK((internal::remove_all_t<decltype(H.transpose())>::RowsAtCompileTime == N));
STATIC_CHECK((internal::remove_all_t<decltype(H.transpose())>::ColsAtCompileTime == M));
MatMN dense = H;
VERIFY_IS_APPROX(dense, MatMN(reference_hankel<Scalar>(h, M, N)));
RowVec x = RowVec::Random();
Matrix<Scalar, M, 1> y = H * x;
VERIFY_IS_APPROX(y, (dense * x).eval());
ColVec w = ColVec::Random();
RowVec tw = H.transpose() * w;
VERIFY_IS_APPROX(tw, (dense.transpose() * w).eval());
}
// solve() on a fixed-size operator returns a fixed-size result (reviewer
// regression for MR 2688: the return type used to have Dynamic rows). Either
// fixed dimension of the square operator determines the compile-time row count.
template <typename Scalar, int N>
void test_hankel_fixed_solve() {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, N, 1> VecN;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, N, N> MatN;
STATIC_CHECK((internal::remove_all_t<decltype(std::declval<const Hankel<Scalar, N, N>&>().solve(
std::declval<const VecN&>()))>::RowsAtCompileTime == N));
STATIC_CHECK((internal::remove_all_t<decltype(std::declval<const Hankel<Scalar, Dynamic, N>&>().solve(
std::declval<const Vec&>()))>::RowsAtCompileTime == N));
STATIC_CHECK((internal::remove_all_t<decltype(std::declval<const Hankel<Scalar, N, Dynamic>&>().solve(
std::declval<const Vec&>()))>::RowsAtCompileTime == N));
// h[N-1] is the constant anti-diagonal that becomes the diagonal of the
// Toeplitz equivalent; boosting it makes the system well conditioned.
Vec h = Vec::Random(2 * N - 1);
h[N - 1] += Scalar(RealScalar(2 * N));
Hankel<Scalar, N, N> H(h.template head<N>(), h.template tail<N>());
MatN dense = H;
VecN b = VecN::Random();
VecN x = H.solve(b);
VERIFY_IS_APPROX((dense * x).eval(), b);
Matrix<Scalar, N, 2> B = Matrix<Scalar, N, 2>::Random();
STATIC_CHECK((internal::remove_all_t<decltype(H.solve(B))>::ColsAtCompileTime == 2));
Matrix<Scalar, N, 2> X = H.solve(B);
VERIFY_IS_APPROX((dense * X).eval(), B);
}
// The transposed / adjoint / conjugated operators agree with the dense references,
// both materialized and through their fast products, which reuse the cached symbol
// (index reversal / conjugation) instead of computing new FFTs.
template <typename Scalar>
void test_circulant_transpose(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec c = Vec::Random(n);
Circulant<Scalar> C(c);
Mat dense = reference_circulant<Scalar>(c);
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 x = Vec::Random(n);
VERIFY_IS_APPROX((C.transpose() * x).eval(), (dense.transpose() * x).eval());
VERIFY_IS_APPROX((C.adjoint() * x).eval(), (dense.adjoint() * x).eval());
VERIFY_IS_APPROX((C.conjugate() * x).eval(), (dense.conjugate() * x).eval());
// The transposition family round-trips exactly: generators and symbols are pure
// permutations / conjugations, so no FFT is recomputed and no roundoff accrues.
Circulant<Scalar> Ctt = C.transpose().transpose();
VERIFY_IS_EQUAL(Ctt.column(), c);
VERIFY_IS_EQUAL(Ctt.symbol(), C.symbol());
Circulant<Scalar> Caa = C.adjoint().adjoint();
VERIFY_IS_EQUAL(Caa.column(), c);
VERIFY_IS_EQUAL(Caa.symbol(), C.symbol());
}
template <typename Scalar>
void test_toeplitz_transpose(Index m, Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
// r[0] is deliberately left random (it is documented as ignored): the transposed
// operator must take its diagonal from c[0], not from r[0].
Vec c = Vec::Random(m), r = Vec::Random(n);
Toeplitz<Scalar> T(c, r);
Mat dense = reference_toeplitz<Scalar>(c, r);
Mat Td = T.transpose();
VERIFY_IS_APPROX(Td, Mat(dense.transpose()));
Mat Ad = T.adjoint();
VERIFY_IS_APPROX(Ad, Mat(dense.adjoint()));
Mat Kd = T.conjugate();
VERIFY_IS_APPROX(Kd, Mat(dense.conjugate()));
Vec y = Vec::Random(m);
VERIFY_IS_APPROX((T.transpose() * y).eval(), (dense.transpose() * y).eval());
VERIFY_IS_APPROX((T.adjoint() * y).eval(), (dense.adjoint() * y).eval());
Vec x = Vec::Random(n);
VERIFY_IS_APPROX((T.conjugate() * x).eval(), (dense.conjugate() * x).eval());
// Exact round trip (row[0] is normalized to the diagonal value on the way).
Toeplitz<Scalar> Ttt = T.transpose().transpose();
VERIFY_IS_EQUAL(Ttt.column(), c);
if (n > 1) VERIFY_IS_EQUAL(Vec(Ttt.row().tail(n - 1)), Vec(r.tail(n - 1)));
VERIFY_IS_EQUAL(Ttt.symbol(), T.symbol());
}
// 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_circulant_delayed_product(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
STATIC_CHECK(!std::is_reference<typename internal::ref_selector<Circulant<Scalar>>::type>::value);
Vec c = Vec::Random(n), x = Vec::Random(n);
Circulant<Scalar> C(c);
Mat dense = reference_circulant<Scalar>(c);
auto expr = C.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
}
template <typename Scalar>
void test_toeplitz_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<Toeplitz<Scalar>>::type>::value);
Vec c = Vec::Random(m), r = Vec::Random(n), y = Vec::Random(m);
Toeplitz<Scalar> T(c, r);
Mat dense = reference_toeplitz<Scalar>(c, r);
auto expr = T.adjoint() * y;
Vec scribble = Vec::Random(m + n);
Vec x = expr;
VERIFY_IS_APPROX(x, (dense.adjoint() * y).eval());
VERIFY_IS_EQUAL(scribble.size(), m + n);
}
// The products are tagged AliasFreeProduct, so no temporary shields an aliased
// right-hand side: the shared product implementation must copy it instead.
// Without the copy, x = C * x reads a zeroed right-hand side and x += C * x
// interleaves destination writes with right-hand-side reads.
template <typename Scalar>
void test_circulant_aliased_product(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec c = Vec::Random(n);
Circulant<Scalar> C(c);
Mat dense = reference_circulant<Scalar>(c);
Vec x = Vec::Random(n);
Vec y = x;
y = C * y;
VERIFY_IS_APPROX(y, (dense * x).eval());
y = x;
y += C * y;
VERIFY_IS_APPROX(y, (x + dense * x).eval());
y = x;
y -= C * y;
VERIFY_IS_APPROX(y, (x - dense * x).eval());
Mat X = Mat::Random(n, 3);
Mat Y = X;
Y = C * Y;
VERIFY_IS_APPROX(Y, (dense * X).eval());
}
template <typename Scalar>
void test_toeplitz_aliased_product(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
// Square so that the destination and the right-hand side can alias.
Vec c = Vec::Random(n), r = Vec::Random(n);
r[0] = c[0];
Toeplitz<Scalar> T(c, r);
Mat dense = reference_toeplitz<Scalar>(c, r);
Vec x = Vec::Random(n);
Vec y = x;
y = T * y;
VERIFY_IS_APPROX(y, (dense * x).eval());
y = x;
y += T * y;
VERIFY_IS_APPROX(y, (x + dense * x).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-assignments where the destination is
// resized by the assignment.
template <typename Scalar>
void test_structured_aliased_expression(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec c = Vec::Random(n);
Circulant<Scalar> C(c);
Mat dense = reference_circulant<Scalar>(c);
// Right-hand-side expression referencing the destination.
Vec x = Vec::Random(n), x0 = x;
x = C * (x + Vec::Ones(n));
VERIFY_IS_APPROX(x, (dense * (x0 + 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: x = T * x resizes the destination, so the
// product must be captured before the destination storage is touched.
const Index m = n + 3;
Vec tc = Vec::Random(m), tr = Vec::Random(n);
tr[0] = tc[0];
Toeplitz<Scalar> T(tc, tr);
Mat denseT = reference_toeplitz<Scalar>(tc, tr);
Vec z = Vec::Random(n), z0 = z;
z = T * z;
VERIFY_IS_EQUAL(z.size(), m);
VERIFY_IS_APPROX(z, (denseT * z0).eval());
}
// 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_circulant_mixed_scalar(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 c = RVec::Random(n);
Circulant<RealScalar> C(c);
CMat dense = reference_circulant<RealScalar>(c).template cast<Complex>();
CVec x = CVec::Random(n);
CVec y = C * x;
VERIFY_IS_APPROX(y, (dense * x).eval());
CVec y0 = CVec::Random(n);
y = y0;
y.noalias() += C * x;
VERIFY_IS_APPROX(y, (y0 + dense * x).eval());
CVec cc = CVec::Random(n);
Circulant<Complex> Cc(cc);
CMat denseC = reference_circulant<Complex>(cc);
RVec xr = RVec::Random(n);
CVec z = Cc * xr;
VERIFY_IS_APPROX(z, (denseC * xr).eval());
}
template <typename RealScalar>
void test_toeplitz_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 c = RVec::Random(m), r = RVec::Random(n);
r[0] = c[0];
Toeplitz<RealScalar> T(c, r);
CMat dense = reference_toeplitz<RealScalar>(c, r).template cast<Complex>();
CVec x = CVec::Random(n);
CVec y = T * x;
VERIFY_IS_APPROX(y, (dense * x).eval());
}
// The FFT product path accumulates up to p addends per transform, so a finite
// right-hand side near the overflow threshold (or a huge generator) used to
// overflow inside the transforms and return NaN for a representable result. The
// power-of-two scaled path must stay finite and accurate.
template <typename RealScalar>
void test_structured_fft_overflow(Index n) {
typedef Matrix<RealScalar, Dynamic, 1> Vec;
// FFT-transform roundoff bound for the identity round trip.
const RealScalar kFftRoundTripTol = RealScalar(100) * NumTraits<RealScalar>::epsilon();
const RealScalar huge = (std::numeric_limits<RealScalar>::max)() / RealScalar(16);
// Identity circulant applied to a huge finite vector returns it unchanged.
Vec c = Vec::Zero(n);
c[0] = RealScalar(1);
Circulant<RealScalar> C(c);
Vec x = Vec::Constant(n, huge);
Vec y = C * x;
VERIFY(y.allFinite());
VERIFY(((y - x).cwiseAbs() / huge).maxCoeff() <= kFftRoundTripTol);
// A huge generator applied to a moderate vector: every output entry is huge
// but representable.
Circulant<RealScalar> Ch(Vec(huge * c));
Vec ones = Vec::Ones(n);
Vec z = Ch * ones;
VERIFY(z.allFinite());
VERIFY(((z.array() - huge).abs() / huge).maxCoeff() <= kFftRoundTripTol);
// Identity Toeplitz, same huge right-hand side.
Vec r = Vec::Zero(n);
r[0] = c[0];
Toeplitz<RealScalar> T(c, r);
Vec w = T * x;
VERIFY(w.allFinite());
VERIFY(((w - x).cwiseAbs() / huge).maxCoeff() <= kFftRoundTripTol);
}
// The scaling exponents are derived from component-wise magnitudes: a finite
// complex value near the overflow threshold has a non-representable modulus,
// which would otherwise disable the scaling and turn an exactly representable
// product into NaN.
template <typename RealScalar>
void test_structured_fft_complex_boundary(Index n) {
typedef std::complex<RealScalar> Complex;
typedef Matrix<RealScalar, Dynamic, 1> RVec;
typedef Matrix<Complex, Dynamic, 1> CVec;
const RealScalar kFftRoundTripTol = RealScalar(100) * NumTraits<RealScalar>::epsilon();
const RealScalar big = RealScalar(0.75) * (std::numeric_limits<RealScalar>::max)();
// Identity circulant with a complex generator: the product returns the
// right-hand side unchanged even though |x_k| overflows.
CVec c = CVec::Zero(n);
c[0] = Complex(1);
Circulant<Complex> C(c);
CVec x = CVec::Constant(n, Complex(big, big));
CVec y = C * x;
VERIFY(y.allFinite());
VERIFY(((y - x).cwiseAbs() / big).maxCoeff() <= kFftRoundTripTol);
// A real identity operator applied to the same complex right-hand side takes
// the mixed-scalar product path.
RVec cr = RVec::Zero(n);
cr[0] = RealScalar(1);
Circulant<RealScalar> Cr(cr);
y = Cr * x;
VERIFY(y.allFinite());
VERIFY(((y - x).cwiseAbs() / big).maxCoeff() <= kFftRoundTripTol);
}
// A single Inf or NaN in the data must propagate like the reference product --
// through the dot products that touch it -- instead of being smeared into NaNs
// across the whole output by the transforms.
template <typename Scalar>
void test_structured_nonfinite_product(Index n) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
const RealScalar inf = std::numeric_limits<RealScalar>::infinity();
const RealScalar nan = std::numeric_limits<RealScalar>::quiet_NaN();
Vec c = Vec::Random(n);
Circulant<Scalar> C(c);
Mat dense = reference_circulant<Scalar>(c);
// Inf in the right-hand side.
Vec x = Vec::Random(n);
x[n / 2] = Scalar(inf);
VERIFY(ieee_entrywise_match((C * x).eval(), reference_product_ieee(dense, x)));
// NaN in the right-hand side.
Vec xn = Vec::Random(n);
xn[n - 1] = Scalar(nan);
VERIFY(ieee_entrywise_match((C * xn).eval(), reference_product_ieee(dense, xn)));
// Mixed multi-column right-hand side: the non-finite column falls back to the
// direct kernel individually while the finite column keeps the FFT path.
Mat Xm(n, 2);
Xm.col(0) = Vec::Random(n);
Xm.col(1) = x;
Mat Ym = C * Xm;
VERIFY_IS_APPROX(Ym.col(0).eval(), (dense * Xm.col(0)).eval());
VERIFY(ieee_entrywise_match(Ym.col(1).eval(), reference_product_ieee(dense, Vec(Xm.col(1)))));
// Inf in the generator: the operator itself is non-finite, whatever the
// right-hand side.
Vec c2 = Vec::Random(n);
c2[1] = Scalar(-inf);
Circulant<Scalar> C2(c2);
Mat dense2 = reference_circulant<Scalar>(c2);
Vec x2 = Vec::Random(n);
VERIFY(ieee_entrywise_match((C2 * x2).eval(), reference_product_ieee(dense2, x2)));
// Toeplitz with an Inf in the row generator.
Vec tc = Vec::Random(n), tr = Vec::Random(n);
tr[0] = tc[0];
tr[n / 2] = Scalar(inf);
Toeplitz<Scalar> T(tc, tr);
Mat denseT = reference_toeplitz<Scalar>(tc, tr);
VERIFY(ieee_entrywise_match((T * x2).eval(), reference_product_ieee(denseT, x2)));
// Non-finite right-hand sides of solve() take the direct pseudo-inverse
// application; on the 1x1 operator this is a single scalar multiply by the
// inverse coefficient, checked against the same multiply on the dense inverse.
Vec b1(1);
b1[0] = Scalar(inf);
Circulant<Scalar> C1(Vec(Vec::Constant(1, Scalar(2))));
Mat pinv1 = Mat(C1.inverse());
VERIFY(ieee_entrywise_match(C1.solve(b1), reference_product_ieee(pinv1, b1)));
}
// Closed-form eigendecomposition: C * V = V * diag(eigenvalues) with V unitary.
template <typename Scalar>
void test_circulant_eigen(Index n) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef std::complex<RealScalar> Complex;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Complex, Dynamic, Dynamic> CMat;
Vec c = Vec::Random(n);
Circulant<Scalar> C(c);
CMat denseC = reference_circulant<Scalar>(c).template cast<Complex>();
Matrix<Complex, Dynamic, 1> lam = C.eigenvalues();
CMat V = C.eigenvectors();
VERIFY_IS_APPROX((denseC * V).eval(), (V * lam.asDiagonal()).eval());
VERIFY_IS_APPROX((V.adjoint() * V).eval(), CMat(CMat::Identity(n, n)));
}
// Closed-form SVD: the singular values match JacobiSVD, and U * S * V^H
// reconstructs the matrix with unitary factors.
template <typename Scalar>
void test_circulant_svd(Index n) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef std::complex<RealScalar> Complex;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
typedef Matrix<Complex, Dynamic, Dynamic> CMat;
Vec c = Vec::Random(n);
Circulant<Scalar> C(c);
Mat dense = reference_circulant<Scalar>(c);
Matrix<RealScalar, Dynamic, 1> sv = C.singularValues();
JacobiSVD<Mat> svd(dense);
VERIFY_IS_APPROX(sv, svd.singularValues());
// The symbol of a real operator is conjugate-symmetric, so every mode but the
// self-conjugate ones (k = 0 and, for even n, k = n/2) is tied with its mirror.
// The tie must be exact rather than accurate to roundoff: singularValues(),
// matrixU() and matrixV() each order the modes from their own symbol
// computation, and a pair ranked by its last bits can come out ordered one way
// in one of them and the other way in another -- pairing a left singular vector
// with the wrong right one. Runs of exactly equal values must therefore all be
// even, apart from those self-conjugate moduli (fewer runs are odd when a
// modulus coincidentally repeats).
if (!NumTraits<Scalar>::IsComplex) {
Index odd = 0;
for (Index t = 0; t < n;) {
Index run = t;
while (run < n && numext::equal_strict(sv[run], sv[t])) ++run;
if ((run - t) % 2 == 1) ++odd;
t = run;
}
VERIFY(odd <= (n % 2 == 0 ? 2 : 1));
}
CMat U = C.matrixU(), V = C.matrixV();
VERIFY_IS_APPROX((U * sv.template cast<Complex>().asDiagonal() * V.adjoint()).eval(),
CMat(dense.template cast<Complex>()));
VERIFY_IS_APPROX((U.adjoint() * U).eval(), CMat(CMat::Identity(n, n)));
VERIFY_IS_APPROX((V.adjoint() * V).eval(), CMat(CMat::Identity(n, n)));
}
// A rank-one circulant (the all-ones matrix): solve() must return the
// minimum-norm least-squares solution -- the SVD pseudo-inverse applied to b --
// for both consistent and inconsistent right-hand sides.
template <typename Scalar>
void test_circulant_minnorm_solve(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec c = Vec::Ones(n);
Circulant<Scalar> C(c);
VERIFY_IS_EQUAL(C.rank(), 1);
Mat dense = reference_circulant<Scalar>(c);
JacobiSVD<Mat, ComputeThinU | ComputeThinV> svd(dense);
Vec b = dense * Vec::Random(n); // consistent right-hand side
VERIFY_IS_APPROX(C.solve(b), svd.solve(b).eval());
Vec b2 = Vec::Random(n); // generally inconsistent; keep the projection non-zero
b2.array() += Scalar(2);
VERIFY_IS_APPROX(C.solve(b2), svd.solve(b2).eval());
}
// General rank-deficient circulant, synthesized by zeroing symbol entries. The
// numerical rank must count exactly the surviving entries and solve() must match
// the SVD pseudo-inverse. Scalar must be complex (the generator is an inverse DFT).
template <typename Scalar>
void test_circulant_rank_deficient(Index n, Index defect) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef std::complex<RealScalar> Complex;
typedef Matrix<Complex, Dynamic, 1> CVec;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
CVec s = CVec::Random(n);
s.array() += Complex(2); // keep the surviving moduli away from the threshold
for (Index k = 0; k < defect; ++k) s[2 * k + 1] = Complex(0);
CVec ct(n);
FFT<RealScalar> fft;
fft.inv(ct, s, n);
Vec c = ct;
Circulant<Scalar> C(c);
VERIFY_IS_EQUAL(C.rank(), n - defect);
Mat dense = reference_circulant<Scalar>(c);
JacobiSVD<Mat, ComputeThinU | ComputeThinV> svd(dense);
Vec b = Vec::Random(n);
VERIFY_IS_APPROX(C.solve(b), svd.solve(b).eval());
// The SVD factors must stay unitary and reconstruct the matrix even with zero
// singular values (exercising the arbitrary-phase completion of U and the
// tie-handling of the shared sort).
typedef Matrix<Complex, Dynamic, Dynamic> CMat;
Matrix<RealScalar, Dynamic, 1> sv = C.singularValues();
VERIFY((sv.tail(defect).array() <= RealScalar(n) * NumTraits<RealScalar>::epsilon() * sv[0]).all());
CMat U = C.matrixU(), V = C.matrixV();
VERIFY_IS_APPROX((U * sv.template cast<Complex>().asDiagonal() * V.adjoint()).eval(),
CMat(dense.template cast<Complex>()));
VERIFY_IS_APPROX((U.adjoint() * U).eval(), CMat(CMat::Identity(n, n)));
VERIFY_IS_APPROX((V.adjoint() * V).eval(), CMat(CMat::Identity(n, n)));
}
// The zero operator: rank 0, pseudo-inverse solve identically zero, all singular
// values zero. Pins the clamped-threshold boundary behavior.
template <typename Scalar>
void test_circulant_zero(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
Circulant<Scalar> C(Vec(Vec::Zero(n)));
VERIFY_IS_EQUAL(C.rank(), 0);
VERIFY(C.singularValues().isZero());
Vec b = Vec::Random(n);
VERIFY(C.solve(b).isZero());
}
// A NaN in the generator must propagate to the solution (and count as non-zero in
// rank()) instead of being silently laundered into zeros by the rank threshold.
void test_circulant_nan_propagation(Index n) {
typedef Matrix<double, Dynamic, 1> Vec;
Vec c = Vec::Random(n);
c[n / 2] = std::numeric_limits<double>::quiet_NaN();
Circulant<double> C(c);
VERIFY_IS_EQUAL(C.rank(), n);
Vec b = Vec::Random(n);
Vec x = C.solve(b);
VERIFY(!(x.array() == x.array()).all());
}
template <typename Scalar>
void test_circulant_inverse(Index n) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec c = Vec::Random(n);
c[0] += Scalar(RealScalar(2 * n)); // diagonally dominant => safely invertible
Circulant<Scalar> C(c);
Mat dense = reference_circulant<Scalar>(c);
Mat inv = C.inverse();
VERIFY_IS_APPROX((inv * dense).eval(), Mat(Mat::Identity(n, n)));
// The inverse operator's fast product acts as a solve.
Vec b = Vec::Random(n);
VERIFY_IS_APPROX((C.inverse() * b).eval(), C.solve(b));
}
template <typename Scalar>
void test_circulant_determinant(Index n) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
// Diagonal dominance keeps every eigenvalue away from zero, so the determinant
// is well conditioned and comparable against the dense LU-based value.
Vec c = Vec::Random(n);
c[0] += Scalar(RealScalar(2 * n));
Circulant<Scalar> C(c);
Mat dense = reference_circulant<Scalar>(c);
VERIFY_IS_APPROX(C.determinant(), dense.determinant());
}
// Spectra with a wide dynamic range: the determinant is representable, but a
// plain product of the eigenvalues in FFT order overflows to infinity (or
// underflows to an exact zero) partway through. Pins the balanced accumulation
// in determinant().
void test_circulant_determinant_scaled() {
typedef Matrix<double, Dynamic, 1> Vec;
typedef Matrix<std::complex<double>, Dynamic, 1> CVec;
const Index n = 1000;
// 653 eigenvalues `lead` and 347 eigenvalues `rest`, arranged symmetrically
// (s[k] == s[n-k]) so the generator is real, with the leading 327 FFT-order
// indices all equal to `lead` so the plain running product leaves the
// representable range partway through.
auto generator = [n](double lead, double rest) {
Vec mag = Vec::Constant(n, rest);
mag[0] = lead;
for (Index k = 1; k <= 326; ++k) mag[k] = mag[n - k] = lead;
CVec s = mag.cast<std::complex<double>>();
CVec ct(n);
FFT<double> fft;
fft.inv(ct, s, n);
return Vec(ct.real());
};
// The spectrum is only reproduced up to the FFT round trip's forward error,
// and the determinant multiplies ~1e3 such factors.
const double tol = 1e8 * NumTraits<double>::epsilon();
{
// det = 10^653 * 10^-347 = 1e306; the naive partial product reaches 1e327.
Circulant<double> C(generator(10.0, 0.1));
const double det = C.determinant();
VERIFY((numext::isfinite)(det));
VERIFY(numext::abs(det / 1e306 - 1.0) <= tol);
}
{
// det = 10^-653 * 10^347 = 1e-306; the naive partial product reaches 1e-327,
// well below the smallest subnormal, and flushes to an exact zero.
Circulant<double> C(generator(0.1, 10.0));
const double det = C.determinant();
VERIFY(det != 0.0);
VERIFY(numext::abs(det / 1e-306 - 1.0) <= tol);
}
}
// The rank decision at the clamped threshold (the smallest normal number): a
// smallest-normal symbol entry is still inverted -- the comparison is strict,
// matching SVDBase::rank(), which likewise reports rank one -- a subnormal entry
// is treated as an exact zero, and non-finite entries count as non-zero.
void test_circulant_rank_boundaries() {
typedef Matrix<double, Dynamic, 1> Vec;
const double mn = (std::numeric_limits<double>::min)();
const Vec b = Vec::Ones(1);
{
Circulant<double> C(Vec(Vec::Constant(1, mn)));
VERIFY_IS_EQUAL(C.rank(), 1);
Vec x = C.solve(b);
VERIFY((numext::isfinite)(x[0]));
VERIFY_IS_APPROX(x[0], 1.0 / mn);
}
{
Circulant<double> C(Vec(Vec::Constant(1, mn / 2))); // subnormal
VERIFY_IS_EQUAL(C.rank(), 0);
VERIFY(C.solve(b).isZero());
}
{
Circulant<double> C(Vec(Vec::Constant(1, std::numeric_limits<double>::infinity())));
VERIFY_IS_EQUAL(C.rank(), 1);
VERIFY((numext::isinf)(C.determinant()));
}
}
// True where a subnormal result survives: under flush-to-zero -- NVHPC's default
// -fast, for one -- it is an exact zero instead. The volatile operands keep the
// compiler from folding the probe under IEEE semantics the run time does not use.
bool subnormals_survive() {
volatile double vtiny = (std::numeric_limits<double>::min)();
volatile double vhalf = 0.5;
return !numext::is_exactly_zero(vtiny * vhalf);
}
// A finite complex symbol entry near the overflow threshold has a
// non-representable modulus. The rank threshold used to be computed from the raw
// moduli, turning it into infinity: the rank was under-reported and solve()
// zeroed valid Fourier modes. Both are now evaluated in an exactly rescaled
// frame.
void test_circulant_rank_complex_boundary() {
typedef std::complex<double> Complex;
typedef Matrix<double, Dynamic, 1> Vec;
typedef Matrix<Complex, Dynamic, 1> CVec;
const double mx = (std::numeric_limits<double>::max)();
// The length-one shortcut has no FFTs, but its pointwise division still needs
// scaling: a huge right-hand side over the balanced O(1) symbol overflows before
// the symbol exponent is folded back, though the result itself is moderate.
const Vec oneCol = Vec::Constant(1, 0.75 * mx);
const Vec oneB = Vec::Constant(1, 0.25 * mx);
const Vec oneX = Circulant<double>(oneCol).solve(oneB);
const double oneExpected = oneB[0] / oneCol[0];
VERIFY(oneX.allFinite());
VERIFY(numext::abs(oneX[0] / oneExpected - 1.0) <= 16 * NumTraits<double>::epsilon());
// n = 2: the symbol is exactly [c0 + c1, c0 - c1], so pick the generator from
// the desired spectrum. |s0| overflows while both of its components are finite.
const Complex s0(0.75 * mx, 0.75 * mx), s1(1e300, 0.0);
CVec c(2);
c[0] = (s0 + s1) * 0.5;
c[1] = (s0 - s1) * 0.5;
Circulant<Complex> C(c);
VERIFY_IS_EQUAL(C.rank(), 2);
// The second Fourier mode must be inverted, not zeroed: a product of a small
// vector solves back to that vector (the accuracy is limited by the condition
// number |s0| / |s1| ~ 2.5e8).
CVec x0(2);
x0[0] = Complex(1e-10, -2e-10);
x0[1] = Complex(-3e-10, 1e-10);
CVec b = C * x0;
VERIFY(b.allFinite());
CVec x = C.solve(b);
VERIFY(((x - x0).cwiseAbs().maxCoeff() / x0.cwiseAbs().maxCoeff()) <= 1e-6);
// A large right-hand side in the smaller retained mode must be scaled for the
// division's amplification as well as for the transforms: the quotient is
// moderate, but the balanced-frame division overflows without it.
const double h = 0.25 * mx;
CVec largeB(2);
largeB << Complex(h), Complex(-h);
const CVec largeX = C.solve(largeB);
const double expected = h / numext::real(C.symbol()[1]);
CVec expectedX(2);
expectedX << Complex(expected), Complex(-expected);
VERIFY(largeX.allFinite());
VERIFY(((largeX - expectedX).cwiseAbs().maxCoeff() / expected) <= 32 * NumTraits<double>::epsilon());
// The reciprocal of an entry near the overflow boundary is subnormal but
// representable, and must not collapse to zero: formed as conj(z)/|z|^2, or by
// Smith's algorithm, it overflows to exactly zero and drops the mode. Gated on
// flush-to-zero, where that entry is not representable at all -- which is why
// solve() divides by the symbol rather than multiplying by its reciprocal.
const CVec sinv = C.inverse().symbol();
VERIFY(sinv.allFinite());
if (subnormals_survive()) VERIFY_IS_EQUAL((sinv.array() == Complex(0)).count(), 0);
// A genuinely negligible second entry still truncates in the scaled frame.
CVec c2(2);
c2[0] = (s0 + Complex(1)) * 0.5;
c2[1] = (s0 - Complex(1)) * 0.5;
VERIFY_IS_EQUAL(Circulant<Complex>(c2).rank(), 1);
}
// Fixed-size operators: generators are stored in fixed-size vectors, products and
// solves return fixed-size results, and small sizes go through the coeff-based
// product dispatch.
template <typename Scalar, int N>
void test_circulant_fixed() {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, N, 1> VecN;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, N, N> MatN;
VecN c = VecN::Random();
c[0] += Scalar(RealScalar(2 * N)); // well conditioned for the solve below
Circulant<Scalar, N> C(c);
STATIC_CHECK((Circulant<Scalar, N>::RowsAtCompileTime == N));
STATIC_CHECK((internal::remove_all_t<decltype(makeCirculant(c))>::RowsAtCompileTime == N));
MatN dense = C;
VERIFY_IS_APPROX(dense, MatN(reference_circulant<Scalar>(Vec(c))));
VecN x = VecN::Random();
VecN y = C * x;
VERIFY_IS_APPROX(y, (dense * x).eval());
VecN b = VecN::Random();
VecN xs = C.solve(b);
VERIFY_IS_APPROX((dense * xs).eval(), b);
STATIC_CHECK((internal::remove_all_t<decltype(C.transpose())>::RowsAtCompileTime == N));
VecN xt = C.transpose() * x;
VERIFY_IS_APPROX(xt, (dense.transpose() * x).eval());
}
template <typename Scalar, int M, int N>
void test_toeplitz_fixed() {
typedef Matrix<Scalar, M, 1> ColVec;
typedef Matrix<Scalar, N, 1> RowVec;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, M, N> MatMN;
ColVec c = ColVec::Random();
RowVec r = RowVec::Random();
r[0] = c[0];
Toeplitz<Scalar, M, N> T(c, r);
STATIC_CHECK((Toeplitz<Scalar, M, N>::RowsAtCompileTime == M));
STATIC_CHECK((Toeplitz<Scalar, M, N>::ColsAtCompileTime == N));
STATIC_CHECK((internal::remove_all_t<decltype(makeToeplitz(c, r))>::ColsAtCompileTime == N));
MatMN dense = T;
VERIFY_IS_APPROX(dense, MatMN(reference_toeplitz<Scalar>(Vec(c), Vec(r))));
RowVec x = RowVec::Random();
Matrix<Scalar, M, 1> y = T * x;
VERIFY_IS_APPROX(y, (dense * x).eval());
STATIC_CHECK((internal::remove_all_t<decltype(T.transpose())>::RowsAtCompileTime == N));
STATIC_CHECK((internal::remove_all_t<decltype(T.transpose())>::ColsAtCompileTime == M));
ColVec w = ColVec::Random();
RowVec tw = T.transpose() * w;
VERIFY_IS_APPROX(tw, (dense.transpose() * w).eval());
}
template <typename Scalar>
void test_matrix_free_cg(Index n) {
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
// Symmetric, strongly diagonally dominant circulant => SPD.
Vec c = Vec::Zero(n);
c[0] = Scalar(4);
c[1] = Scalar(-1);
c[n - 1] = Scalar(-1);
Circulant<Scalar> C(c);
Mat dense = reference_circulant<Scalar>(c);
Vec b = Vec::Random(n);
ConjugateGradient<Circulant<Scalar>, Lower | Upper, IdentityPreconditioner> cg;
cg.compute(C);
Vec x = cg.solve(b);
VERIFY(cg.info() == Success);
VERIFY_IS_APPROX((dense * x).eval(), b);
}
template <typename Scalar>
void test_matrix_free_gmres(Index n) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
// Strongly diagonally dominant (nonsymmetric) Toeplitz.
Vec c = Vec::Random(n) * Scalar(RealScalar(0.1));
Vec r = Vec::Random(n) * Scalar(RealScalar(0.1));
c[0] = Scalar(3);
r[0] = Scalar(3);
Toeplitz<Scalar> T(c, r);
Mat dense = reference_toeplitz<Scalar>(c, r);
Vec b = Vec::Random(n);
GMRES<Toeplitz<Scalar>, IdentityPreconditioner> gmres;
gmres.compute(T);
Vec x = gmres.solve(b);
VERIFY(gmres.info() == Success);
VERIFY_IS_APPROX((dense * x).eval(), b);
}
// With adjoint() available, the rectangular operators feed Eigen's matrix-free
// least-squares solvers end to end: both LSMR and LeastSquaresConjugateGradient
// evaluate mat.adjoint() * v through the fast product.
template <typename Scalar>
void test_matrix_free_least_squares(Index m, Index n) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec c = Vec::Random(m), r = Vec::Random(n);
c[0] += Scalar(RealScalar(4)); // boost the diagonal to bound the conditioning
Toeplitz<Scalar> T(c, r);
Mat dense = reference_toeplitz<Scalar>(c, r);
Vec b = Vec::Random(m);
Vec xref = dense.colPivHouseholderQr().solve(b);
// Forward-error bound for the diagonally-boosted (well-conditioned) operator.
const RealScalar tol = RealScalar(5e8) * NumTraits<RealScalar>::epsilon(); // ~1e-7 in double
LSMR<Toeplitz<Scalar>, IdentityPreconditioner> lsmr;
lsmr.setTolerance(RealScalar(1e-12)).setMaxIterations(10 * n);
lsmr.compute(T);
Vec x = lsmr.solve(b);
VERIFY(lsmr.info() == Success);
VERIFY((x - xref).norm() <= tol * xref.norm());
LeastSquaresConjugateGradient<Toeplitz<Scalar>, IdentityPreconditioner> lscg;
lscg.setTolerance(RealScalar(1e-12));
lscg.setMaxIterations(10 * n);
lscg.compute(T);
Vec x2 = lscg.solve(b);
VERIFY(lscg.info() == Success);
VERIFY((x2 - xref).norm() <= tol * xref.norm());
}
// Diagonally dominant (well-conditioned) Toeplitz: the look-ahead solver must agree
// with a dense LU solve.
template <typename Scalar>
void test_levinson_wellcond(Index n) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec c = Vec::Random(n), r = Vec::Random(n);
c[0] = r[0] = Scalar(RealScalar(2 * n));
Toeplitz<Scalar> T(c, r);
Mat dense = T;
Vec b = Vec::Random(n);
LookAheadLevinson<Scalar> lev(T);
VERIFY(lev.info() == Success);
Vec x = lev.solve(b);
VERIFY_IS_APPROX(x, dense.fullPivLu().solve(b).eval());
// Multiple right-hand sides.
Mat B = Mat::Random(n, 3);
VERIFY_IS_APPROX(lev.solve(B), dense.fullPivLu().solve(B).eval());
// Transposed and adjoint systems reuse the same factorization (persymmetry).
Vec xt = lev.transpose().solve(b);
VERIFY_IS_APPROX(xt, dense.transpose().fullPivLu().solve(b).eval());
Vec xa = lev.adjoint().solve(b);
VERIFY_IS_APPROX(xa, dense.adjoint().fullPivLu().solve(b).eval());
Mat Xt = lev.transpose().solve(B);
VERIFY_IS_APPROX(Xt, dense.transpose().fullPivLu().solve(B).eval());
}
// Indefinite / ill-conditioned matrices that force look-ahead block steps. The
// generators and required block sizes are from Chan & Hansen's test set; the true
// solution is the all-ones vector.
void test_levinson_lookahead() {
typedef Matrix<double, Dynamic, 1> Vec;
// Loose bound for these deliberately ill-conditioned look-ahead cases (~1e-9);
// the look-ahead Levinson recursion is weakly stable, so the forward error is a
// large but bounded multiple of epsilon.
const double tol = 5e6 * NumTraits<double>::epsilon();
auto check = [tol](const Vec& c, const Vec& r, Index pmax) {
Toeplitz<double> T(c, r);
Vec xt = Vec::Ones(c.size());
Vec b = T * xt;
LookAheadLevinson<double> lev;
lev.setMaxBlockSize(pmax).compute(T);
VERIFY(lev.info() == Success);
Vec x = lev.solve(b);
VERIFY((x - xt).norm() <= tol * xt.norm());
// The transposed solve must track the same look-ahead block steps.
Matrix<double, Dynamic, Dynamic> dense = T;
Vec bt = dense.transpose() * xt;
Vec y = lev.transpose().solve(bt);
VERIFY((y - xt).norm() <= tol * xt.norm());
};
Vec c1(6), r1(6); // Sweet-1 (block size 2)
c1 << 4, 6, 71.0 / 15 + 5e-8, 5, 3, 1;
r1 << 4, 8, 1, 6, 2, 3;
check(c1, r1, 3);
Vec c2(6), r2(6); // Sweet-2 (block size 2)
c2 << 8, 4, -34 + 5e-13, 5, 3, 1;
r2 << 8, 4, 1, 6, 2, 3;
check(c2, r2, 3);
Vec c3(13), r3(13); // Sweet-3 (block size 6)
c3 << 5, 1, -3, 12.755, -19.656, 28.361, -7, -1, 2, 1, -6, 1, -0.5;
r3 << 5, -1, 6, 2, 5.697, 5.850, 3, -5, -2, -7, 1, 10, -15;
check(c3, r3, 6);
// shifted KMS: leading submatrices T_k with k = 1,4,7,... are singular, so the
// full order n must be a multiple of 3 for T_n itself to be non-singular.
for (Index n : {15, 30, 60}) {
Vec c(n), r(n);
c[0] = r[0] = 1e-14;
for (Index i = 1; i < n; ++i) c[i] = r[i] = std::pow(0.5, double(i - 1));
check(c, r, 3);
}
}
// Fixed-size Toeplitz operators also feed the solver.
void test_levinson_fixed() {
typedef Matrix<double, 12, 1> Vec12;
Vec12 c = Vec12::Random(), r = Vec12::Random();
c[0] = r[0] = 24.0;
Toeplitz<double, 12, 12> T(c, r);
Matrix<double, 12, 12> dense = T;
Vec12 b = Vec12::Random();
LookAheadLevinson<double> lev(T);
VERIFY(lev.info() == Success);
VERIFY_IS_APPROX(lev.solve(b), dense.fullPivLu().solve(b).eval());
}
// Evaluating the Solve expression inside a larger expression (rather than
// assigning it straight to a plain matrix) goes through evaluator<Solve>, which
// caches pointers into its preallocated result before _solve_impl runs:
// _solve_impl must copy into the destination coefficient-wise, never move-assign
// it (a move steals the buffer and leaves the cached pointers dangling). Only a
// matrix right-hand side is affected: a vector one has a different plain type,
// which cannot select the move-assignment. Hankel::solve wraps the Levinson
// solve in exactly such an expression (colwise().reverse()).
template <typename Scalar>
void test_levinson_solve_in_expression(Index n) {
typedef typename NumTraits<Scalar>::Real RealScalar;
typedef Matrix<Scalar, Dynamic, 1> Vec;
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
Vec c = Vec::Random(n), r = Vec::Random(n);
r[0] = c[0] += Scalar(RealScalar(2 * n)); // diagonally dominant => well conditioned
Toeplitz<Scalar> T(c, r);
LookAheadLevinson<Scalar> lev(T);
VERIFY(lev.info() == Success);
Mat B = Mat::Random(n, 3);
Mat direct = lev.solve(B); // plain assignment: solves straight into `direct`
Mat inExpr = lev.solve(B).colwise().reverse();
VERIFY_IS_APPROX(inExpr, direct.colwise().reverse().eval());
Vec b = Vec::Random(n);
Vec x = lev.solve(b).colwise().reverse();
VERIFY_IS_APPROX(x, Vec(Vec(lev.solve(b)).reverse()));
}
// A numerically singular Toeplitz must be reported through info().
void test_levinson_singular() {
typedef Matrix<double, Dynamic, 1> Vec;
for (Index n : {4, 9}) {
Vec c = Vec::Ones(n), r = Vec::Ones(n); // all-ones Toeplitz is rank 1 for n >= 2
LookAheadLevinson<double> lev(Toeplitz<double>(c, r));
VERIFY(lev.info() == NumericalIssue);
}
}
EIGEN_DECLARE_TEST(structured_matrices) {
for (int i = 0; i < g_repeat; ++i) {
// Circulant: direct path (small), FFT path (composite and prime sizes), edge cases.
CALL_SUBTEST_1((test_circulant_product<double>(1)));
CALL_SUBTEST_1((test_circulant_product<double>(2)));
CALL_SUBTEST_1((test_circulant_product<double>(8)));
CALL_SUBTEST_1((test_circulant_product<double>(64)));
CALL_SUBTEST_1((test_circulant_product<double>(97))); // prime: padded embedding product
CALL_SUBTEST_1((test_circulant_product<std::complex<double>>(97)));
CALL_SUBTEST_1((test_circulant_product<float>(48)));
CALL_SUBTEST_1((test_circulant_product<std::complex<double>>(7))); // direct path, complex
CALL_SUBTEST_1((test_circulant_product<std::complex<double>>(50)));
CALL_SUBTEST_1((test_circulant_product<std::complex<float>>(40)));
CALL_SUBTEST_1((test_circulant_solve<double>(1))); // degenerate 1x1 solve
CALL_SUBTEST_1((test_circulant_solve<double>(8)));
CALL_SUBTEST_1((test_circulant_solve<double>(50)));
CALL_SUBTEST_1((test_circulant_solve<double>(97))); // prime: solve keeps the exact-size symbol
CALL_SUBTEST_1((test_circulant_solve<std::complex<double>>(40)));
CALL_SUBTEST_1((test_circulant_solve<float>(32)));
CALL_SUBTEST_1((test_circulant_symbol<double>(16)));
CALL_SUBTEST_1((test_circulant_symbol<std::complex<double>>(12)));
// Toeplitz: square, tall, wide, small (direct), single row/column, real and complex.
CALL_SUBTEST_2((test_toeplitz_product<double>(1, 1)));
CALL_SUBTEST_2((test_toeplitz_product<double>(2, 2)));
CALL_SUBTEST_2((test_toeplitz_product<double>(10, 10)));
CALL_SUBTEST_2((test_toeplitz_product<double>(64, 64)));
CALL_SUBTEST_2((test_toeplitz_product<double>(96, 48)));
CALL_SUBTEST_2((test_toeplitz_product<double>(48, 96)));
CALL_SUBTEST_2((test_toeplitz_product<double>(1, 40))); // single row, FFT path
CALL_SUBTEST_2((test_toeplitz_product<double>(40, 1))); // single column, FFT path
CALL_SUBTEST_2((test_toeplitz_product<float>(50, 50)));
CALL_SUBTEST_2((test_toeplitz_product<std::complex<double>>(5, 7))); // direct path, complex
CALL_SUBTEST_2((test_toeplitz_product<std::complex<double>>(48, 64)));
CALL_SUBTEST_2((test_toeplitz_product<std::complex<float>>(40, 40)));
// Matrix-free iterative solves through the existing solvers.
CALL_SUBTEST_3((test_matrix_free_cg<double>(80)));
CALL_SUBTEST_3((test_matrix_free_gmres<double>(80)));
CALL_SUBTEST_3((test_matrix_free_least_squares<double>(60, 40)));
CALL_SUBTEST_3((test_matrix_free_least_squares<std::complex<double>>(48, 32)));
// Fixed-size operators: small (coeff-based dispatch) and above the FFT threshold.
CALL_SUBTEST_4((test_circulant_fixed<double, 4>()));
CALL_SUBTEST_4((test_circulant_fixed<std::complex<float>, 4>()));
CALL_SUBTEST_4((test_circulant_fixed<double, 48>()));
CALL_SUBTEST_4((test_toeplitz_fixed<double, 4, 6>()));
CALL_SUBTEST_4((test_toeplitz_fixed<double, 40, 24>()));
CALL_SUBTEST_4((test_toeplitz_fixed<std::complex<float>, 6, 4>()));
// Transposition family across the dispatch tiers (scalar, direct, FFT incl. prime).
CALL_SUBTEST_6((test_circulant_transpose<double>(1)));
CALL_SUBTEST_6((test_circulant_transpose<double>(8)));
CALL_SUBTEST_6((test_circulant_transpose<double>(24)));
CALL_SUBTEST_6((test_circulant_transpose<double>(97)));
CALL_SUBTEST_6((test_circulant_transpose<float>(48)));
CALL_SUBTEST_6((test_circulant_transpose<std::complex<double>>(40)));
CALL_SUBTEST_6((test_circulant_transpose<std::complex<float>>(33)));
CALL_SUBTEST_6((test_toeplitz_transpose<double>(1, 1)));
CALL_SUBTEST_6((test_toeplitz_transpose<double>(12, 7)));
CALL_SUBTEST_6((test_toeplitz_transpose<double>(64, 40)));
CALL_SUBTEST_6((test_toeplitz_transpose<double>(40, 64)));
CALL_SUBTEST_6((test_toeplitz_transpose<double>(1, 40)));
CALL_SUBTEST_6((test_toeplitz_transpose<std::complex<double>>(48, 48)));
CALL_SUBTEST_6((test_toeplitz_transpose<std::complex<float>>(20, 36)));
// Closed-form eigendecomposition, SVD, pseudo-inverse solve, inverse, determinant.
CALL_SUBTEST_7((test_circulant_eigen<double>(1)));
CALL_SUBTEST_7((test_circulant_eigen<double>(16)));
CALL_SUBTEST_7((test_circulant_eigen<double>(40)));
CALL_SUBTEST_7((test_circulant_eigen<std::complex<double>>(21)));
CALL_SUBTEST_7((test_circulant_svd<double>(1)));
CALL_SUBTEST_7((test_circulant_svd<double>(18))); // conjugate-pair moduli routinely differ in the last bits
CALL_SUBTEST_7((test_circulant_svd<double>(24)));
CALL_SUBTEST_7((test_circulant_svd<std::complex<double>>(18)));
CALL_SUBTEST_7((test_circulant_svd<float>(12)));
CALL_SUBTEST_7((test_circulant_minnorm_solve<double>(20)));
CALL_SUBTEST_7((test_circulant_minnorm_solve<std::complex<double>>(15)));
CALL_SUBTEST_7((test_circulant_rank_deficient<std::complex<double>>(24, 3)));
CALL_SUBTEST_7((test_circulant_rank_deficient<std::complex<double>>(50, 5)));
CALL_SUBTEST_7((test_circulant_rank_deficient<std::complex<float>>(16, 2)));
CALL_SUBTEST_7((test_circulant_zero<double>(12)));
CALL_SUBTEST_7((test_circulant_zero<std::complex<double>>(7)));
CALL_SUBTEST_7(test_circulant_nan_propagation(20));
CALL_SUBTEST_7((test_circulant_inverse<double>(1)));
CALL_SUBTEST_7((test_circulant_inverse<double>(50)));
CALL_SUBTEST_7((test_circulant_inverse<double>(97))); // prime: inverse rebuilds the padded product symbol
CALL_SUBTEST_7((test_circulant_inverse<std::complex<double>>(20)));
CALL_SUBTEST_7((test_circulant_inverse<float>(16)));
CALL_SUBTEST_7((test_circulant_determinant<double>(1)));
CALL_SUBTEST_7((test_circulant_determinant<double>(12)));
CALL_SUBTEST_7((test_circulant_determinant<std::complex<double>>(9)));
// Numerical and lifetime boundaries: value-nested (owning) delayed products,
// aliased products across the dispatch tiers (scalar, direct, FFT), the
// balanced determinant accumulation, and the rank threshold boundary.
CALL_SUBTEST_8((test_circulant_delayed_product<double>(24)));
CALL_SUBTEST_8((test_circulant_delayed_product<std::complex<double>>(48)));
CALL_SUBTEST_8((test_toeplitz_delayed_product<double>(40, 24)));
CALL_SUBTEST_8((test_toeplitz_delayed_product<std::complex<float>>(12, 8)));
CALL_SUBTEST_8((test_circulant_aliased_product<double>(8)));
CALL_SUBTEST_8((test_circulant_aliased_product<double>(64)));
CALL_SUBTEST_8((test_circulant_aliased_product<std::complex<double>>(40)));
CALL_SUBTEST_8((test_toeplitz_aliased_product<double>(12)));
CALL_SUBTEST_8((test_toeplitz_aliased_product<double>(48)));
CALL_SUBTEST_8((test_toeplitz_aliased_product<std::complex<float>>(36)));
CALL_SUBTEST_8((test_circulant_mixed_scalar<double>(8)));
CALL_SUBTEST_8((test_circulant_mixed_scalar<double>(24)));
CALL_SUBTEST_8((test_circulant_mixed_scalar<double>(64)));
CALL_SUBTEST_8((test_circulant_mixed_scalar<float>(97)));
CALL_SUBTEST_8((test_toeplitz_mixed_scalar<double>(8, 8)));
CALL_SUBTEST_8((test_toeplitz_mixed_scalar<double>(64, 40)));
CALL_SUBTEST_8(test_circulant_determinant_scaled());
CALL_SUBTEST_8(test_circulant_rank_boundaries());
CALL_SUBTEST_8(test_circulant_rank_complex_boundary());
CALL_SUBTEST_8((test_structured_fft_overflow<double>(40)));
CALL_SUBTEST_8((test_structured_fft_overflow<double>(97))); // prime, no 5-smooth padding
CALL_SUBTEST_8((test_structured_fft_overflow<float>(40)));
CALL_SUBTEST_8((test_structured_fft_complex_boundary<double>(40)));
CALL_SUBTEST_8((test_structured_fft_complex_boundary<float>(40)));
// Aliasing beyond the same-object case, across the dispatch tiers.
CALL_SUBTEST_8((test_structured_aliased_expression<double>(8)));
CALL_SUBTEST_8((test_structured_aliased_expression<double>(24)));
CALL_SUBTEST_8((test_structured_aliased_expression<double>(48)));
CALL_SUBTEST_8((test_structured_aliased_expression<std::complex<double>>(40)));
// Entrywise Inf/NaN propagation: FFT-sized operators must fall back to the
// direct kernel; small ones are IEEE-exact already.
CALL_SUBTEST_8((test_structured_nonfinite_product<double>(40)));
CALL_SUBTEST_8((test_structured_nonfinite_product<double>(12)));
CALL_SUBTEST_8((test_structured_nonfinite_product<std::complex<double>>(40)));
// Look-ahead Levinson direct Toeplitz solver, one subtest per scalar type:
// each type pulls in its own FullPivLU reference solves for vector and matrix
// right-hand sides, which together made this the most memory-hungry
// translation unit in the test suite.
CALL_SUBTEST_5((test_levinson_wellcond<double>(1)));
CALL_SUBTEST_5((test_levinson_wellcond<double>(2)));
CALL_SUBTEST_5((test_levinson_wellcond<double>(20)));
CALL_SUBTEST_5((test_levinson_wellcond<double>(60)));
CALL_SUBTEST_11((test_levinson_wellcond<float>(40)));
CALL_SUBTEST_12((test_levinson_wellcond<std::complex<double>>(30)));
CALL_SUBTEST_12((test_levinson_solve_in_expression<std::complex<double>>(16)));
CALL_SUBTEST_13(test_levinson_lookahead());
CALL_SUBTEST_13(test_levinson_fixed());
CALL_SUBTEST_13(test_levinson_singular());
CALL_SUBTEST_13((test_levinson_solve_in_expression<double>(1)));
CALL_SUBTEST_13((test_levinson_solve_in_expression<double>(24)));
CALL_SUBTEST_14((test_levinson_wellcond<std::complex<float>>(24)));
// Hankel: products across dispatch tiers, transposition family (validating the
// phase-multiplication symbol reuse on rectangular FFT-tier operators), the
// Toeplitz equivalence, direct solves, matrix-free least squares, fixed sizes.
CALL_SUBTEST_9((test_hankel_product<double>(1, 1)));
CALL_SUBTEST_9((test_hankel_product<double>(2, 3)));
CALL_SUBTEST_9((test_hankel_product<double>(8, 8)));
CALL_SUBTEST_9((test_hankel_product<double>(12, 7)));
CALL_SUBTEST_9((test_hankel_product<double>(17, 16))); // just above the scalar tier
CALL_SUBTEST_9((test_hankel_product<double>(20, 24))); // direct segment tier
CALL_SUBTEST_9((test_hankel_product<double>(32, 33))); // first FFT-tier size
CALL_SUBTEST_9((test_hankel_product<double>(64, 40)));
CALL_SUBTEST_9((test_hankel_product<double>(40, 64)));
CALL_SUBTEST_9((test_hankel_product<double>(97, 50)));
CALL_SUBTEST_9((test_hankel_product<double>(1, 40))); // single row: direct O(n) path
CALL_SUBTEST_9((test_hankel_product<double>(40, 1))); // single column: direct O(n) path
CALL_SUBTEST_9((test_hankel_product<double>(1, 400))); // skinny far beyond the FFT threshold
CALL_SUBTEST_9((test_hankel_product<double>(400, 1)));
CALL_SUBTEST_9((test_hankel_product<float>(50, 50)));
CALL_SUBTEST_9((test_hankel_product<std::complex<double>>(5, 7)));
CALL_SUBTEST_9((test_hankel_product<std::complex<double>>(48, 64)));
CALL_SUBTEST_9((test_hankel_product<std::complex<float>>(40, 40)));
CALL_SUBTEST_9((test_hankel_transpose<double>(1, 1)));
CALL_SUBTEST_9((test_hankel_transpose<double>(12, 7)));
CALL_SUBTEST_9((test_hankel_transpose<double>(20, 24))); // direct tier, empty symbol
CALL_SUBTEST_9((test_hankel_transpose<double>(64, 40)));
CALL_SUBTEST_9((test_hankel_transpose<double>(40, 64)));
CALL_SUBTEST_9((test_hankel_transpose<double>(1, 40)));
CALL_SUBTEST_9((test_hankel_transpose<std::complex<double>>(33, 20)));
CALL_SUBTEST_9((test_hankel_transpose<std::complex<float>>(20, 36)));
CALL_SUBTEST_9((test_hankel_symmetry<double>(48)));
CALL_SUBTEST_9((test_hankel_to_toeplitz<double>(12, 7)));
CALL_SUBTEST_9((test_hankel_to_toeplitz<std::complex<double>>(40, 56)));
CALL_SUBTEST_9((test_hankel_solve<double>(1)));
CALL_SUBTEST_9((test_hankel_solve<double>(20)));
CALL_SUBTEST_9((test_hankel_solve<double>(45)));
CALL_SUBTEST_9((test_hankel_solve<std::complex<double>>(24)));
CALL_SUBTEST_9((test_hankel_solve<float>(16)));
CALL_SUBTEST_9((test_hankel_solve_lookahead<double>(20)));
CALL_SUBTEST_9((test_hankel_solve_lookahead<std::complex<double>>(15)));
CALL_SUBTEST_9(test_hankel_hilbert());
CALL_SUBTEST_9((test_hankel_least_squares<double>(60, 40)));
CALL_SUBTEST_9((test_hankel_least_squares<std::complex<double>>(48, 32)));
CALL_SUBTEST_9((test_hankel_fixed<double, 4, 6>()));
CALL_SUBTEST_9((test_hankel_fixed<double, 40, 24>()));
CALL_SUBTEST_9((test_hankel_fixed<std::complex<float>, 6, 4>()));
CALL_SUBTEST_9((test_hankel_fixed_solve<double, 8>()));
CALL_SUBTEST_9((test_hankel_fixed_solve<std::complex<double>, 12>()));
// MR 2688 review regressions: finite-overflow scaling in the FFT tier,
// delayed (value-nested) products, aliased right-hand sides across the
// dispatch tiers, and mixed real/complex products.
CALL_SUBTEST_10(test_hankel_fft_overflow());
CALL_SUBTEST_10((test_hankel_fft_complex_boundary<double>(40)));
CALL_SUBTEST_10((test_hankel_fft_complex_boundary<float>(40)));
CALL_SUBTEST_10((test_hankel_delayed_product<double>(24, 12)));
CALL_SUBTEST_10((test_hankel_delayed_product<double>(40, 64)));
CALL_SUBTEST_10((test_hankel_delayed_product<std::complex<double>>(48, 33)));
CALL_SUBTEST_10((test_hankel_aliased_product<double>(8))); // scalar tier
CALL_SUBTEST_10((test_hankel_aliased_product<double>(24))); // direct segment tier
CALL_SUBTEST_10((test_hankel_aliased_product<double>(64))); // FFT tier
CALL_SUBTEST_10((test_hankel_aliased_product<std::complex<double>>(40)));
// Aliasing beyond the same-object case, across the dispatch tiers.
CALL_SUBTEST_10((test_hankel_aliased_expression<double>(8)));
CALL_SUBTEST_10((test_hankel_aliased_expression<double>(24)));
CALL_SUBTEST_10((test_hankel_aliased_expression<double>(48)));
CALL_SUBTEST_10((test_hankel_aliased_expression<std::complex<double>>(40)));
// Entrywise Inf/NaN propagation: FFT-sized operators must fall back to the
// direct kernels; small ones are IEEE-exact already.
CALL_SUBTEST_10((test_hankel_nonfinite_product<double>(40)));
CALL_SUBTEST_10((test_hankel_nonfinite_product<double>(12)));
CALL_SUBTEST_10((test_hankel_nonfinite_product<std::complex<double>>(40)));
CALL_SUBTEST_10((test_hankel_mixed_scalar<double>(8, 8))); // scalar tier
CALL_SUBTEST_10((test_hankel_mixed_scalar<double>(24, 20))); // direct segment tier
CALL_SUBTEST_10((test_hankel_mixed_scalar<double>(64, 40))); // FFT tier
CALL_SUBTEST_10((test_hankel_mixed_scalar<double>(1, 40))); // skinny direct paths
CALL_SUBTEST_10((test_hankel_mixed_scalar<double>(40, 1)));
CALL_SUBTEST_10((test_hankel_mixed_scalar<float>(48, 64)));
}
}