| // 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))); |
| } |
| } |