// 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 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 Matrix reference_circulant(const Matrix& c) { const Index n = c.size(); Matrix 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 Matrix reference_toeplitz(const Matrix& c, const Matrix& r) { const Index m = c.size(), n = r.size(); Matrix 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 void test_circulant_product(Index n) { typedef Matrix Vec; typedef Matrix Mat; Vec c = Vec::Random(n); Circulant C(c); Mat dense = reference_circulant(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(0, n - 1), j = internal::random(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 void test_circulant_solve(Index n) { typedef typename NumTraits::Real RealScalar; typedef Matrix Vec; typedef Matrix Mat; // Diagonally dominant => well conditioned, so the direct FFT solve is accurate. Vec c = Vec::Random(n); c[0] += Scalar(RealScalar(2 * n)); Circulant C(c); Mat dense = reference_circulant(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 void test_circulant_symbol(Index n) { typedef typename NumTraits::Real RealScalar; typedef std::complex Complex; typedef Matrix Vec; typedef Matrix ComplexVec; Vec c = Vec::Random(n); Circulant C(c); VERIFY_IS_EQUAL(C.symbol().size(), n); Matrix denseC = reference_circulant(c).template cast(); const Index step = numext::maxi(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 void test_toeplitz_product(Index m, Index n) { typedef Matrix Vec; typedef Matrix Mat; Vec c = Vec::Random(m), r = Vec::Random(n); r[0] = c[0]; // diagonal entry; r[0] is ignored anyway Toeplitz T(c, r); Mat dense = reference_toeplitz(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()); } // 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 void test_circulant_fixed() { typedef typename NumTraits::Real RealScalar; typedef Matrix VecN; typedef Matrix Vec; typedef Matrix MatN; VecN c = VecN::Random(); c[0] += Scalar(RealScalar(2 * N)); // well conditioned for the solve below Circulant C(c); STATIC_CHECK((Circulant::RowsAtCompileTime == N)); STATIC_CHECK((internal::remove_all_t::RowsAtCompileTime == N)); MatN dense = C; VERIFY_IS_APPROX(dense, MatN(reference_circulant(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); } template void test_toeplitz_fixed() { typedef Matrix ColVec; typedef Matrix RowVec; typedef Matrix Vec; typedef Matrix MatMN; ColVec c = ColVec::Random(); RowVec r = RowVec::Random(); r[0] = c[0]; Toeplitz T(c, r); STATIC_CHECK((Toeplitz::RowsAtCompileTime == M)); STATIC_CHECK((Toeplitz::ColsAtCompileTime == N)); STATIC_CHECK((internal::remove_all_t::ColsAtCompileTime == N)); MatMN dense = T; VERIFY_IS_APPROX(dense, MatMN(reference_toeplitz(Vec(c), Vec(r)))); RowVec x = RowVec::Random(); Matrix y = T * x; VERIFY_IS_APPROX(y, (dense * x).eval()); } template void test_matrix_free_cg(Index n) { typedef Matrix Vec; typedef Matrix 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 C(c); Mat dense = reference_circulant(c); Vec b = Vec::Random(n); ConjugateGradient, Lower | Upper, IdentityPreconditioner> cg; cg.compute(C); Vec x = cg.solve(b); VERIFY(cg.info() == Success); VERIFY_IS_APPROX((dense * x).eval(), b); } template void test_matrix_free_gmres(Index n) { typedef typename NumTraits::Real RealScalar; typedef Matrix Vec; typedef Matrix 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 T(c, r); Mat dense = reference_toeplitz(c, r); Vec b = Vec::Random(n); GMRES, IdentityPreconditioner> gmres; gmres.compute(T); Vec x = gmres.solve(b); VERIFY(gmres.info() == Success); VERIFY_IS_APPROX((dense * x).eval(), b); } // Diagonally dominant (well-conditioned) Toeplitz: the look-ahead solver must agree // with a dense LU solve. template void test_levinson_wellcond(Index n) { typedef typename NumTraits::Real RealScalar; typedef Matrix Vec; typedef Matrix Mat; Vec c = Vec::Random(n), r = Vec::Random(n); c[0] = r[0] = Scalar(RealScalar(2 * n)); Toeplitz T(c, r); Mat dense = T; Vec b = Vec::Random(n); LookAheadLevinson 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 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::epsilon(); auto check = [tol](const Vec& c, const Vec& r, Index pmax) { Toeplitz T(c, r); Vec xt = Vec::Ones(c.size()); Vec b = T * xt; LookAheadLevinson 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 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 Vec12; Vec12 c = Vec12::Random(), r = Vec12::Random(); c[0] = r[0] = 24.0; Toeplitz T(c, r); Matrix dense = T; Vec12 b = Vec12::Random(); LookAheadLevinson lev(T); VERIFY(lev.info() == Success); VERIFY_IS_APPROX(lev.solve(b), dense.fullPivLu().solve(b).eval()); } // A numerically singular Toeplitz must be reported through info(). void test_levinson_singular() { typedef Matrix 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 lev(Toeplitz(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(1))); CALL_SUBTEST_1((test_circulant_product(2))); CALL_SUBTEST_1((test_circulant_product(8))); CALL_SUBTEST_1((test_circulant_product(64))); CALL_SUBTEST_1((test_circulant_product(97))); // prime, FFT path CALL_SUBTEST_1((test_circulant_product(48))); CALL_SUBTEST_1((test_circulant_product>(7))); // direct path, complex CALL_SUBTEST_1((test_circulant_product>(50))); CALL_SUBTEST_1((test_circulant_product>(40))); CALL_SUBTEST_1((test_circulant_solve(1))); // degenerate 1x1 solve CALL_SUBTEST_1((test_circulant_solve(8))); CALL_SUBTEST_1((test_circulant_solve(50))); CALL_SUBTEST_1((test_circulant_solve>(40))); CALL_SUBTEST_1((test_circulant_solve(32))); CALL_SUBTEST_1((test_circulant_symbol(16))); CALL_SUBTEST_1((test_circulant_symbol>(12))); // Toeplitz: square, tall, wide, small (direct), single row/column, real and complex. CALL_SUBTEST_2((test_toeplitz_product(1, 1))); CALL_SUBTEST_2((test_toeplitz_product(2, 2))); CALL_SUBTEST_2((test_toeplitz_product(10, 10))); CALL_SUBTEST_2((test_toeplitz_product(64, 64))); CALL_SUBTEST_2((test_toeplitz_product(96, 48))); CALL_SUBTEST_2((test_toeplitz_product(48, 96))); CALL_SUBTEST_2((test_toeplitz_product(1, 40))); // single row, FFT path CALL_SUBTEST_2((test_toeplitz_product(40, 1))); // single column, FFT path CALL_SUBTEST_2((test_toeplitz_product(50, 50))); CALL_SUBTEST_2((test_toeplitz_product>(5, 7))); // direct path, complex CALL_SUBTEST_2((test_toeplitz_product>(48, 64))); CALL_SUBTEST_2((test_toeplitz_product>(40, 40))); // Matrix-free iterative solves through the existing solvers. CALL_SUBTEST_3((test_matrix_free_cg(80))); CALL_SUBTEST_3((test_matrix_free_gmres(80))); // Fixed-size operators: small (coeff-based dispatch) and above the FFT threshold. CALL_SUBTEST_4((test_circulant_fixed())); CALL_SUBTEST_4((test_circulant_fixed, 4>())); CALL_SUBTEST_4((test_circulant_fixed())); CALL_SUBTEST_4((test_toeplitz_fixed())); CALL_SUBTEST_4((test_toeplitz_fixed())); CALL_SUBTEST_4((test_toeplitz_fixed, 6, 4>())); // Look-ahead Levinson direct Toeplitz solver. CALL_SUBTEST_5((test_levinson_wellcond(1))); CALL_SUBTEST_5((test_levinson_wellcond(2))); CALL_SUBTEST_5((test_levinson_wellcond(20))); CALL_SUBTEST_5((test_levinson_wellcond(60))); CALL_SUBTEST_5((test_levinson_wellcond(40))); CALL_SUBTEST_5((test_levinson_wellcond>(30))); CALL_SUBTEST_5((test_levinson_wellcond>(24))); CALL_SUBTEST_5(test_levinson_lookahead()); CALL_SUBTEST_5(test_levinson_fixed()); CALL_SUBTEST_5(test_levinson_singular()); } }